diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index c78a274..9f78c3f 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -20,3 +20,13 @@ - manual types: - yaml +- id: check-pipeline-name-only-steps + name: check pipeline name-only steps + description: check that pipeline steps don't have only a name without uses or other details + entry: check-pipeline-name-only-steps + language: python + stages: + - pre-commit + - manual + types: + - yaml diff --git a/pre_commit_hooks/check_pipeline_name_only_steps.py b/pre_commit_hooks/check_pipeline_name_only_steps.py new file mode 100644 index 0000000..c110f00 --- /dev/null +++ b/pre_commit_hooks/check_pipeline_name_only_steps.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import argparse +import sys +from collections.abc import Sequence +from typing import Any + +import ruamel.yaml + +yaml = ruamel.yaml.YAML(typ="safe") + + +def check_pipeline_steps(melange_cfg: dict[str, Any]) -> tuple[bool, list[str]]: + """ + Check if any pipeline steps have only a 'name' field without 'uses' or other details. + Returns (is_valid, list_of_issues). + """ + issues = [] + + # Check main pipeline + pipelines = melange_cfg.get("pipeline", []) + for i, step in enumerate(pipelines): + if isinstance(step, dict): + # Check if step has only 'name' and no 'uses' + if "name" in step and "uses" not in step and len(step) == 1: + step_name = step.get("name", f"step {i}") + issues.append( + f"main pipeline step '{step_name}' has only a name with no 'uses' or other details", + ) + + # Check test pipeline + test_section = melange_cfg.get("test", {}) + test_pipelines = test_section.get("pipeline", []) + for i, step in enumerate(test_pipelines): + if isinstance(step, dict): + # Check if step has only 'name' and no 'uses' + if "name" in step and "uses" not in step and len(step) == 1: + step_name = step.get("name", f"step {i}") + issues.append( + f"test pipeline step '{step_name}' has only a name with no 'uses' or other details", + ) + + # Check each subpackage + for sub_idx, subpkg in enumerate(melange_cfg.get("subpackages", [])): + subpkg_name = subpkg.get("name", f"subpackage-{sub_idx}") + + # Check subpackage pipelines + subpkg_pipelines = subpkg.get("pipeline", []) + for i, step in enumerate(subpkg_pipelines): + if isinstance(step, dict): + # Check if step has only 'name' and no 'uses' + if "name" in step and "uses" not in step and len(step) == 1: + step_name = step.get("name", f"step {i}") + issues.append( + f"subpackage '{subpkg_name}' pipeline step '{step_name}' has only a name with no 'uses' or other details", + ) + + # Check subpackage test pipelines + subpkg_test_section = subpkg.get("test", {}) + subpkg_test_pipelines = subpkg_test_section.get("pipeline", []) + for i, step in enumerate(subpkg_test_pipelines): + if isinstance(step, dict): + # Check if step has only 'name' and no 'uses' + if "name" in step and "uses" not in step and len(step) == 1: + step_name = step.get("name", f"step {i}") + issues.append( + f"subpackage '{subpkg_name}' test pipeline step '{step_name}' has only a name with no 'uses' or other details", + ) + + return len(issues) == 0, issues + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Check that pipeline steps don't have only a name without uses or other details", + ) + parser.add_argument("filenames", nargs="*", help="Filenames to check") + args = parser.parse_args(argv) + + retval = 0 + + for filename in args.filenames: + try: + with open(filename) as f: + melange_cfg = yaml.load(f) + except Exception as e: + print(f"Error loading {filename}: {e}") + retval = 1 + continue + + if not melange_cfg: + continue + + is_valid, issues = check_pipeline_steps(melange_cfg) + if not is_valid: + for issue in issues: + print(f"{filename}: {issue}") + retval = 1 + + return retval + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/setup.cfg b/setup.cfg index b508882..264145e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,7 @@ python_requires = >=3.9 [options.entry_points] console_scripts = shellcheck-run-steps = pre_commit_hooks.shellcheck_run_steps:main + check-pipeline-name-only-steps = pre_commit_hooks.check_pipeline_name_only_steps:main [bdist_wheel] universal = True diff --git a/test-data/README.md b/test-data/README.md new file mode 100644 index 0000000..ab0538c --- /dev/null +++ b/test-data/README.md @@ -0,0 +1,29 @@ +# Test Data for Pre-commit Hooks + +This directory contains sample YAML files that can be used to test the pre-commit hooks in this repository. + +## Testing hooks locally + +To test a specific hook against a test file, use the `pre-commit try-repo` command: + +```bash +# From any directory with YAML files to test: +pre-commit try-repo /path/to/this/repo HOOK_ID --files FILE_TO_TEST + +# Example for check-pipeline-name-only-steps: +pre-commit try-repo /home/amber-arcadia/Documents/GitRepos/pre-commit-hooks \ + check-pipeline-name-only-steps \ + --files test-data/pipeline-name-only-bad.yaml +``` + +## Test files + +### pipeline-name-only-bad.yaml +- **Tests**: `check-pipeline-name-only-steps` +- **Expected**: Should FAIL +- **Issues**: Contains pipeline steps that have only a `name` field without `uses` or other details + +### pipeline-name-only-good.yaml +- **Tests**: `check-pipeline-name-only-steps` +- **Expected**: Should PASS +- **Issues**: None - all pipeline steps are properly formatted \ No newline at end of file diff --git a/test-data/pipeline-name-only-bad.yaml b/test-data/pipeline-name-only-bad.yaml new file mode 100644 index 0000000..1efb093 --- /dev/null +++ b/test-data/pipeline-name-only-bad.yaml @@ -0,0 +1,38 @@ +package: + name: test-package + version: "1.0.0" + epoch: 0 + description: "Test package with incorrectly formatted pipeline steps" + copyright: + - license: Apache-2.0 + +environment: + contents: + packages: + - busybox + +pipeline: + - name: test/go-fips-check # BAD: This should be 'uses' not 'name' + - name: "Configure build" + uses: autoconf/configure + with: + opts: --enable-shared + - uses: autoconf/make + +test: + pipeline: + - name: run-tests # BAD: Only has name, no uses + - uses: test/daemon-check-output + with: + expected_output: | + Server started + +subpackages: + - name: test-subpkg + description: "Subpackage with bad pipeline" + pipeline: + - name: bad-subpkg-step # BAD: Only has name + - uses: split/dev + test: + pipeline: + - name: subpkg-test-step # BAD: Only has name in test section \ No newline at end of file diff --git a/test-data/pipeline-name-only-good.yaml b/test-data/pipeline-name-only-good.yaml new file mode 100644 index 0000000..1a9183f --- /dev/null +++ b/test-data/pipeline-name-only-good.yaml @@ -0,0 +1,48 @@ +package: + name: test-package-good + version: "1.0.0" + epoch: 0 + description: "Test package with correctly formatted pipeline steps" + copyright: + - license: Apache-2.0 + +environment: + contents: + packages: + - busybox + - go-fips-1.22 + +pipeline: + - uses: test/go-fips-check # GOOD: Uses 'uses' instead of 'name' + - name: "Configure build" + uses: autoconf/configure # GOOD: Has both name and uses + with: + opts: --enable-shared + - uses: autoconf/make # GOOD: Just uses is fine + - name: "Run custom script" # GOOD: Has name and runs + runs: | + echo "Building package" + make install + +test: + pipeline: + - uses: test/go-fips-check # GOOD: Properly uses 'uses' + - name: "Test daemon output" + uses: test/daemon-check-output # GOOD: Has both name and uses + with: + expected_output: | + Server started + - uses: test/emptypackage # GOOD: Just uses + +subpackages: + - name: test-subpkg-good + description: "Subpackage with correct pipeline" + pipeline: + - uses: split/dev # GOOD: Uses 'uses' + - name: "Move files" + runs: | # GOOD: Has name and runs + mkdir -p ${{targets.subpkgdir}}/usr/bin + mv usr/bin/tool ${{targets.subpkgdir}}/usr/bin/ + test: + pipeline: + - uses: test/emptypackage # GOOD: Uses 'uses' in test section \ No newline at end of file