From c129857515495bec3785a31670c02e46e8c94d64 Mon Sep 17 00:00:00 2001 From: Amber Arcadia Date: Mon, 11 Aug 2025 12:31:34 -0400 Subject: [PATCH 1/7] Add check-for-go-fips-test pre-commit hook Implements GitHub issue requirements: - Detects packages using go-fips in environment or pipeline - Verifies they have corresponding go-fips test (uses: test/go-fips-check) - Reports failures for packages missing the required test --- .gitignore | 1 + .pre-commit-hooks.yaml | 4 ++ example.pre-commit-config.yaml | 1 + pre_commit_hooks/check_for_go_fips_test.py | 83 ++++++++++++++++++++++ setup.cfg | 1 + 5 files changed, 90 insertions(+) create mode 100644 .gitignore create mode 100644 pre_commit_hooks/check_for_go_fips_test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index b3cea9b..5cc3547 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -33,6 +33,10 @@ 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 +- id: check-for-go-fips-test + name: check for go-fips test + description: Check that packages using go-fips have corresponding go-fips tests + entry: check-for-go-fips-test language: python stages: - pre-commit diff --git a/example.pre-commit-config.yaml b/example.pre-commit-config.yaml index d462eb3..f07c8c7 100644 --- a/example.pre-commit-config.yaml +++ b/example.pre-commit-config.yaml @@ -19,6 +19,7 @@ repos: [^/]+\.ya?ml # matches .yaml or .yml files at the top level only )$ - id: check-pipeline-name-only-steps + - id: check-for-go-fips-test files: '^[^.][^/]*\.yaml$' # matches non-hidden .yaml files at the top level only - repo: https://github.com/chainguard-dev/yam rev: 768695300c5f663012a77911eb4920c12e5ed2e5 # frozen: v0.2.26 diff --git a/pre_commit_hooks/check_for_go_fips_test.py b/pre_commit_hooks/check_for_go_fips_test.py new file mode 100644 index 0000000..2183846 --- /dev/null +++ b/pre_commit_hooks/check_for_go_fips_test.py @@ -0,0 +1,83 @@ +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 uses_go_fips(melange_cfg: dict[str, Any]) -> bool: + """Check if package uses go-fips.""" + # Check environment packages + env_packages = melange_cfg.get("environment", {}).get("contents", {}).get("packages", []) + if "go-fips" in env_packages: + return True + + # Check pipeline steps for go/build with go-package: go-fips + pipelines = melange_cfg.get("pipeline", []) + for step in pipelines: + if step.get("uses") == "go/build": + if step.get("with", {}).get("go-package") == "go-fips": + return True + + # Check subpackage pipelines + for subpkg in melange_cfg.get("subpackages", []): + subpkg_pipelines = subpkg.get("pipeline", []) + for step in subpkg_pipelines: + if step.get("uses") == "go/build": + if step.get("with", {}).get("go-package") == "go-fips": + return True + + return False + + +def has_go_fips_test(melange_cfg: dict[str, Any]) -> bool: + """Check if package has go-fips test.""" + test_section = melange_cfg.get("test", {}) + test_pipelines = test_section.get("pipeline", []) + + for step in test_pipelines: + if step.get("uses") == "test/go-fips-check": + return True + + return False + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Check that packages using go-fips have corresponding go-fips tests" + ) + 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 + + if uses_go_fips(melange_cfg): + if not has_go_fips_test(melange_cfg): + print( + f"{filename}: Package uses go-fips but does not have " + "a corresponding go-fips test (add '- uses: test/go-fips-check' to test pipeline)" + ) + retval = 1 + + return retval + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 264145e..47ec3fd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ python_requires = >=3.9 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 + check-for-go-fips-test = pre_commit_hooks.check_for_go_fips_test:main [bdist_wheel] universal = True From 2292b52b1421a743c8a2cd1c7993c69d147de083 Mon Sep 17 00:00:00 2001 From: Amber Arcadia Date: Mon, 11 Aug 2025 13:55:15 -0400 Subject: [PATCH 2/7] Improve go-fips detection for all variants and subpackages - Detect any go-fips variant (go-fips, go-fips-md5, etc.) using startswith() - Check each package/subpackage independently for compliance - Require matching tests for each go-fips usage (not just any test) - Provide detailed error messages specifying which component lacks tests - Handle subpackage test sections properly Fixes comprehensive validation of go-fips test requirements. --- pre_commit_hooks/check_for_go_fips_test.py | 90 ++++++++++++++-------- 1 file changed, 59 insertions(+), 31 deletions(-) diff --git a/pre_commit_hooks/check_for_go_fips_test.py b/pre_commit_hooks/check_for_go_fips_test.py index 2183846..10c2af7 100644 --- a/pre_commit_hooks/check_for_go_fips_test.py +++ b/pre_commit_hooks/check_for_go_fips_test.py @@ -10,41 +10,71 @@ yaml = ruamel.yaml.YAML(typ="safe") -def uses_go_fips(melange_cfg: dict[str, Any]) -> bool: - """Check if package uses go-fips.""" - # Check environment packages +def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[str]]: + """ + Check if all go-fips usages have corresponding tests. + Returns (is_compliant, list_of_missing_tests). + """ + issues = [] + + # Check if main package uses go-fips + main_uses_fips = False + main_has_test = False + + # Check environment packages for any go-fips variant env_packages = melange_cfg.get("environment", {}).get("contents", {}).get("packages", []) - if "go-fips" in env_packages: - return True + for pkg in env_packages: + if pkg.startswith("go-fips"): + main_uses_fips = True + break - # Check pipeline steps for go/build with go-package: go-fips + # Check main pipeline steps for go/build with go-package: go-fips* pipelines = melange_cfg.get("pipeline", []) for step in pipelines: if step.get("uses") == "go/build": - if step.get("with", {}).get("go-package") == "go-fips": - return True - - # Check subpackage pipelines - for subpkg in melange_cfg.get("subpackages", []): - subpkg_pipelines = subpkg.get("pipeline", []) - for step in subpkg_pipelines: - if step.get("uses") == "go/build": - if step.get("with", {}).get("go-package") == "go-fips": - return True + go_package = step.get("with", {}).get("go-package", "") + if go_package.startswith("go-fips"): + main_uses_fips = True + break - return False - - -def has_go_fips_test(melange_cfg: dict[str, Any]) -> bool: - """Check if package has go-fips test.""" + # Check main test section test_section = melange_cfg.get("test", {}) test_pipelines = test_section.get("pipeline", []) - for step in test_pipelines: if step.get("uses") == "test/go-fips-check": - return True + main_has_test = True + break + + if main_uses_fips and not main_has_test: + issues.append("main package uses go-fips but lacks test/go-fips-check") - return False + # Check each subpackage + for i, subpkg in enumerate(melange_cfg.get("subpackages", [])): + subpkg_uses_fips = False + subpkg_has_test = False + subpkg_name = subpkg.get("name", f"subpackage-{i}") + + # Check subpackage pipelines for go-fips usage + subpkg_pipelines = subpkg.get("pipeline", []) + for step in subpkg_pipelines: + if step.get("uses") == "go/build": + go_package = step.get("with", {}).get("go-package", "") + if go_package.startswith("go-fips"): + subpkg_uses_fips = True + break + + # Check subpackage test sections + subpkg_test_section = subpkg.get("test", {}) + subpkg_test_pipelines = subpkg_test_section.get("pipeline", []) + for step in subpkg_test_pipelines: + if step.get("uses") == "test/go-fips-check": + subpkg_has_test = True + break + + if subpkg_uses_fips and not subpkg_has_test: + issues.append(f"subpackage '{subpkg_name}' uses go-fips but lacks test/go-fips-check") + + return len(issues) == 0, issues def main(argv: Sequence[str] | None = None) -> int: @@ -68,13 +98,11 @@ def main(argv: Sequence[str] | None = None) -> int: if not melange_cfg: continue - if uses_go_fips(melange_cfg): - if not has_go_fips_test(melange_cfg): - print( - f"{filename}: Package uses go-fips but does not have " - "a corresponding go-fips test (add '- uses: test/go-fips-check' to test pipeline)" - ) - retval = 1 + is_compliant, issues = check_go_fips_compliance(melange_cfg) + if not is_compliant: + for issue in issues: + print(f"{filename}: {issue}") + retval = 1 return retval From 0d2639dcef6ac3de7b36b7e073e2a892ab55a3d5 Mon Sep 17 00:00:00 2001 From: Amber Arcadia Date: Tue, 12 Aug 2025 10:18:23 -0400 Subject: [PATCH 3/7] Linting --- pre_commit_hooks/check_for_go_fips_test.py | 42 ++++++++++++---------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/pre_commit_hooks/check_for_go_fips_test.py b/pre_commit_hooks/check_for_go_fips_test.py index 10c2af7..f8bc3b2 100644 --- a/pre_commit_hooks/check_for_go_fips_test.py +++ b/pre_commit_hooks/check_for_go_fips_test.py @@ -16,18 +16,20 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[st Returns (is_compliant, list_of_missing_tests). """ issues = [] - + # Check if main package uses go-fips main_uses_fips = False main_has_test = False - + # Check environment packages for any go-fips variant - env_packages = melange_cfg.get("environment", {}).get("contents", {}).get("packages", []) + env_packages = ( + melange_cfg.get("environment", {}).get("contents", {}).get("packages", []) + ) for pkg in env_packages: if pkg.startswith("go-fips"): main_uses_fips = True break - + # Check main pipeline steps for go/build with go-package: go-fips* pipelines = melange_cfg.get("pipeline", []) for step in pipelines: @@ -36,7 +38,7 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[st if go_package.startswith("go-fips"): main_uses_fips = True break - + # Check main test section test_section = melange_cfg.get("test", {}) test_pipelines = test_section.get("pipeline", []) @@ -44,16 +46,16 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[st if step.get("uses") == "test/go-fips-check": main_has_test = True break - + if main_uses_fips and not main_has_test: issues.append("main package uses go-fips but lacks test/go-fips-check") - + # Check each subpackage for i, subpkg in enumerate(melange_cfg.get("subpackages", [])): subpkg_uses_fips = False subpkg_has_test = False subpkg_name = subpkg.get("name", f"subpackage-{i}") - + # Check subpackage pipelines for go-fips usage subpkg_pipelines = subpkg.get("pipeline", []) for step in subpkg_pipelines: @@ -62,7 +64,7 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[st if go_package.startswith("go-fips"): subpkg_uses_fips = True break - + # Check subpackage test sections subpkg_test_section = subpkg.get("test", {}) subpkg_test_pipelines = subpkg_test_section.get("pipeline", []) @@ -70,22 +72,24 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[st if step.get("uses") == "test/go-fips-check": subpkg_has_test = True break - + if subpkg_uses_fips and not subpkg_has_test: - issues.append(f"subpackage '{subpkg_name}' uses go-fips but lacks test/go-fips-check") - + issues.append( + f"subpackage '{subpkg_name}' uses go-fips but lacks test/go-fips-check", + ) + return len(issues) == 0, issues def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser( - description="Check that packages using go-fips have corresponding go-fips tests" + description="Check that packages using go-fips have corresponding go-fips tests", ) 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: @@ -94,18 +98,18 @@ def main(argv: Sequence[str] | None = None) -> int: print(f"Error loading {filename}: {e}") retval = 1 continue - + if not melange_cfg: continue - + is_compliant, issues = check_go_fips_compliance(melange_cfg) if not is_compliant: for issue in issues: print(f"{filename}: {issue}") retval = 1 - + return retval if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) From 2d5109339ce77507f608d8527e9d54b122259745 Mon Sep 17 00:00:00 2001 From: Amber Arcadia Date: Tue, 12 Aug 2025 12:52:09 -0400 Subject: [PATCH 4/7] Avoid adding go-fips test to empty packages --- pre_commit_hooks/check_for_go_fips_test.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pre_commit_hooks/check_for_go_fips_test.py b/pre_commit_hooks/check_for_go_fips_test.py index f8bc3b2..e6dda74 100644 --- a/pre_commit_hooks/check_for_go_fips_test.py +++ b/pre_commit_hooks/check_for_go_fips_test.py @@ -42,12 +42,15 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[st # Check main test section test_section = melange_cfg.get("test", {}) test_pipelines = test_section.get("pipeline", []) + main_has_emptypackage_test = False for step in test_pipelines: if step.get("uses") == "test/go-fips-check": main_has_test = True - break + elif step.get("uses") == "test/emptypackage": + main_has_emptypackage_test = True - if main_uses_fips and not main_has_test: + # If main package has emptypackage test, it doesn't need go-fips test + if main_uses_fips and not main_has_test and not main_has_emptypackage_test: issues.append("main package uses go-fips but lacks test/go-fips-check") # Check each subpackage @@ -68,12 +71,15 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[st # Check subpackage test sections subpkg_test_section = subpkg.get("test", {}) subpkg_test_pipelines = subpkg_test_section.get("pipeline", []) + subpkg_has_emptypackage_test = False for step in subpkg_test_pipelines: if step.get("uses") == "test/go-fips-check": subpkg_has_test = True - break + elif step.get("uses") == "test/emptypackage": + subpkg_has_emptypackage_test = True - if subpkg_uses_fips and not subpkg_has_test: + # If subpackage has emptypackage test, it doesn't need go-fips test + if subpkg_uses_fips and not subpkg_has_test and not subpkg_has_emptypackage_test: issues.append( f"subpackage '{subpkg_name}' uses go-fips but lacks test/go-fips-check", ) From d1cdeefd506fd795f62217de60ce07ea2005a92a Mon Sep 17 00:00:00 2001 From: Amber Arcadia Date: Tue, 12 Aug 2025 13:04:28 -0400 Subject: [PATCH 5/7] Linting --- pre_commit_hooks/check_for_go_fips_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pre_commit_hooks/check_for_go_fips_test.py b/pre_commit_hooks/check_for_go_fips_test.py index e6dda74..0fad3df 100644 --- a/pre_commit_hooks/check_for_go_fips_test.py +++ b/pre_commit_hooks/check_for_go_fips_test.py @@ -79,7 +79,11 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[st subpkg_has_emptypackage_test = True # If subpackage has emptypackage test, it doesn't need go-fips test - if subpkg_uses_fips and not subpkg_has_test and not subpkg_has_emptypackage_test: + if ( + subpkg_uses_fips + and not subpkg_has_test + and not subpkg_has_emptypackage_test + ): issues.append( f"subpackage '{subpkg_name}' uses go-fips but lacks test/go-fips-check", ) From 9c8b08849be2250359ecb151a425f9a772fd2b0f Mon Sep 17 00:00:00 2001 From: Amber Arcadia Date: Fri, 2 Oct 2026 13:53:19 -0400 Subject: [PATCH 6/7] check-for-go-fips-test: attribute builds per scope, widen coverage rules Running the previous version over the 10,047 package files on stereo main flagged 25 files, of which 2 were real gaps. The rest were main packages whose environment lists go-fips only for the benefit of subpackages, range subpackages covered by a go-fips-check in the main test, one package using test/tw/emptypackage, and five files that crashed the hook on a null `pipeline:`. The check now asks, per package and subpackage, whether that scope compiles Go with a FIPS toolchain and whether a go-fips-check covers it: - go/build and go/install count when their go-package input starts with go-fips. go/build/v2 and hand-written `go build` / `go install` lines use the environment's go, so they count only when the environment provides a go-fips toolchain. A main environment entry alone no longer implicates a main package that builds nothing. - Coverage is the scope's own test, test/emptypackage or test/tw/emptypackage, or for subpackages a main test that runs go-fips-check and installs the subpackage (range names match on the prefix before ${{range.key}}). - split/bin in a subpackage moves the main binaries there, so that subpackage inherits the obligation and the main package is released. - Nested `pipeline:` lists are searched; null pipelines no longer crash. - The main and subpackage code paths share one set of helpers. With this version stereo main reports exactly the 2 real gaps: crossplane-provider-aws-cloudsearch-fips and two harvester-fips subpackages. A stereo PR adds the missing tests. Known limits, left for follow-ups: FIPS_GO_PREFIXES is only go-fips (not go-msft or go-geomys), templated go-package values such as ${{vars.go-version}} are invisible without a melange compile, and Go built through make or a script is not detected. Fixtures under test-data/ show the three flagged shapes and the covered ones. --- pre_commit_hooks/check_for_go_fips_test.py | 221 +++++++++++++-------- test-data/go-fips-test-bad.yaml | 49 +++++ test-data/go-fips-test-good.yaml | 73 +++++++ 3 files changed, 265 insertions(+), 78 deletions(-) create mode 100644 test-data/go-fips-test-bad.yaml create mode 100644 test-data/go-fips-test-good.yaml diff --git a/pre_commit_hooks/check_for_go_fips_test.py b/pre_commit_hooks/check_for_go_fips_test.py index 0fad3df..207e2d1 100644 --- a/pre_commit_hooks/check_for_go_fips_test.py +++ b/pre_commit_hooks/check_for_go_fips_test.py @@ -1,7 +1,9 @@ from __future__ import annotations import argparse +import re import sys +from collections.abc import Iterator from collections.abc import Sequence from typing import Any @@ -9,91 +11,156 @@ yaml = ruamel.yaml.YAML(typ="safe") +# Go toolchain packages that imply a FIPS build, matched as prefixes against +# environment packages and `go-package:` inputs (go-fips, go-fips-md5-1.25, ...). +FIPS_GO_PREFIXES = ("go-fips",) -def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> tuple[bool, list[str]]: +# Pipelines that install the toolchain named by their `go-package` input. +GO_PIPELINES_WITH_TOOLCHAIN_INPUT = frozenset({"go/build", "go/install"}) +# Pipelines that build with whatever `go` the build environment provides. +GO_PIPELINES_FROM_ENVIRONMENT = frozenset({"go/build/v2"}) +# `runs:` blocks that call the compiler directly also use the environment's go. +GO_BUILD_COMMAND = re.compile(r"\bgo\s+(build|install)\b") + +FIPS_TEST = "test/go-fips-check" +EMPTY_PACKAGE_TESTS = frozenset({"test/emptypackage", "test/tw/emptypackage"}) +# Moves the main package's binaries into a subpackage, which then owns the test. +SPLIT_BIN = "split/bin" + + +def iter_steps(steps: Any) -> Iterator[dict[str, Any]]: + """Yield every dict step in *steps*, descending into nested ``pipeline:`` lists.""" + for step in steps or []: + if not isinstance(step, dict): + continue + yield step + yield from iter_steps(step.get("pipeline")) + + +def uses_any(steps: Any, pipelines: frozenset[str]) -> bool: + return any(step.get("uses") in pipelines for step in iter_steps(steps)) + + +def is_fips_toolchain(package: Any) -> bool: + return isinstance(package, str) and package.startswith(FIPS_GO_PREFIXES) + + +def environment_packages(scope: dict[str, Any]) -> list[Any]: + """Packages listed under ``environment.contents.packages`` of *scope*.""" + env = scope.get("environment") or {} + contents = (env.get("contents") or {}) if isinstance(env, dict) else {} + packages = contents.get("packages") if isinstance(contents, dict) else None + return list(packages or []) + + +def test_steps(scope: dict[str, Any]) -> list[Any]: + test = scope.get("test") or {} + return list(test.get("pipeline") or []) if isinstance(test, dict) else [] + + +def builds_with_fips(steps: Any, env_has_fips_go: bool) -> bool: + """Whether *steps* compile Go with a FIPS toolchain. + + `go/build` and `go/install` say so explicitly through `go-package`. + `go/build/v2` and hand-written `go build` lines use the environment's go, + so they count only when the environment provides a FIPS toolchain. """ - Check if all go-fips usages have corresponding tests. - Returns (is_compliant, list_of_missing_tests). + for step in iter_steps(steps): + uses = step.get("uses") + if uses in GO_PIPELINES_WITH_TOOLCHAIN_INPUT: + inputs = step.get("with") or {} + if is_fips_toolchain(inputs.get("go-package")): + return True + elif uses in GO_PIPELINES_FROM_ENVIRONMENT: + if env_has_fips_go: + return True + elif env_has_fips_go and isinstance(step.get("runs"), str): + if GO_BUILD_COMMAND.search(step["runs"]): + return True + return False + + +def installed_in_main_test(melange_cfg: dict[str, Any], subpkg_name: str) -> bool: + """Whether the main test environment installs *subpkg_name*. + + Names are compared before template expansion. For range subpackages the + part before ``${{range.key}}`` is matched as a prefix, so a main test that + installs ``${{package.name}}-controller`` covers + ``${{package.name}}-${{range.key}}``. """ - issues = [] + test = melange_cfg.get("test") or {} + packages = environment_packages(test) if isinstance(test, dict) else [] + if subpkg_name in packages: + return True + prefix, sep, _ = subpkg_name.partition("${{range.key}}") + return bool(sep and prefix) and any( + isinstance(p, str) and p.startswith(prefix) for p in packages + ) + - # Check if main package uses go-fips - main_uses_fips = False - main_has_test = False +def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> list[str]: + """Return a message for every package or subpackage that builds Go with a + FIPS toolchain and has no test/go-fips-check covering it. - # Check environment packages for any go-fips variant - env_packages = ( - melange_cfg.get("environment", {}).get("contents", {}).get("packages", []) + Coverage is the scope's own test pipeline, an empty-package test (nothing + to check), or for subpackages the main test pipeline when it both runs + test/go-fips-check and installs the subpackage. + """ + issues = [] + env_has_fips_go = any( + is_fips_toolchain(p) for p in environment_packages(melange_cfg) ) - for pkg in env_packages: - if pkg.startswith("go-fips"): - main_uses_fips = True - break - - # Check main pipeline steps for go/build with go-package: go-fips* - pipelines = melange_cfg.get("pipeline", []) - for step in pipelines: - if step.get("uses") == "go/build": - go_package = step.get("with", {}).get("go-package", "") - if go_package.startswith("go-fips"): - main_uses_fips = True - break - - # Check main test section - test_section = melange_cfg.get("test", {}) - test_pipelines = test_section.get("pipeline", []) - main_has_emptypackage_test = False - for step in test_pipelines: - if step.get("uses") == "test/go-fips-check": - main_has_test = True - elif step.get("uses") == "test/emptypackage": - main_has_emptypackage_test = True - - # If main package has emptypackage test, it doesn't need go-fips test - if main_uses_fips and not main_has_test and not main_has_emptypackage_test: - issues.append("main package uses go-fips but lacks test/go-fips-check") - - # Check each subpackage - for i, subpkg in enumerate(melange_cfg.get("subpackages", [])): - subpkg_uses_fips = False - subpkg_has_test = False - subpkg_name = subpkg.get("name", f"subpackage-{i}") - - # Check subpackage pipelines for go-fips usage - subpkg_pipelines = subpkg.get("pipeline", []) - for step in subpkg_pipelines: - if step.get("uses") == "go/build": - go_package = step.get("with", {}).get("go-package", "") - if go_package.startswith("go-fips"): - subpkg_uses_fips = True - break - - # Check subpackage test sections - subpkg_test_section = subpkg.get("test", {}) - subpkg_test_pipelines = subpkg_test_section.get("pipeline", []) - subpkg_has_emptypackage_test = False - for step in subpkg_test_pipelines: - if step.get("uses") == "test/go-fips-check": - subpkg_has_test = True - elif step.get("uses") == "test/emptypackage": - subpkg_has_emptypackage_test = True - - # If subpackage has emptypackage test, it doesn't need go-fips test - if ( - subpkg_uses_fips - and not subpkg_has_test - and not subpkg_has_emptypackage_test + subpackages = [ + s for s in melange_cfg.get("subpackages") or [] if isinstance(s, dict) + ] + + main_test = test_steps(melange_cfg) + main_has_fips_test = uses_any(main_test, frozenset({FIPS_TEST})) + main_builds_fips = builds_with_fips(melange_cfg.get("pipeline"), env_has_fips_go) + # split/bin in a subpackage moves the main package's binaries there: the + # subpackage inherits the obligation and the main package is left with none. + split_off = [ + s for s in subpackages if uses_any(s.get("pipeline"), frozenset({SPLIT_BIN})) + ] + + if ( + main_builds_fips + and not split_off + and not main_has_fips_test + and not uses_any(main_test, EMPTY_PACKAGE_TESTS) + ): + issues.append( + f"main package builds with a FIPS Go toolchain but lacks {FIPS_TEST}", + ) + + for i, subpkg in enumerate(subpackages): + name = str(subpkg.get("name", f"#{i}")) + builds_fips = builds_with_fips(subpkg.get("pipeline"), env_has_fips_go) or ( + main_builds_fips and any(s is subpkg for s in split_off) + ) + if not builds_fips: + continue + sub_test = test_steps(subpkg) + if uses_any(sub_test, frozenset({FIPS_TEST})) or uses_any( + sub_test, + EMPTY_PACKAGE_TESTS, ): - issues.append( - f"subpackage '{subpkg_name}' uses go-fips but lacks test/go-fips-check", - ) + continue + if main_has_fips_test and installed_in_main_test(melange_cfg, name): + continue + issues.append( + f"subpackage '{name}' builds with a FIPS Go toolchain but lacks " + f"{FIPS_TEST} (in its own test, or in the main test with the " + "subpackage installed)", + ) - return len(issues) == 0, issues + return issues def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser( - description="Check that packages using go-fips have corresponding go-fips tests", + description="Check that packages built with a FIPS Go toolchain " + f"have a {FIPS_TEST} test", ) parser.add_argument("filenames", nargs="*", help="Filenames to check") args = parser.parse_args(argv) @@ -109,13 +176,11 @@ def main(argv: Sequence[str] | None = None) -> int: retval = 1 continue - if not melange_cfg: + if not isinstance(melange_cfg, dict): continue - is_compliant, issues = check_go_fips_compliance(melange_cfg) - if not is_compliant: - for issue in issues: - print(f"{filename}: {issue}") + for issue in check_go_fips_compliance(melange_cfg): + print(f"{filename}: {issue}") retval = 1 return retval diff --git a/test-data/go-fips-test-bad.yaml b/test-data/go-fips-test-bad.yaml new file mode 100644 index 0000000..5c93040 --- /dev/null +++ b/test-data/go-fips-test-bad.yaml @@ -0,0 +1,49 @@ +package: + name: fips-bad + version: "1.0.0" + epoch: 0 + description: "Builds Go binaries with go-fips but never runs test/go-fips-check" + copyright: + - license: Apache-2.0 + +environment: + contents: + packages: + - busybox + - go-fips-1.25 + +pipeline: + - uses: git-checkout + with: + repository: https://example.invalid/fips-bad + tag: v${{package.version}} + expected-commit: 0000000000000000000000000000000000000000 + - uses: go/build + with: + packages: . + output: fips-bad + go-package: go-fips-md5-1.25 + +test: + pipeline: + - uses: test/tw/ldd-check # BAD: main binary is built with go-fips, no test/go-fips-check + +subpackages: + - name: ${{package.name}}-helper + pipeline: + - uses: go/build/v2 # BAD: builds with the environment's go-fips, test lacks go-fips-check + with: + packages: ./cmd/helper + output: helper + test: + pipeline: + - uses: test/tw/help-check + with: + bins: helper + - name: ${{package.name}}-manual + pipeline: + - runs: | # BAD: hand-written go build with the environment's go-fips + go build -o "${{targets.contextdir}}/usr/bin/manual" ./cmd/manual + test: + pipeline: + - uses: test/tw/ldd-check diff --git a/test-data/go-fips-test-good.yaml b/test-data/go-fips-test-good.yaml new file mode 100644 index 0000000..9027869 --- /dev/null +++ b/test-data/go-fips-test-good.yaml @@ -0,0 +1,73 @@ +package: + name: fips-good + version: "1.0.0" + epoch: 0 + description: "Every go-fips build is covered by a test/go-fips-check" + copyright: + - license: Apache-2.0 + +environment: + contents: + packages: + - busybox + - go-fips-1.25 + +pipeline: + - uses: git-checkout + with: + repository: https://example.invalid/fips-good + tag: v${{package.version}} + expected-commit: 0000000000000000000000000000000000000000 + - uses: go/build + with: + packages: . + output: fips-good + go-package: go-fips-md5-1.25 + +data: + - name: components + items: + controller: controller + agent: agent + +test: + environment: + contents: + packages: + - ${{package.name}}-controller + - ${{package.name}}-agent + pipeline: + - uses: test/go-fips-check # GOOD: covers the main binary and the installed subpackages + +subpackages: + - name: ${{package.name}}-${{range.key}} # GOOD: installed in the main test, which runs go-fips-check + range: components + pipeline: + - uses: go/build/v2 + with: + packages: ./cmd/${{range.key}} + output: ${{range.key}} + test: + pipeline: + - uses: test/tw/ldd-check + - name: ${{package.name}}-tools + pipeline: + - runs: | + go build -o "${{targets.contextdir}}/usr/bin/tools" ./cmd/tools + test: + pipeline: + - uses: test/go-fips-check # GOOD: covered by its own test + - name: ${{package.name}}-compat + pipeline: + - runs: | + mkdir -p "${{targets.contextdir}}/usr/local/bin" + ln -sf /usr/bin/fips-good "${{targets.contextdir}}/usr/local/bin/fips-good" + test: + pipeline: + - uses: test/tw/symlink-check # GOOD: no Go build in this subpackage + - name: ${{package.name}}-meta + description: "An empty pipeline is allowed and must not trip the hook" + pipeline: + test: + pipeline: + - uses: test/tw/emptypackage From 458f58e84f72c5a76951de0160000b5b19ae6819 Mon Sep 17 00:00:00 2001 From: Amber Arcadia Date: Fri, 23 Oct 2026 16:10:14 -0400 Subject: [PATCH 7/7] check-for-go-fips-test: share config walkers, cover every FIPS toolchain, resolve vars Review follow-ups on #18 now that #20 is in: - Move the melange config walkers (iter_steps, iter_pipelines, subpackages, build_steps, test_steps, environment_packages, uses_any) and the load-and-report loop into pre_commit_hooks/melange.py, and rebuild both check-pipeline-name-only-steps and check-for-go-fips-test on it. Each hook is now its check function plus argparse. - Treat every toolchain family test/go-fips-check can verify as FIPS: go-fips and go-fips-md5 (at 1.27 these are meta packages pointing at go-msft-1.27), go-msft, go-geomys and go-openssl. Limiting the rule to go-fips would have exempted the toolchain new FIPS builds are moving to. On stereo main this adds 15 findings, all gitlab-rails FIPS streams that rebuild the devfile gem's Go helper with go-msft and never run go-fips-check. - Resolve ${{vars.NAME}} from the top-level vars: mapping before matching toolchain names, so templated go-package values are visible without a melange compile. var-transforms are left alone. - Repair the rebase onto #20: both hooks keep their full manifest entries and example-config lines, which a keep-both merge had truncated. - Document the go-fips fixtures in test-data/README.md. --- .pre-commit-hooks.yaml | 6 + example.pre-commit-config.yaml | 1 + pre_commit_hooks/check_for_go_fips_test.py | 103 +++++--------- .../check_pipeline_name_only_steps.py | 55 +------- pre_commit_hooks/melange.py | 131 ++++++++++++++++++ test-data/README.md | 15 ++ 6 files changed, 189 insertions(+), 122 deletions(-) create mode 100644 pre_commit_hooks/melange.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 5cc3547..94f97b8 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -33,6 +33,12 @@ 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 - id: check-for-go-fips-test name: check for go-fips test description: Check that packages using go-fips have corresponding go-fips tests diff --git a/example.pre-commit-config.yaml b/example.pre-commit-config.yaml index f07c8c7..c18c34a 100644 --- a/example.pre-commit-config.yaml +++ b/example.pre-commit-config.yaml @@ -19,6 +19,7 @@ repos: [^/]+\.ya?ml # matches .yaml or .yml files at the top level only )$ - id: check-pipeline-name-only-steps + files: '^[^.][^/]*\.yaml$' # matches non-hidden .yaml files at the top level only - id: check-for-go-fips-test files: '^[^.][^/]*\.yaml$' # matches non-hidden .yaml files at the top level only - repo: https://github.com/chainguard-dev/yam diff --git a/pre_commit_hooks/check_for_go_fips_test.py b/pre_commit_hooks/check_for_go_fips_test.py index 207e2d1..2e28d68 100644 --- a/pre_commit_hooks/check_for_go_fips_test.py +++ b/pre_commit_hooks/check_for_go_fips_test.py @@ -3,17 +3,27 @@ import argparse import re import sys -from collections.abc import Iterator from collections.abc import Sequence from typing import Any -import ruamel.yaml - -yaml = ruamel.yaml.YAML(typ="safe") +from pre_commit_hooks.melange import build_steps +from pre_commit_hooks.melange import environment_packages +from pre_commit_hooks.melange import iter_steps +from pre_commit_hooks.melange import resolve_vars +from pre_commit_hooks.melange import run_check +from pre_commit_hooks.melange import subpackages +from pre_commit_hooks.melange import test_steps +from pre_commit_hooks.melange import uses_any # Go toolchain packages that imply a FIPS build, matched as prefixes against -# environment packages and `go-package:` inputs (go-fips, go-fips-md5-1.25, ...). -FIPS_GO_PREFIXES = ("go-fips",) +# environment packages and `go-package:` inputs. One entry per toolchain family +# test/go-fips-check knows how to verify: +# go-fips, go-fips-md5 Microsoft-patched Go with the OpenSSL backend; at 1.27 +# these are meta packages pointing at go-msft-1.27 +# go-msft the same toolchain under its own name (systemcrypto) +# go-geomys Geomys Go with the native FIPS 140 module +# go-openssl unpatched golang-fips/go +FIPS_GO_PREFIXES = ("go-fips", "go-msft", "go-geomys", "go-openssl") # Pipelines that install the toolchain named by their `go-package` input. GO_PIPELINES_WITH_TOOLCHAIN_INPUT = frozenset({"go/build", "go/install"}) @@ -22,42 +32,16 @@ # `runs:` blocks that call the compiler directly also use the environment's go. GO_BUILD_COMMAND = re.compile(r"\bgo\s+(build|install)\b") -FIPS_TEST = "test/go-fips-check" +FIPS_TEST = frozenset({"test/go-fips-check"}) EMPTY_PACKAGE_TESTS = frozenset({"test/emptypackage", "test/tw/emptypackage"}) # Moves the main package's binaries into a subpackage, which then owns the test. -SPLIT_BIN = "split/bin" - - -def iter_steps(steps: Any) -> Iterator[dict[str, Any]]: - """Yield every dict step in *steps*, descending into nested ``pipeline:`` lists.""" - for step in steps or []: - if not isinstance(step, dict): - continue - yield step - yield from iter_steps(step.get("pipeline")) - - -def uses_any(steps: Any, pipelines: frozenset[str]) -> bool: - return any(step.get("uses") in pipelines for step in iter_steps(steps)) +SPLIT_BIN = frozenset({"split/bin"}) def is_fips_toolchain(package: Any) -> bool: return isinstance(package, str) and package.startswith(FIPS_GO_PREFIXES) -def environment_packages(scope: dict[str, Any]) -> list[Any]: - """Packages listed under ``environment.contents.packages`` of *scope*.""" - env = scope.get("environment") or {} - contents = (env.get("contents") or {}) if isinstance(env, dict) else {} - packages = contents.get("packages") if isinstance(contents, dict) else None - return list(packages or []) - - -def test_steps(scope: dict[str, Any]) -> list[Any]: - test = scope.get("test") or {} - return list(test.get("pipeline") or []) if isinstance(test, dict) else [] - - def builds_with_fips(steps: Any, env_has_fips_go: bool) -> bool: """Whether *steps* compile Go with a FIPS toolchain. @@ -107,21 +91,20 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> list[str]: test/go-fips-check and installs the subpackage. """ issues = [] + # Toolchain names are often templated (`go-package: ${{vars.go-package}}`); + # resolve what can be resolved without a melange compile. + melange_cfg = resolve_vars(melange_cfg) env_has_fips_go = any( is_fips_toolchain(p) for p in environment_packages(melange_cfg) ) - subpackages = [ - s for s in melange_cfg.get("subpackages") or [] if isinstance(s, dict) - ] + subs = subpackages(melange_cfg) main_test = test_steps(melange_cfg) - main_has_fips_test = uses_any(main_test, frozenset({FIPS_TEST})) - main_builds_fips = builds_with_fips(melange_cfg.get("pipeline"), env_has_fips_go) + main_has_fips_test = uses_any(main_test, FIPS_TEST) + main_builds_fips = builds_with_fips(build_steps(melange_cfg), env_has_fips_go) # split/bin in a subpackage moves the main package's binaries there: the # subpackage inherits the obligation and the main package is left with none. - split_off = [ - s for s in subpackages if uses_any(s.get("pipeline"), frozenset({SPLIT_BIN})) - ] + split_off = [s for s in subs if uses_any(build_steps(s), SPLIT_BIN)] if ( main_builds_fips @@ -130,27 +113,24 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> list[str]: and not uses_any(main_test, EMPTY_PACKAGE_TESTS) ): issues.append( - f"main package builds with a FIPS Go toolchain but lacks {FIPS_TEST}", + "main package builds with a FIPS Go toolchain but lacks test/go-fips-check", ) - for i, subpkg in enumerate(subpackages): + for i, subpkg in enumerate(subs): name = str(subpkg.get("name", f"#{i}")) - builds_fips = builds_with_fips(subpkg.get("pipeline"), env_has_fips_go) or ( + builds_fips = builds_with_fips(build_steps(subpkg), env_has_fips_go) or ( main_builds_fips and any(s is subpkg for s in split_off) ) if not builds_fips: continue sub_test = test_steps(subpkg) - if uses_any(sub_test, frozenset({FIPS_TEST})) or uses_any( - sub_test, - EMPTY_PACKAGE_TESTS, - ): + if uses_any(sub_test, FIPS_TEST) or uses_any(sub_test, EMPTY_PACKAGE_TESTS): continue if main_has_fips_test and installed_in_main_test(melange_cfg, name): continue issues.append( f"subpackage '{name}' builds with a FIPS Go toolchain but lacks " - f"{FIPS_TEST} (in its own test, or in the main test with the " + "test/go-fips-check (in its own test, or in the main test with the " "subpackage installed)", ) @@ -160,30 +140,11 @@ def check_go_fips_compliance(melange_cfg: dict[str, Any]) -> list[str]: def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser( description="Check that packages built with a FIPS Go toolchain " - f"have a {FIPS_TEST} test", + "have a test/go-fips-check test", ) 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 isinstance(melange_cfg, dict): - continue - - for issue in check_go_fips_compliance(melange_cfg): - print(f"{filename}: {issue}") - retval = 1 - - return retval + return run_check(args.filenames, check_go_fips_compliance) if __name__ == "__main__": diff --git a/pre_commit_hooks/check_pipeline_name_only_steps.py b/pre_commit_hooks/check_pipeline_name_only_steps.py index ba65696..c1fd997 100644 --- a/pre_commit_hooks/check_pipeline_name_only_steps.py +++ b/pre_commit_hooks/check_pipeline_name_only_steps.py @@ -2,40 +2,12 @@ import argparse import sys -from collections.abc import Iterator from collections.abc import Sequence from typing import Any -import ruamel.yaml - -yaml = ruamel.yaml.YAML(typ="safe") - - -def iter_pipelines(melange_cfg: dict[str, Any]) -> Iterator[tuple[str, list[Any]]]: - """Yield (label, steps) for every pipeline in a melange config. - - Covers the main build and test pipelines and each subpackage's build and - test pipelines. A ``pipeline:`` key with no value (YAML null) yields an - empty list, matching how melange treats it. - """ - scopes: list[tuple[str, dict[str, Any]]] = [("main", melange_cfg)] - for i, subpkg in enumerate(melange_cfg.get("subpackages") or []): - if isinstance(subpkg, dict): - scopes.append((f"subpackage '{subpkg.get('name', i)}'", subpkg)) - for label, scope in scopes: - yield f"{label} pipeline", scope.get("pipeline") or [] - test = scope.get("test") or {} - if isinstance(test, dict): - yield f"{label} test pipeline", test.get("pipeline") or [] - - -def iter_steps(steps: list[Any]) -> Iterator[dict[str, Any]]: - """Yield every dict step in *steps*, descending into nested ``pipeline:`` lists.""" - for step in steps: - if not isinstance(step, dict): - continue - yield step - yield from iter_steps(step.get("pipeline") or []) +from pre_commit_hooks.melange import iter_pipelines +from pre_commit_hooks.melange import iter_steps +from pre_commit_hooks.melange import run_check def check_pipeline_steps(melange_cfg: dict[str, Any]) -> list[str]: @@ -62,26 +34,7 @@ def main(argv: Sequence[str] | None = None) -> int: ) 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 isinstance(melange_cfg, dict): - continue - - for issue in check_pipeline_steps(melange_cfg): - print(f"{filename}: {issue}") - retval = 1 - - return retval + return run_check(args.filenames, check_pipeline_steps) if __name__ == "__main__": diff --git a/pre_commit_hooks/melange.py b/pre_commit_hooks/melange.py new file mode 100644 index 0000000..69ba08a --- /dev/null +++ b/pre_commit_hooks/melange.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import re +from collections.abc import Callable +from collections.abc import Iterator +from collections.abc import Sequence +from typing import Any + +import ruamel.yaml + +# Helpers for walking melange package configurations. +# +# Shared by the hooks that inspect package YAML before melange compiles it. +# Every accessor tolerates a key that is present with no value (YAML null), +# which melange treats the same as an absent key. + +yaml = ruamel.yaml.YAML(typ="safe") + + +def iter_steps(steps: Any) -> Iterator[dict[str, Any]]: + """Yield every dict step in *steps*, descending into nested ``pipeline:`` lists.""" + for step in steps or []: + if not isinstance(step, dict): + continue + yield step + yield from iter_steps(step.get("pipeline")) + + +def uses_any(steps: Any, pipelines: frozenset[str]) -> bool: + """Whether any step in *steps*, at any depth, ``uses:`` one of *pipelines*.""" + return any(step.get("uses") in pipelines for step in iter_steps(steps)) + + +def subpackages(melange_cfg: dict[str, Any]) -> list[dict[str, Any]]: + """The ``subpackages:`` entries that are mappings, in file order.""" + return [s for s in melange_cfg.get("subpackages") or [] if isinstance(s, dict)] + + +def build_steps(scope: dict[str, Any]) -> list[Any]: + """Top-level steps of *scope*'s build ``pipeline:``.""" + return list(scope.get("pipeline") or []) + + +def test_steps(scope: dict[str, Any]) -> list[Any]: + """Top-level steps of *scope*'s ``test.pipeline:``.""" + test = scope.get("test") or {} + return list(test.get("pipeline") or []) if isinstance(test, dict) else [] + + +def iter_pipelines(melange_cfg: dict[str, Any]) -> Iterator[tuple[str, list[Any]]]: + """Yield (label, steps) for the main and every subpackage build and test pipeline.""" + scopes: list[tuple[str, dict[str, Any]]] = [("main", melange_cfg)] + scopes += [ + (f"subpackage '{s.get('name', i)}'", s) + for i, s in enumerate(subpackages(melange_cfg)) + ] + for label, scope in scopes: + yield f"{label} pipeline", build_steps(scope) + yield f"{label} test pipeline", test_steps(scope) + + +_VAR_REF = re.compile(r"\$\{\{vars\.([A-Za-z0-9_.-]+)\}\}") + + +def resolve_vars(melange_cfg: dict[str, Any]) -> dict[str, Any]: + """Return a copy of *melange_cfg* with ``${{vars.NAME}}`` substituted. + + Only plain entries of the top-level ``vars:`` mapping are resolved; values + produced by ``var-transforms`` and other template namespaces are left as + written. That is enough to see through ``go-package: ${{vars.go-package}}`` + without running ``melange compile``. + """ + vars_ = melange_cfg.get("vars") or {} + if not isinstance(vars_, dict): + return melange_cfg + + def substitute(value: Any) -> Any: + if isinstance(value, str): + return _VAR_REF.sub( + lambda m: str(vars_.get(m.group(1), m.group(0))), + value, + ) + if isinstance(value, list): + return [substitute(v) for v in value] + if isinstance(value, dict): + return {k: substitute(v) for k, v in value.items()} + return value + + resolved: dict[str, Any] = substitute(melange_cfg) + return resolved + + +def environment_packages(scope: dict[str, Any]) -> list[Any]: + """Packages under ``environment.contents.packages`` of *scope*. + + Works for the top-level build environment and for a ``test:`` section, + which has the same shape. + """ + env = scope.get("environment") or {} + contents = (env.get("contents") or {}) if isinstance(env, dict) else {} + packages = contents.get("packages") if isinstance(contents, dict) else None + return list(packages or []) + + +def run_check( + filenames: Sequence[str], + check: Callable[[dict[str, Any]], list[str]], +) -> int: + """Load each file as a melange config, print *check*'s findings per file. + + Returns 1 if any file failed to load or produced a finding, else 0. + Files that do not parse to a mapping are skipped. + """ + retval = 0 + for filename in 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 isinstance(melange_cfg, dict): + continue + + for issue in check(melange_cfg): + print(f"{filename}: {issue}") + retval = 1 + + return retval diff --git a/test-data/README.md b/test-data/README.md index 8926836..853e323 100644 --- a/test-data/README.md +++ b/test-data/README.md @@ -28,3 +28,18 @@ same command works from another repo by replacing `.` with that path. - Expected: passes. Every step has `uses:` or `runs:` alongside any `name:`, and the last subpackage has an empty `pipeline:` key, which melange allows and the hook must not trip over. + +### go-fips-test-bad.yaml + +- Hook: `check-for-go-fips-test` +- Expected: fails with three findings: the main package, built by `go/build` + with a `go-fips` toolchain; a subpackage built by `go/build/v2` with the + environment's `go-fips`; and a subpackage with a hand-written `go build`. + None of the three has a `test/go-fips-check`. + +### go-fips-test-good.yaml + +- Hook: `check-for-go-fips-test` +- Expected: passes. Range subpackages are covered by the main test, which + installs them and runs `test/go-fips-check`; another subpackage carries its + own check; one builds no Go at all; and one has an empty `pipeline:`.