From fccb3f719eeb39850f92f1bfe38d5b4d25a734e0 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 3 Jul 2026 09:03:02 -0700 Subject: [PATCH] ci(release): drop the ghd distribution integration and fix the openapi smoke tests The release workflows validated and staged assets against a ghd.toml manifest that was intentionally removed; the ghd distribution channel is not part of this repo. Keep the goreleaser artifact/checksum validation and the dist/release-assets staging the rest of the release depends on, drop only the ghd.toml checks and the ghd download snippet in the release notes. Also fix both container smoke tests: 'openapi | grep -Fq' breaks the pipe when grep exits at the first match, failing docker run under pipefail; capture the spec to a file first. Co-Authored-By: Claude Fable 5 --- ...ease_assets.py => stage_release_assets.py} | 85 +------------------ ...assets.py => test_stage_release_assets.py} | 75 ++-------------- .github/workflows/release-dry-run.yml | 20 ++--- .github/workflows/release.yml | 10 ++- 4 files changed, 19 insertions(+), 171 deletions(-) rename .github/scripts/{stage_ghd_release_assets.py => stage_release_assets.py} (73%) rename .github/scripts/{test_stage_ghd_release_assets.py => test_stage_release_assets.py} (75%) diff --git a/.github/scripts/stage_ghd_release_assets.py b/.github/scripts/stage_release_assets.py similarity index 73% rename from .github/scripts/stage_ghd_release_assets.py rename to .github/scripts/stage_release_assets.py index fdd059f..bc7b5e3 100644 --- a/.github/scripts/stage_ghd_release_assets.py +++ b/.github/scripts/stage_release_assets.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Stage and validate ghd release assets generated by GoReleaser.""" +"""Stage and validate release assets generated by GoReleaser.""" from __future__ import annotations @@ -9,7 +9,6 @@ import os import shutil import sys -import tomllib from pathlib import Path from typing import Any @@ -31,7 +30,6 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--tag", required=True, help="Release tag, for example v1.2.3") parser.add_argument("--artifacts", default=Path("dist/artifacts.json"), type=Path) - parser.add_argument("--config", default=Path("ghd.toml"), type=Path) parser.add_argument("--output", default=Path("dist/release-assets"), type=Path) parser.add_argument("--binary-name", default="mock-oidc") return parser.parse_args(argv) @@ -43,7 +41,6 @@ def main(argv: list[str] | None = None) -> int: stage_release_assets( tag=args.tag, artifacts_path=args.artifacts, - config_path=args.config, output_dir=args.output, binary_name=args.binary_name, ) @@ -57,17 +54,10 @@ def stage_release_assets( *, tag: str, artifacts_path: Path, - config_path: Path, output_dir: Path, binary_name: str, ) -> None: version = release_version(tag) - repository = os.environ.get("GITHUB_REPOSITORY") - if not repository: - raise StageError("GITHUB_REPOSITORY must be set") - - config = load_toml(config_path) - validate_ghd_config(config, binary_name=binary_name, repository=repository) artifacts = load_artifacts(artifacts_path) stage_artifacts(artifacts, output_dir=output_dir, binary_name=binary_name) @@ -89,20 +79,6 @@ def release_version(tag: str) -> str: return tag[1:] -def load_toml(path: Path) -> dict[str, Any]: - try: - with path.open("rb") as config_file: - data = tomllib.load(config_file) - except FileNotFoundError as exc: - raise StageError(f"missing ghd config {path}") from exc - except tomllib.TOMLDecodeError as exc: - raise StageError(f"invalid TOML in {path}: {exc}") from exc - - if not isinstance(data, dict): - raise StageError(f"ghd config {path} must be a TOML table") - return data - - def load_artifacts(path: Path) -> list[dict[str, Any]]: try: data = json.loads(path.read_text(encoding="utf-8")) @@ -122,65 +98,6 @@ def load_artifacts(path: Path) -> list[dict[str, Any]]: return artifacts -def validate_ghd_config(config: dict[str, Any], *, binary_name: str, repository: str) -> None: - provenance = config.get("provenance") - if not isinstance(provenance, dict): - raise StageError("ghd.toml must define a [provenance] table") - - expected_signer = f"{repository}/.github/workflows/attest.yml" - actual_signer = provenance.get("signer_workflow") - if actual_signer != expected_signer: - raise StageError( - f"ghd.toml signer_workflow must be {expected_signer}, got {actual_signer}" - ) - - package = find_package(config, binary_name) - if package.get("tag_pattern") != "v${version}": - raise StageError( - f"ghd.toml package {binary_name!r} must use tag_pattern = \"v${{version}}\"" - ) - - binaries = package.get("binaries") - if not isinstance(binaries, list) or not any( - isinstance(binary, dict) and binary.get("path") == binary_name for binary in binaries - ): - raise StageError( - f"ghd.toml package {binary_name!r} must include binary path {binary_name!r}" - ) - - assets = package.get("assets") - if not isinstance(assets, list): - raise StageError(f"ghd.toml package {binary_name!r} must include assets") - - actual_patterns = { - asset.get("pattern") - for asset in assets - if isinstance(asset, dict) and isinstance(asset.get("pattern"), str) - } - expected_patterns = { - f"{binary_name}_${{version}}_{goos}_{goarch}" for goos, goarch in EXPECTED_PLATFORMS - } - missing_patterns = sorted(expected_patterns - actual_patterns) - if missing_patterns: - raise StageError(f"ghd.toml is missing expected asset pattern(s): {', '.join(missing_patterns)}") - - -def find_package(config: dict[str, Any], binary_name: str) -> dict[str, Any]: - packages = config.get("packages") - if not isinstance(packages, list): - raise StageError("ghd.toml must define at least one [[packages]] entry") - - matches = [ - package - for package in packages - if isinstance(package, dict) and package.get("name") == binary_name - ] - if len(matches) != 1: - raise StageError(f"ghd.toml must define exactly one package named {binary_name!r}") - - return matches[0] - - def stage_artifacts( artifacts: list[dict[str, Any]], *, diff --git a/.github/scripts/test_stage_ghd_release_assets.py b/.github/scripts/test_stage_release_assets.py similarity index 75% rename from .github/scripts/test_stage_ghd_release_assets.py rename to .github/scripts/test_stage_release_assets.py index 1b15ff9..c1eafdf 100644 --- a/.github/scripts/test_stage_ghd_release_assets.py +++ b/.github/scripts/test_stage_release_assets.py @@ -11,12 +11,12 @@ from pathlib import Path -SCRIPT_PATH = Path(__file__).with_name("stage_ghd_release_assets.py") -SPEC = importlib.util.spec_from_file_location("stage_ghd_release_assets", SCRIPT_PATH) +SCRIPT_PATH = Path(__file__).with_name("stage_release_assets.py") +SPEC = importlib.util.spec_from_file_location("stage_release_assets", SCRIPT_PATH) assert SPEC is not None assert SPEC.loader is not None -stage_ghd_release_assets = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(stage_ghd_release_assets) +stage_release_assets = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(stage_release_assets) PLATFORMS = ( @@ -37,20 +37,7 @@ def working_directory(path: Path): os.chdir(original) -@contextlib.contextmanager -def github_repository(value: str): - original = os.environ.get("GITHUB_REPOSITORY") - os.environ["GITHUB_REPOSITORY"] = value - try: - yield - finally: - if original is None: - os.environ.pop("GITHUB_REPOSITORY", None) - else: - os.environ["GITHUB_REPOSITORY"] = original - - -class StageGhdReleaseAssetsTest(unittest.TestCase): +class StageReleaseAssetsTest(unittest.TestCase): def test_stages_expected_assets(self) -> None: with fixture() as root: result, stdout, stderr = run_script(root) @@ -92,13 +79,6 @@ def test_fails_on_checksum_mismatch(self) -> None: self.assertEqual(result, 1) self.assertIn("checksum mismatch for mock-oidc_1.2.3_linux_amd64", stderr) - def test_fails_on_wrong_signer_workflow(self) -> None: - with fixture(signer="other/repo/.github/workflows/release.yml") as root: - result, _, stderr = run_script(root) - - self.assertEqual(result, 1) - self.assertIn("signer_workflow", stderr) - def test_fails_on_missing_os_arch_asset(self) -> None: with fixture(omit_artifact=("linux", "arm64", "Binary")) as root: result, _, stderr = run_script(root) @@ -117,16 +97,15 @@ def test_fails_on_unexpected_asset_count(self) -> None: def run_script(root: Path) -> tuple[int, str, str]: stdout = io.StringIO() stderr = io.StringIO() - with working_directory(root), github_repository("meigma/mock-oidc"): + with working_directory(root): with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): - result = stage_ghd_release_assets.main(["--tag", "v1.2.3"]) + result = stage_release_assets.main(["--tag", "v1.2.3"]) return result, stdout.getvalue(), stderr.getvalue() @contextlib.contextmanager def fixture( *, - signer: str = "meigma/mock-oidc/.github/workflows/attest.yml", missing_checksum: str | None = None, checksum_override: tuple[str, str] | None = None, omit_artifact: tuple[str, str, str] | None = None, @@ -135,7 +114,6 @@ def fixture( with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "dist").mkdir() - write_ghd_toml(root / "ghd.toml", signer) artifacts: list[dict[str, str]] = [] checksum_entries: dict[str, str] = {} @@ -189,45 +167,6 @@ def fixture( yield root -def write_ghd_toml(path: Path, signer: str) -> None: - path.write_text( - f'''version = 1 - -[provenance] -signer_workflow = "{signer}" - -[[packages]] -name = "mock-oidc" -description = "Meigma Go repository template starter CLI." -tag_pattern = "v${{version}}" - -[[packages.assets]] -os = "darwin" -arch = "amd64" -pattern = "mock-oidc_${{version}}_darwin_amd64" - -[[packages.assets]] -os = "darwin" -arch = "arm64" -pattern = "mock-oidc_${{version}}_darwin_arm64" - -[[packages.assets]] -os = "linux" -arch = "amd64" -pattern = "mock-oidc_${{version}}_linux_amd64" - -[[packages.assets]] -os = "linux" -arch = "arm64" -pattern = "mock-oidc_${{version}}_linux_arm64" - -[[packages.binaries]] -path = "mock-oidc" -''', - encoding="utf-8", - ) - - def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index 1cd19ac..d5ad5ef 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -55,24 +55,13 @@ jobs: GITHUB_TOKEN: ${{ github.token }} GORELEASER_CURRENT_TAG: v0.0.0-dryrun.${{ github.run_id }}.${{ github.run_attempt }} - - name: Validate ghd-compatible dry-run artifacts + - name: Validate dry-run release artifacts run: | set -euo pipefail version="0.0.0-dryrun.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}" binary_name="mock-oidc" asset_prefix="${binary_name}_" - expected_signer="${GITHUB_REPOSITORY}/.github/workflows/attest.yml" - actual_signer="$(awk -F'"' '/signer_workflow/ { print $2; exit }' ghd.toml)" - - if [ "$actual_signer" != "$expected_signer" ]; then - echo "ghd.toml signer_workflow must be $expected_signer, got $actual_signer" >&2 - exit 1 - fi - - grep -Fq "name = \"$binary_name\"" ghd.toml - grep -Fq "tag_pattern = \"v\${version}\"" ghd.toml - grep -Fq "path = \"$binary_name\"" ghd.toml jq -e --arg asset_prefix "$asset_prefix" ' [ @@ -94,11 +83,9 @@ jobs: for arch in amd64 arm64; do expected="${binary_name}_${version}_${os}_${arch}" expected_sbom="${expected}.sbom.json" - expected_pattern="pattern = \"${binary_name}_\${version}_${os}_${arch}\"" jq -e --arg name "$expected" '.[] | select(.type == "Binary" and .name == $name)' dist/artifacts.json >/dev/null jq -e --arg name "$expected_sbom" '.[] | select(.type == "SBOM" and .name == $name)' dist/artifacts.json >/dev/null - grep -Fq "$expected_pattern" ghd.toml awk -v name="$expected" '$2 == name && length($1) == 64 && $1 ~ /^[[:xdigit:]]+$/ { found = 1 } END { exit(found ? 0 : 1) }' dist/checksums.txt done done @@ -251,4 +238,7 @@ jobs: docker tag mock-oidc:dry-run-amd64 mock-oidc:dry-run docker run --rm mock-oidc:dry-run --version - docker run --rm mock-oidc:dry-run openapi | grep -Fq "openapi: 3.0.3" + # Capture before grepping: grep -q exits at the first match and the + # resulting broken pipe fails `docker run` under pipefail. + docker run --rm mock-oidc:dry-run openapi > openapi-smoke.yaml + grep -Fq "openapi: 3.0.3" openapi-smoke.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cba447..1df2a6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,10 +109,10 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} - - name: Stage and validate ghd release assets + - name: Stage and validate release assets env: RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }} - run: python3 .github/scripts/stage_ghd_release_assets.py --tag "$RELEASE_TAG" + run: python3 .github/scripts/stage_release_assets.py --tag "$RELEASE_TAG" - name: Smoke test release binary env: @@ -363,7 +363,10 @@ jobs: set -euo pipefail docker run --rm "$IMAGE_REF" --version - docker run --rm "$IMAGE_REF" openapi | grep -Fq "openapi: 3.0.3" + # Capture before grepping: grep -q exits at the first match and the + # resulting broken pipe fails `docker run` under pipefail. + docker run --rm "$IMAGE_REF" openapi > openapi-smoke.yaml + grep -Fq "openapi: 3.0.3" openapi-smoke.yaml - name: Sign image (keyless, Sigstore/Fulcio via OIDC) env: @@ -427,7 +430,6 @@ jobs: echo "gh release view $RELEASE_TAG --repo $GITHUB_REPOSITORY --json isDraft,assets" echo "asset=\"mock-oidc_${RELEASE_VERSION}_\$(go env GOOS)_\$(go env GOARCH)\"" echo "gh attestation verify \"dist/release-assets/\${asset}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/attest.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners" - echo "ghd download \"$GITHUB_REPOSITORY/mock-oidc@${RELEASE_VERSION}\" --output \"\$(mktemp -d)\"" echo '```' echo echo "Container verification commands:"