From f77be7f9137595318079876171a3e3f4e9c02d96 Mon Sep 17 00:00:00 2001 From: dann frazier Date: Thu, 8 May 2025 11:58:16 -0600 Subject: [PATCH] Add a hook to run shellcheck on all "runs" steps in melange pipelines Signed-off-by: dann frazier --- .pre-commit-hooks.yaml | 6 ++ pre_commit_hooks/__init__.py | 0 pre_commit_hooks/shellcheck_run_steps.py | 86 ++++++++++++++++++++++++ setup.cfg | 44 ++++++++++++ setup.py | 4 ++ 5 files changed, 140 insertions(+) create mode 100644 pre_commit_hooks/__init__.py create mode 100644 pre_commit_hooks/shellcheck_run_steps.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 67ce835..7becda0 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -10,3 +10,9 @@ - manual types: - yaml +- id: shellcheck-run-steps + name: shellcheck run steps + description: run shellcheck on each "run" step in a melange pipeline + entry: shellcheck-run-steps + language: python + types: [yaml] diff --git a/pre_commit_hooks/__init__.py b/pre_commit_hooks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pre_commit_hooks/shellcheck_run_steps.py b/pre_commit_hooks/shellcheck_run_steps.py new file mode 100644 index 0000000..18c2a9d --- /dev/null +++ b/pre_commit_hooks/shellcheck_run_steps.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import argparse +import subprocess +import tempfile +from collections.abc import Generator +from collections.abc import Sequence +from typing import Any +from typing import NamedTuple + +import ruamel.yaml + +yaml = ruamel.yaml.YAML(typ='safe') + + +def _exhaust(gen: Generator[str]) -> None: + for _ in gen: + pass + + +def _parse_unsafe(*args: Any, **kwargs: Any) -> None: + _exhaust(yaml.parse(*args, **kwargs)) + + +def _load_all(*args: Any, **kwargs: Any) -> None: + _exhaust(yaml.load_all(*args, **kwargs)) + + +class Key(NamedTuple): + multi: bool + unsafe: bool + + +def do_shellcheck(melange_cfg): + if melange_cfg == {}: + return 0 + + pkgs = [melange_cfg] + pkgs.extend(melange_cfg.get('subpackages', [])) + pipelines = [] + for pkg in pkgs: + pipelines.extend(pkg.get('pipeline', [])) + if 'test' in pkg.keys(): + test_pipeline = pkg['test'].get('pipeline', []) + pipelines.extend(test_pipeline) + + for step in pipelines: + if 'runs' not in step.keys(): + continue + with tempfile.NamedTemporaryFile(mode='w') as shfile: + shfile.write(step['runs']) + subprocess.check_call( + ['shellcheck', '--shell=busybox', shfile.name] + ) + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', help='Filenames to check.') + args = parser.parse_args(argv) + + melange_cfg = {} + for filename in args.filenames: + with tempfile.NamedTemporaryFile( + 'w', delete_on_close=False + ) as compiled_out: + subprocess.check_call( + [ + 'melange', 'compile', '--arch', 'x86_64', + '--pipeline-dir', './pipelines', filename, + ], + stdout=compiled_out, + ) + compiled_out.close() + try: + with open(compiled_out.name, 'r') as compiled_in: + melange_cfg = yaml.load(compiled_in) + do_shellcheck(melange_cfg) + except ruamel.yaml.YAMLError as exc: + print(exc) + return 1 + + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b508882 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,44 @@ +[metadata] +name = pre_commit_hooks +version = 0.0.1 +description = chainguard hooks for pre-commit +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/chainguard-dev/pre-commit-hooks +license = MIT +license_files = LICENSE +classifiers = + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + +[options] +packages = find: +install_requires = + ruamel.yaml>=0.15 +python_requires = >=3.9 + +[options.entry_points] +console_scripts = + shellcheck-run-steps = pre_commit_hooks.shellcheck_run_steps:main + +[bdist_wheel] +universal = True + +[coverage:run] +plugins = covdefaults + +[mypy] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +warn_redundant_casts = true +warn_unused_ignores = true + +[mypy-testing.*] +disallow_untyped_defs = false + +[mypy-tests.*] +disallow_untyped_defs = false diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3d93aef --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from setuptools import setup +setup()