From 4a75fbca3f41763547f360ccdc2ad4d0a7c01423 Mon Sep 17 00:00:00 2001 From: Steve Gontzes Date: Wed, 21 Oct 2026 22:43:35 +0000 Subject: [PATCH 1/4] Adopt pinned trust and tool actions in release jobs Bootstrap guarded jobs before checkouts, retain the failure-notification exemption and conditional preflight tooling, and preserve module gates through action-local helpers. Retire forwarding wrappers and keep ordering regressions enabled for release-only changes. Co-authored-by: c1-squire-dev[bot] --- .gitattributes | 3 - .github/workflows/ci.yaml | 2 + .github/workflows/release.yaml | 142 +++++++++++++++------------------ docs/release-workflow.md | 32 ++++++++ scripts/gon-source.sh | 5 -- scripts/install-tool.ps1 | 7 -- scripts/install-tool.sh | 5 -- scripts/test-ci-workflow.py | 2 +- scripts/test-tool-integrity.py | 130 +++++++++++++++++++++++------- 9 files changed, 202 insertions(+), 126 deletions(-) delete mode 100755 scripts/gon-source.sh delete mode 100644 scripts/install-tool.ps1 delete mode 100755 scripts/install-tool.sh diff --git a/.gitattributes b/.gitattributes index 54249ed..bc6adac 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,3 @@ scripts/test-deny-os.sh text eol=lf scripts/test-ca-trust-guard.sh text eol=lf # The canonical lock hash and action scripts must survive Windows checkout unchanged. .github/actions/locked-tools/** text eol=lf -scripts/install-tool.sh text eol=lf -scripts/install-tool.ps1 text eol=lf -scripts/gon-source.sh text eol=lf diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a937870..6219d88 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,7 @@ on: paths: - .github/workflows/ci.yaml - .github/workflows/verify.yaml + - .github/workflows/release.yaml - .github/actions/** - .gitattributes - actions/** @@ -23,6 +24,7 @@ on: paths: - .github/workflows/ci.yaml - .github/workflows/verify.yaml + - .github/workflows/release.yaml - .github/actions/** - .gitattributes - actions/** diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7cd61d2..194de8f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -120,6 +120,9 @@ jobs: runs-on: ubuntu-latest steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + - name: Validate tag format env: TAG: ${{ inputs.tag }} @@ -176,6 +179,9 @@ jobs: go_main_package: ${{ steps.release-options.outputs.go_main_package }} brew_tap: ${{ steps.release-options.outputs.brew_tap }} steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + - name: Checkout pinned connector workflows uses: actions/checkout@v5 with: @@ -257,6 +263,15 @@ jobs: outputs: github_prerelease: ${{ steps.github-release.outputs.prerelease }} steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + + - name: Install locked release tools + if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true + uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + with: + tools: go + - name: Checkout caller repo if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 @@ -284,17 +299,9 @@ jobs: EXPECTED_COMMIT: ${{ github.sha }} run: bash _workflows/scripts/verify-release-checkout.sh _caller - - name: Install locked Go - if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true - run: bash _workflows/scripts/install-tool.sh go "$RUNNER_TEMP/locked-go" - - - name: Verify locked Go version - if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true - run: bash _workflows/scripts/verify-locked-go.sh version - - name: Check caller Go requirement if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true - run: bash _workflows/scripts/verify-locked-go.sh module _caller/go.mod _workflows/go.mod + run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - name: Capture GitHub Release pre-release setting id: github-release @@ -383,6 +390,14 @@ jobs: binaries_checksums: ${{ steps.output-checksums.outputs.checksums }} released_at: ${{ steps.release-meta.outputs.released_at }} steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + + - name: Install locked release tools + uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + with: + tools: go,gon,syft,goreleaser + - name: Checkout caller repo uses: actions/checkout@v5 with: @@ -420,35 +435,8 @@ jobs: --suffix "${REPO_OWNER}-${REPO_NAME}" \ --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" - - name: Read locked gon source commit - id: gon-source - run: bash _workflows/scripts/gon-source.sh ref - - - name: Checkout locked gon source - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 - with: - repository: ConductorOne/gon - ref: ${{ steps.gon-source.outputs.ref }} - path: _gon - persist-credentials: false - - - name: Install locked Go - run: bash _workflows/scripts/install-tool.sh go "$RUNNER_TEMP/locked-go" - - - name: Verify locked Go version - run: bash _workflows/scripts/verify-locked-go.sh version - - name: Check caller Go requirement - run: bash _workflows/scripts/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - - - name: Build native gon from locked source - run: bash _workflows/scripts/gon-source.sh build _gon "$RUNNER_TEMP/locked-gon" - - - name: Install locked syft - run: bash _workflows/scripts/install-tool.sh syft "$RUNNER_TEMP/locked-syft" - - - name: Install locked GoReleaser - run: bash _workflows/scripts/install-tool.sh goreleaser "$RUNNER_TEMP/locked-goreleaser" + run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - name: Install cosign uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 @@ -716,6 +704,14 @@ jobs: outputs: windows_manifest: ${{ steps.generate-windows-manifest.outputs.windows_manifest }} steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + + - name: Install locked release tools + uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + with: + tools: go,syft,goreleaser-pro + - name: Enable Git long paths shell: pwsh run: git config --system core.longpaths true @@ -757,25 +753,9 @@ jobs: --suffix "${REPO_OWNER}-${REPO_NAME}" \ --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" - - name: Install locked Go - shell: pwsh - run: ./_workflows/scripts/install-tool.ps1 go "$env:RUNNER_TEMP/locked-go" - - - name: Verify locked Go version - shell: bash - run: bash _workflows/scripts/verify-locked-go.sh version - - name: Check caller Go requirement shell: bash - run: bash _workflows/scripts/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - - - name: Install locked syft - shell: pwsh - run: ./_workflows/scripts/install-tool.ps1 syft "$env:RUNNER_TEMP/locked-syft" - - - name: Install locked GoReleaser Pro - shell: pwsh - run: ./_workflows/scripts/install-tool.ps1 goreleaser-pro "$env:RUNNER_TEMP/locked-goreleaser" + run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - name: Generate or locate WXS file id: wxs @@ -1002,6 +982,14 @@ jobs: env: GOTOOLCHAIN: local steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + + - name: Install locked release tools + uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + with: + tools: go,goreleaser + - name: Checkout caller repo uses: actions/checkout@v5 with: @@ -1045,17 +1033,8 @@ jobs: --suffix "${REPO_NAME}" \ --output-name ecr_push_role_name >> "$GITHUB_OUTPUT" - - name: Install locked Go - run: bash _workflows/scripts/install-tool.sh go "$RUNNER_TEMP/locked-go" - - - name: Verify locked Go version - run: bash _workflows/scripts/verify-locked-go.sh version - - name: Check caller Go requirement - run: bash _workflows/scripts/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - - - name: Install locked GoReleaser - run: bash _workflows/scripts/install-tool.sh goreleaser "$RUNNER_TEMP/locked-goreleaser" + run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - name: Install cosign if: inputs.docker == true @@ -1295,6 +1274,14 @@ jobs: env: GOTOOLCHAIN: local steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + + - name: Install locked release tools + uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + with: + tools: go + - name: Checkout connector workflows uses: actions/checkout@v5 with: @@ -1316,14 +1303,8 @@ jobs: --suffix "${REPO_OWNER}-${REPO_NAME}" \ --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" - - name: Install locked Go - run: bash _workflows/scripts/install-tool.sh go "$RUNNER_TEMP/locked-go" - - - name: Verify locked Go version - run: bash _workflows/scripts/verify-locked-go.sh version - - name: Check workflows Go requirement - run: bash _workflows/scripts/verify-locked-go.sh module _workflows/go.mod + run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _workflows/go.mod - name: Merge binaries, Windows, and images manifests working-directory: _workflows @@ -1550,6 +1531,14 @@ jobs: env: GOTOOLCHAIN: local steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + + - name: Install locked release tools + uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + with: + tools: go + - name: Checkout connector workflows uses: actions/checkout@v5 with: @@ -1575,14 +1564,8 @@ jobs: EXPECTED_COMMIT: ${{ github.sha }} run: bash _workflows/scripts/verify-release-checkout.sh _connector - - name: Install locked Go - run: bash _workflows/scripts/install-tool.sh go "$RUNNER_TEMP/locked-go" - - - name: Verify locked Go version - run: bash _workflows/scripts/verify-locked-go.sh version - - name: Check workflows Go requirement - run: bash _workflows/scripts/verify-locked-go.sh module _workflows/go.mod + run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _workflows/go.mod - name: Read connector documentation id: read-docs @@ -1711,6 +1694,9 @@ jobs: contents: read runs-on: ubuntu-latest steps: + - name: Enforce runner CA trust + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + - name: Checkout shared workflows uses: actions/checkout@v5 with: diff --git a/docs/release-workflow.md b/docs/release-workflow.md index eeb70be..cd2a2a8 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -233,6 +233,38 @@ The same event commit remains in the provenance's resolved Git dependency. Every GoReleaser invocation receives `GORELEASER_CURRENT_TAG` from the workflow input. This pins archive names, release metadata, and image digest files to the requested tag when multiple tags identify the same commit. +### Release Job Bootstrap + +Nine release jobs start with the SHA-pinned `ca-trust-guard` action before +**any checkout or credential access**, using its default +`enforce`/`true`/`true`/`fail` inputs. Six tool-using jobs then run the pinned +`locked-tools` action as step 2, followed by source/workflow checkouts, release +commit verification, module requirements, and release credentials. Both actions +use commit `99f9b2fc150f0351f2ef6f9bf9f631a17749ef15`; neither requires a caller +checkout to bootstrap. The guard's measurements and residual-client limits are +documented in [CA Trust Guard](../.github/actions/ca-trust-guard/README.md). + +`notify-release-failure` is deliberately exempt: guard errors must not prevent +failure reporting. The job remains failure-only and does not install build +tools. `validate-inputs`, `determine-workflows-ref`, and `verify-release` need +the guard but no locked-tools installation. + +| Tool-using job | `locked-tools` input | +| --- | --- | +| release-preflight | `go` | +| goreleaser-binaries | `go,gon,syft,goreleaser` | +| goreleaser-windows | `go,syft,goreleaser-pro` | +| goreleaser-docker | `go,goreleaser` | +| publish-release-manifest | `go` | +| record-registry-api | `go` | + +Preflight keeps its existing `go_source_hygiene || go_vulnerability_scan` +condition on the tool action, checkouts, and module gate, avoiding new downloads +when both checks are disabled. Its guard still runs first unconditionally. +Job-level `GOTOOLCHAIN=local` and the binaries job's readonly-module/proxy/sumdb +settings are unchanged. Cosign and bundled-buildx/QEMU setup remain after the +trust/tool bootstrap and before their signing or AWS credentials. + ### Locked Release Tools `.github/actions/locked-tools/tool-locks.txt` identifies tool versions, artifact diff --git a/scripts/gon-source.sh b/scripts/gon-source.sh deleted file mode 100755 index c48a511..0000000 --- a/scripts/gon-source.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -# Transitional entry point; implementation and lock live with the action. -set -euo pipefail -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -exec bash "$script_dir/../.github/actions/locked-tools/gon-source.sh" "$@" diff --git a/scripts/install-tool.ps1 b/scripts/install-tool.ps1 deleted file mode 100644 index 90fa78d..0000000 --- a/scripts/install-tool.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -# Transitional entry point; implementation and lock live with the action. -param( - [Parameter(Mandatory = $true)][string]$Tool, - [Parameter(Mandatory = $true)][string]$DestDir -) -$ErrorActionPreference = 'Stop' -& (Join-Path $PSScriptRoot '../.github/actions/locked-tools/install-tool.ps1') -Tool $Tool -DestDir $DestDir diff --git a/scripts/install-tool.sh b/scripts/install-tool.sh deleted file mode 100755 index 6fc1b17..0000000 --- a/scripts/install-tool.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -# Transitional entry point; implementation and lock live with the action. -set -euo pipefail -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -exec bash "$script_dir/../.github/actions/locked-tools/install-tool.sh" "$@" diff --git a/scripts/test-ci-workflow.py b/scripts/test-ci-workflow.py index ae6048b..4a6e1e3 100755 --- a/scripts/test-ci-workflow.py +++ b/scripts/test-ci-workflow.py @@ -21,7 +21,7 @@ def test_repository_events_jobs_and_read_only_permissions(self): self.assertEqual(workflow["on"]["push"]["branches"], ["main"]) expected_paths = {"cmd/**", ".github/actions/**", "actions/**", "scripts/**", "tools/**", "go.*", "renovate.json", "templates/**", ".github/workflows/ci.yaml", - ".github/workflows/verify.yaml", "Makefile", ".gitattributes"} + ".github/workflows/verify.yaml", ".github/workflows/release.yaml", "Makefile", ".gitattributes"} pull_paths = set(workflow["on"]["pull_request"]["paths"]) self.assertTrue(expected_paths <= pull_paths) self.assertEqual(pull_paths, set(workflow["on"]["push"]["paths"])) diff --git a/scripts/test-tool-integrity.py b/scripts/test-tool-integrity.py index dbd0fdc..df3202b 100755 --- a/scripts/test-tool-integrity.py +++ b/scripts/test-tool-integrity.py @@ -17,6 +17,9 @@ ROOT = Path(__file__).resolve().parent.parent ACTION = ROOT / ".github/actions/locked-tools" +ACTION_PIN = "99f9b2fc150f0351f2ef6f9bf9f631a17749ef15" +GUARD_ACTION = f"ConductorOne/github-workflows/.github/actions/ca-trust-guard@{ACTION_PIN}" +TOOLS_ACTION = f"ConductorOne/github-workflows/.github/actions/locked-tools@{ACTION_PIN}" class IntegrityTests(unittest.TestCase): @@ -32,8 +35,6 @@ def setUp(self) -> None: shutil.copyfile(ACTION / name, self.action_dir / name) shutil.copyfile(ACTION / "locked-tools.sh", self.action_dir / "locked-tools.sh") shutil.copyfile(ACTION / "install-tool.ps1", self.action_dir / "install-tool.ps1") - for name in ("install-tool.sh", "gon-source.sh"): - shutil.copyfile(ROOT / "scripts" / name, self.scripts / name) for name in ("verify-release-checkout.sh", "setup-release-buildx.sh"): shutil.copyfile(ROOT / "scripts" / name, self.scripts / name) self.lock = self.action_dir / "tool-locks.txt" @@ -412,24 +413,22 @@ def test_minisign_zip_verified_before_extraction(self) -> None: self.assertFalse(self.path_file.exists()) self.assertFalse((self.root / "bad-minisign").exists()) - def test_custom_lock_and_legacy_forwarders(self) -> None: + def test_custom_lock_overrides_action_defaults(self) -> None: self.fixture_download("node", "linux/amd64") expected_member = self.archive_member("node", "linux/amd64") custom = self.root / "custom locks.txt" shutil.copyfile(self.lock, custom) self.lock.write_text("# The canonical fixture lock is deliberately unusable.\n") self.env["TOOL_LOCK_FILE"] = str(custom) - result = subprocess.run(["bash", str(self.scripts / "install-tool.sh"), "node", str(self.root / "legacy")], - env=self.env, text=True, capture_output=True) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertTrue((self.root / "legacy" / expected_member).is_file()) + self.run_script("install-tool.sh", "node", self.root / "custom-install") + self.assertTrue((self.root / "custom-install" / expected_member).is_file()) self.run_script("gon-source.sh", "ref") self.assertIn("ref=", self.output_file.read_text()) - self.output_file.unlink() - result = subprocess.run(["bash", str(self.scripts / "gon-source.sh"), "ref"], - env=self.env, text=True, capture_output=True) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("ref=", self.output_file.read_text()) + module = self.root / "go.mod" + module.write_bytes(b"module fixture\r\ngo 1.27.3\r\n") + self.go_contract(lock=custom) + self.mock_go_gate() + self.run_script("verify-locked-go.sh", "module", module) def phase_outputs(self) -> dict[str, str]: return dict(line.split("=", 1) for line in self.output_file.read_text().splitlines()) @@ -635,23 +634,100 @@ def test_workflow_security_wiring(self) -> None: workflow = json.loads(subprocess.check_output(["yq", ".", str(ROOT / ".github/workflows/release.yaml")], text=True)) jobs = workflow["jobs"] self.assertEqual(jobs["determine-workflows-ref"]["outputs"]["ref"], "${{ job.workflow_sha }}") + for legacy in ("_workflows/scripts/install-tool", "_workflows/scripts/gon-source", "_workflows/scripts/verify-locked-go"): + self.assertNotIn(legacy, json.dumps(workflow)) + expected_tools = { + "validate-inputs": "", + "determine-workflows-ref": "", + "release-preflight": "go", + "goreleaser-binaries": "go,gon,syft,goreleaser", + "goreleaser-windows": "go,syft,goreleaser-pro", + "goreleaser-docker": "go,goreleaser", + "publish-release-manifest": "go", + "record-registry-api": "go", + "verify-release": "", + "notify-release-failure": "", + } + self.assertEqual(set(jobs), set(expected_tools)) + guard_count = tool_count = 0 for job_name, job in jobs.items(): - steps = job["steps"] - installs = [i for i, step in enumerate(steps) if any(token in step.get("run", "") for token in ("install-tool.", "gon-source.sh build", "setup-release-buildx.sh")) or "cosign-installer@" in step.get("uses", "") or "setup-qemu-action@" in step.get("uses", "")] - credentials = [i for i, step in enumerate(steps) if "secrets." in str(step) or "configure-aws-credentials@" in step.get("uses", "")] - if installs and credentials: - self.assertLess(max(installs), min(credentials), job_name) - for step in steps: - if "install-tool." in step.get("run", ""): - self.assertNotIn("\n", step["run"], job_name) + with self.subTest(job=job_name): + steps = job["steps"] + guards = [index for index, step in enumerate(steps) if "ca-trust-guard@" in step.get("uses", "")] + tools = [index for index, step in enumerate(steps) if "/locked-tools@" in step.get("uses", "")] + if job_name == "notify-release-failure": + self.assertEqual(guards, [], "guard failures must not suppress failure reporting") + self.assertEqual(tools, []) + self.assertEqual(job["if"], "failure()") + continue + self.assertEqual(guards, [0]) + self.assertEqual(steps[0]["uses"], GUARD_ACTION) + self.assertEqual(steps[0].get("with", {}), {}, "use enforce/true/true/fail defaults") + self.assertNotIn("if", steps[0]) + self.assertNotIn("continue-on-error", steps[0]) + guard_count += 1 + prefix_length = 1 + if expected_tools[job_name]: + self.assertEqual(tools, [1]) + self.assertEqual(steps[1]["uses"], TOOLS_ACTION) + self.assertEqual(steps[1]["with"], {"tools": expected_tools[job_name]}) + self.assertNotIn("continue-on-error", steps[1]) self.assertEqual(job["env"]["GOTOOLCHAIN"], "local") - if step.get("with", {}).get("path") in ("_caller", "_connector"): - self.assertEqual(step["with"]["ref"], "${{ github.sha }}") - if "verify-release-checkout.sh" in step.get("run", ""): - self.assertEqual(step["env"]["EXPECTED_COMMIT"], "${{ github.sha }}") - if "cosign-installer@" in step.get("uses", ""): - self.assertEqual(step["with"]["cosign-release"], "v2.5.2") - self.assertRegex(step["uses"], r"@[0-9a-f]{40}$") + if job_name == "release-preflight": + self.assertEqual(steps[1]["if"], "inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true") + else: + self.assertNotIn("if", steps[1]) + prefix_length = 2 + tool_count += 1 + else: + self.assertEqual(tools, []) + checkouts = [index for index, step in enumerate(steps) if step.get("uses", "").startswith("actions/checkout@")] + verifications = [index for index, step in enumerate(steps) if "verify-release-checkout.sh" in step.get("run", "")] + credentials = [index for index, step in enumerate(steps) + if "secrets." in str(step) or "github.token" in str(step) or "core.getIDToken" in str(step) + or "configure-aws-credentials@" in step.get("uses", "")] + self.assertTrue(all(index >= prefix_length for index in checkouts)) + if checkouts and verifications: + self.assertLess(max(checkouts), min(verifications)) + if credentials: + self.assertLess(prefix_length - 1, min(credentials)) + if verifications: + self.assertLess(max(verifications), min(credentials)) + installs = [index for index, step in enumerate(steps) + if "cosign-installer@" in step.get("uses", "") + or "setup-qemu-action@" in step.get("uses", "") + or "setup-release-buildx.sh" in step.get("run", "")] + if installs and credentials: + self.assertLess(max(installs), min(credentials)) + module_gates = [index for index, step in enumerate(steps) + if "/locked-tools/verify-locked-go.sh module " in step.get("run", "")] + if expected_tools[job_name]: + self.assertEqual(len(module_gates), 1) + self.assertLess(max(checkouts), module_gates[0]) + if verifications: + self.assertLess(max(verifications), module_gates[0]) + if credentials: + self.assertLess(module_gates[0], min(credentials)) + modules = "_workflows/go.mod" if job_name in ("publish-release-manifest", "record-registry-api") else "_caller/go.mod _workflows/go.mod" + self.assertEqual(steps[module_gates[0]]["run"], + "bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module " + modules) + if job_name == "release-preflight": + self.assertEqual(steps[module_gates[0]]["if"], steps[1]["if"]) + for step in steps: + run = step.get("run", "") + self.assertNotEqual(step.get("with", {}).get("repository"), "ConductorOne/gon") + if step.get("with", {}).get("path") in ("_caller", "_connector"): + self.assertEqual(step["with"]["ref"], "${{ github.sha }}") + if step.get("with", {}).get("path") == "_workflows": + self.assertIn(step["with"]["ref"], ("${{ job.workflow_sha }}", "${{ needs.determine-workflows-ref.outputs.ref }}")) + if "verify-release-checkout.sh" in run: + self.assertEqual(step["env"]["EXPECTED_COMMIT"], "${{ github.sha }}") + if "cosign-installer@" in step.get("uses", ""): + self.assertEqual(step["with"]["cosign-release"], "v2.5.2") + self.assertRegex(step["uses"], r"@[0-9a-f]{40}$") + self.assertEqual((guard_count, tool_count), (9, 6)) + for wrapper in ("install-tool.sh", "install-tool.ps1", "gon-source.sh", "verify-locked-go.sh"): + self.assertFalse((ROOT / "scripts" / wrapper).exists()) if __name__ == "__main__": From 4e7b1716c32c57756c9ce66f6e484bf5b2814fe9 Mon Sep 17 00:00:00 2001 From: Steve Gontzes Date: Thu, 22 Oct 2026 00:39:27 +0000 Subject: [PATCH 2/4] Pin release bootstrap actions to final reviewed commits Use distinct immutable guard and locked-tools refs from main, and align the expected-pin assertions and bootstrap documentation without changing job behavior. Co-authored-by: c1-squire-dev[bot] --- .github/workflows/release.yaml | 30 +++++++++++++++--------------- docs/release-workflow.md | 7 ++++--- scripts/test-tool-integrity.py | 5 ++--- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 194de8f..5578caf 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -121,7 +121,7 @@ jobs: steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Validate tag format env: @@ -180,7 +180,7 @@ jobs: brew_tap: ${{ steps.release-options.outputs.brew_tap }} steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Checkout pinned connector workflows uses: actions/checkout@v5 @@ -264,11 +264,11 @@ jobs: github_prerelease: ${{ steps.github-release.outputs.prerelease }} steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Install locked release tools if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true - uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 with: tools: go @@ -391,10 +391,10 @@ jobs: released_at: ${{ steps.release-meta.outputs.released_at }} steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 with: tools: go,gon,syft,goreleaser @@ -705,10 +705,10 @@ jobs: windows_manifest: ${{ steps.generate-windows-manifest.outputs.windows_manifest }} steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 with: tools: go,syft,goreleaser-pro @@ -983,10 +983,10 @@ jobs: GOTOOLCHAIN: local steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 with: tools: go,goreleaser @@ -1275,10 +1275,10 @@ jobs: GOTOOLCHAIN: local steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 with: tools: go @@ -1532,10 +1532,10 @@ jobs: GOTOOLCHAIN: local steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 with: tools: go @@ -1695,7 +1695,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@99f9b2fc150f0351f2ef6f9bf9f631a17749ef15 + uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - name: Checkout shared workflows uses: actions/checkout@v5 diff --git a/docs/release-workflow.md b/docs/release-workflow.md index cd2a2a8..0dd083d 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -239,9 +239,10 @@ Nine release jobs start with the SHA-pinned `ca-trust-guard` action before **any checkout or credential access**, using its default `enforce`/`true`/`true`/`fail` inputs. Six tool-using jobs then run the pinned `locked-tools` action as step 2, followed by source/workflow checkouts, release -commit verification, module requirements, and release credentials. Both actions -use commit `99f9b2fc150f0351f2ef6f9bf9f631a17749ef15`; neither requires a caller -checkout to bootstrap. The guard's measurements and residual-client limits are +commit verification, module requirements, and release credentials. The guard +uses commit `4d06ff31da09d2012770879c2c43c54dec24fe76`; locked-tools uses +`562a1ea6fa6abd0460950dfc95c7f845aa5f1448`. Neither requires a caller checkout +to bootstrap. The guard's measurements and residual-client limits are documented in [CA Trust Guard](../.github/actions/ca-trust-guard/README.md). `notify-release-failure` is deliberately exempt: guard errors must not prevent diff --git a/scripts/test-tool-integrity.py b/scripts/test-tool-integrity.py index df3202b..df11c1c 100755 --- a/scripts/test-tool-integrity.py +++ b/scripts/test-tool-integrity.py @@ -17,9 +17,8 @@ ROOT = Path(__file__).resolve().parent.parent ACTION = ROOT / ".github/actions/locked-tools" -ACTION_PIN = "99f9b2fc150f0351f2ef6f9bf9f631a17749ef15" -GUARD_ACTION = f"ConductorOne/github-workflows/.github/actions/ca-trust-guard@{ACTION_PIN}" -TOOLS_ACTION = f"ConductorOne/github-workflows/.github/actions/locked-tools@{ACTION_PIN}" +GUARD_ACTION = "ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76" +TOOLS_ACTION = "ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448" class IntegrityTests(unittest.TestCase): From a096b3eb6e20bf15202cdf00b3dfa23a7a522dec Mon Sep 17 00:00:00 2001 From: Steve Gontzes Date: Thu, 22 Oct 2026 02:24:57 +0000 Subject: [PATCH 3/4] Bind release bootstrap to the reusable workflow checkout Load local trust and tool actions from job.workflow_sha and bind module gates to installed version and lock-identity outputs. Keep optional preflight output parsing skip-safe without a version fallback, and preserve caller verification before release credentials. Co-authored-by: c1-squire-dev[bot] --- .github/workflows/release.yaml | 182 ++++++++++++++++++++------------- docs/release-workflow.md | 67 +++++++----- scripts/test-tool-integrity.py | 96 +++++++++++------ 3 files changed, 216 insertions(+), 129 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5578caf..729dd7f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -118,10 +118,20 @@ concurrency: jobs: validate-inputs: runs-on: ubuntu-latest + permissions: + contents: read steps: + - name: Checkout connector workflows + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 + with: + path: _workflows + repository: ConductorOne/github-workflows + ref: ${{ job.workflow_sha }} + persist-credentials: false + - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 + uses: ./_workflows/.github/actions/ca-trust-guard - name: Validate tag format env: @@ -179,17 +189,17 @@ jobs: go_main_package: ${{ steps.release-options.outputs.go_main_package }} brew_tap: ${{ steps.release-options.outputs.brew_tap }} steps: - - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - - name: Checkout pinned connector workflows - uses: actions/checkout@v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 with: path: _workflows repository: ConductorOne/github-workflows ref: ${{ job.workflow_sha }} persist-credentials: false + - name: Enforce runner CA trust + uses: ./_workflows/.github/actions/ca-trust-guard + - name: Validate dockerfile_template has safe path if: inputs.dockerfile_template != '' working-directory: _workflows @@ -263,12 +273,21 @@ jobs: outputs: github_prerelease: ${{ steps.github-release.outputs.prerelease }} steps: + - name: Checkout connector workflows + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 + with: + path: _workflows + repository: ConductorOne/github-workflows + ref: ${{ job.workflow_sha }} + persist-credentials: false + - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 + uses: ./_workflows/.github/actions/ca-trust-guard - name: Install locked release tools + id: tools if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true - uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 + uses: ./_workflows/.github/actions/locked-tools with: tools: go @@ -282,15 +301,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Checkout connector workflows - if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 - with: - path: _workflows - repository: ConductorOne/github-workflows - ref: ${{ needs.determine-workflows-ref.outputs.ref }} - persist-credentials: false - - name: Verify caller checkout matches release tag if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true shell: bash @@ -301,6 +311,10 @@ jobs: - name: Check caller Go requirement if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true + env: + EXPECTED_GO_VERSION: ${{ fromJSON(steps.tools.outputs.versions || '{}').go }} + EXPECTED_LOCK_SHA256: ${{ steps.tools.outputs.lock_sha256 }} + LOCK_PATH: ${{ steps.tools.outputs.lock_path }} run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - name: Capture GitHub Release pre-release setting @@ -390,11 +404,20 @@ jobs: binaries_checksums: ${{ steps.output-checksums.outputs.checksums }} released_at: ${{ steps.release-meta.outputs.released_at }} steps: + - name: Checkout connector workflows + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 + with: + path: _workflows + repository: ConductorOne/github-workflows + ref: ${{ job.workflow_sha }} + persist-credentials: false + - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 + uses: ./_workflows/.github/actions/ca-trust-guard - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 + id: tools + uses: ./_workflows/.github/actions/locked-tools with: tools: go,gon,syft,goreleaser @@ -407,14 +430,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Checkout connector workflows - uses: actions/checkout@v5 - with: - path: _workflows - repository: ConductorOne/github-workflows - ref: ${{ needs.determine-workflows-ref.outputs.ref }} - persist-credentials: false - - name: Verify caller checkout matches release tag shell: bash env: @@ -436,6 +451,10 @@ jobs: --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" - name: Check caller Go requirement + env: + EXPECTED_GO_VERSION: ${{ fromJSON(steps.tools.outputs.versions || '{}').go }} + EXPECTED_LOCK_SHA256: ${{ steps.tools.outputs.lock_sha256 }} + LOCK_PATH: ${{ steps.tools.outputs.lock_path }} run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - name: Install cosign @@ -704,11 +723,20 @@ jobs: outputs: windows_manifest: ${{ steps.generate-windows-manifest.outputs.windows_manifest }} steps: + - name: Checkout connector workflows + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 + with: + path: _workflows + repository: ConductorOne/github-workflows + ref: ${{ job.workflow_sha }} + persist-credentials: false + - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 + uses: ./_workflows/.github/actions/ca-trust-guard - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 + id: tools + uses: ./_workflows/.github/actions/locked-tools with: tools: go,syft,goreleaser-pro @@ -725,14 +753,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Checkout connector workflows - uses: actions/checkout@v5 - with: - path: _workflows - repository: ConductorOne/github-workflows - ref: ${{ needs.determine-workflows-ref.outputs.ref }} - persist-credentials: false - - name: Verify caller checkout matches release tag shell: bash env: @@ -755,6 +775,10 @@ jobs: - name: Check caller Go requirement shell: bash + env: + EXPECTED_GO_VERSION: ${{ fromJSON(steps.tools.outputs.versions || '{}').go }} + EXPECTED_LOCK_SHA256: ${{ steps.tools.outputs.lock_sha256 }} + LOCK_PATH: ${{ steps.tools.outputs.lock_path }} run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - name: Generate or locate WXS file @@ -982,11 +1006,20 @@ jobs: env: GOTOOLCHAIN: local steps: + - name: Checkout connector workflows + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 + with: + path: _workflows + repository: ConductorOne/github-workflows + ref: ${{ job.workflow_sha }} + persist-credentials: false + - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 + uses: ./_workflows/.github/actions/ca-trust-guard - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 + id: tools + uses: ./_workflows/.github/actions/locked-tools with: tools: go,goreleaser @@ -999,15 +1032,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Checkout connector workflows - if: inputs.docker == true || inputs.lambda == true - uses: actions/checkout@v5 - with: - path: _workflows - repository: ConductorOne/github-workflows - ref: ${{ needs.determine-workflows-ref.outputs.ref }} - persist-credentials: false - - name: Verify caller checkout matches release tag shell: bash env: @@ -1034,6 +1058,10 @@ jobs: --output-name ecr_push_role_name >> "$GITHUB_OUTPUT" - name: Check caller Go requirement + env: + EXPECTED_GO_VERSION: ${{ fromJSON(steps.tools.outputs.versions || '{}').go }} + EXPECTED_LOCK_SHA256: ${{ steps.tools.outputs.lock_sha256 }} + LOCK_PATH: ${{ steps.tools.outputs.lock_path }} run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _caller/go.mod _workflows/go.mod - name: Install cosign @@ -1274,22 +1302,23 @@ jobs: env: GOTOOLCHAIN: local steps: - - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - - - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 - with: - tools: go - - name: Checkout connector workflows - uses: actions/checkout@v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 with: path: _workflows repository: ConductorOne/github-workflows - ref: ${{ needs.determine-workflows-ref.outputs.ref }} + ref: ${{ job.workflow_sha }} persist-credentials: false + - name: Enforce runner CA trust + uses: ./_workflows/.github/actions/ca-trust-guard + + - name: Install locked release tools + id: tools + uses: ./_workflows/.github/actions/locked-tools + with: + tools: go + - name: Derive AWS role names id: role-names working-directory: _workflows @@ -1304,6 +1333,10 @@ jobs: --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" - name: Check workflows Go requirement + env: + EXPECTED_GO_VERSION: ${{ fromJSON(steps.tools.outputs.versions || '{}').go }} + EXPECTED_LOCK_SHA256: ${{ steps.tools.outputs.lock_sha256 }} + LOCK_PATH: ${{ steps.tools.outputs.lock_path }} run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _workflows/go.mod - name: Merge binaries, Windows, and images manifests @@ -1531,22 +1564,23 @@ jobs: env: GOTOOLCHAIN: local steps: - - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - - - name: Install locked release tools - uses: ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448 - with: - tools: go - - name: Checkout connector workflows - uses: actions/checkout@v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 with: path: _workflows repository: ConductorOne/github-workflows - ref: ${{ needs.determine-workflows-ref.outputs.ref }} + ref: ${{ job.workflow_sha }} persist-credentials: false + - name: Enforce runner CA trust + uses: ./_workflows/.github/actions/ca-trust-guard + + - name: Install locked release tools + id: tools + uses: ./_workflows/.github/actions/locked-tools + with: + tools: go + - name: Checkout connector repo uses: actions/checkout@v5 with: @@ -1565,6 +1599,10 @@ jobs: run: bash _workflows/scripts/verify-release-checkout.sh _connector - name: Check workflows Go requirement + env: + EXPECTED_GO_VERSION: ${{ fromJSON(steps.tools.outputs.versions || '{}').go }} + EXPECTED_LOCK_SHA256: ${{ steps.tools.outputs.lock_sha256 }} + LOCK_PATH: ${{ steps.tools.outputs.lock_path }} run: bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module _workflows/go.mod - name: Read connector documentation @@ -1694,17 +1732,17 @@ jobs: contents: read runs-on: ubuntu-latest steps: - - name: Enforce runner CA trust - uses: ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76 - - name: Checkout shared workflows - uses: actions/checkout@v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # actions/checkout@v5 with: repository: ConductorOne/github-workflows - ref: ${{ needs.determine-workflows-ref.outputs.ref }} + ref: ${{ job.workflow_sha }} path: _workflows persist-credentials: false + - name: Enforce runner CA trust + uses: ./_workflows/.github/actions/ca-trust-guard + - name: Install cosign uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 with: diff --git a/docs/release-workflow.md b/docs/release-workflow.md index 0dd083d..a82faca 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -235,15 +235,18 @@ Every GoReleaser invocation receives `GORELEASER_CURRENT_TAG` from the workflow ### Release Job Bootstrap -Nine release jobs start with the SHA-pinned `ca-trust-guard` action before -**any checkout or credential access**, using its default -`enforce`/`true`/`true`/`fail` inputs. Six tool-using jobs then run the pinned -`locked-tools` action as step 2, followed by source/workflow checkouts, release -commit verification, module requirements, and release credentials. The guard -uses commit `4d06ff31da09d2012770879c2c43c54dec24fe76`; locked-tools uses -`562a1ea6fa6abd0460950dfc95c7f845aa5f1448`. Neither requires a caller checkout -to bootstrap. The guard's measurements and residual-client limits are -documented in [CA Trust Guard](../.github/actions/ca-trust-guard/README.md). +Nine release jobs first check out `_workflows` at `${{ job.workflow_sha }}` +with a SHA-pinned checkout action, `persist-credentials: false`, and no token +input. This read-only infrastructure checkout provides the workflow's own +action implementations and canonical lock; it does not execute caller code. +The job then runs `./_workflows/.github/actions/ca-trust-guard` with its default +`enforce`/`true`/`true`/`fail` inputs. Six tool-using jobs next run +`./_workflows/.github/actions/locked-tools` with `id: tools`, before caller +checkout, release-commit verification, module requirements, and credentials. +The workflow uses no remote self-action SHA references: its workflow, local +actions, and lock come from the same checkout commit. The guard's measurements +and residual-client limits are documented in +[CA Trust Guard](../.github/actions/ca-trust-guard/README.md). `notify-release-failure` is deliberately exempt: guard errors must not prevent failure reporting. The job remains failure-only and does not install build @@ -259,12 +262,14 @@ the guard but no locked-tools installation. | publish-release-manifest | `go` | | record-registry-api | `go` | -Preflight keeps its existing `go_source_hygiene || go_vulnerability_scan` -condition on the tool action, checkouts, and module gate, avoiding new downloads -when both checks are disabled. Its guard still runs first unconditionally. -Job-level `GOTOOLCHAIN=local` and the binaries job's readonly-module/proxy/sumdb -settings are unchanged. Cosign and bundled-buildx/QEMU setup remain after the -trust/tool bootstrap and before their signing or AWS credentials. +The preflight `_workflows` checkout and local guard always run. Its +`go_source_hygiene || go_vulnerability_scan` condition applies to locked-tools, +caller checkout, source verification, and the module gate, so disabling both +optional checks does not download a Go toolchain. +Tool-using jobs set `GOTOOLCHAIN=local` at job scope. The binaries job also sets +`GOFLAGS=-mod=readonly`, `GOPROXY=https://proxy.golang.org`, and +`GOSUMDB=sum.golang.org` at job scope. Cosign and bundled-buildx/QEMU setup run +after trust/tool bootstrap and before their signing or AWS credentials. ### Locked Release Tools @@ -276,13 +281,22 @@ binaries during extraction. The action's Go assertion and native gon build run after verified installation. Release credentials are configured only after tool installation and checks. Helper code checkouts use `job.workflow_sha`. -Go is installed once per job, with `GOTOOLCHAIN=local`. Go gates take -`EXPECTED_GO_VERSION`, `EXPECTED_LOCK_SHA256` and `LOCK_PATH` from the -locked-tools action's `go`, `lock_sha256` and `lock_path` outputs. The gate -hashes the referenced lock to check identity but never parses its versions; -prepare owns the single-version sanity. Version equality and readonly/offline -`GOTOOLCHAIN=local go list -m` checks reject incompatible module requirements. -The release workflow does not cache Go modules. +Go is installed once per tool-using job, with `GOTOOLCHAIN=local`. Each of the +six module gates invokes the checked-out action-local `verify-locked-go.sh` +after source verification and receives the installed action's identity: + +- `EXPECTED_GO_VERSION`: `${{ fromJSON(steps.tools.outputs.versions || '{}').go }}` +- `EXPECTED_LOCK_SHA256`: `${{ steps.tools.outputs.lock_sha256 }}` +- `LOCK_PATH`: `${{ steps.tools.outputs.lock_path }}` + +The empty-JSON expression is safe when optional tools are skipped: step +environments are evaluated before step conditions. It supplies no version +fallback; an executing gate with incomplete outputs fails closed. +The gate hashes the referenced lock to check identity but never parses its +versions; prepare owns the single-version sanity. Version equality and +readonly/offline `GOTOOLCHAIN=local go list -m` checks reject incompatible module +requirements. The action asserts the installed Go version itself; the workflow +does not duplicate standalone version gates or cache Go modules. Artifact hashes identify the archives at the upstream release URLs. [Go release metadata](https://go.dev/dl/?mode=json) provides the toolchain @@ -291,7 +305,7 @@ integrity uses the committed lock rather than a checksum fetched during the job. Node.js 26.12.1 uses platform-specific archive locks with checksums from [SHASUMS256.txt](https://nodejs.org/dist/v26.12.1/SHASUMS256.txt). -The `ConductorOne/github-workflows/.github/actions/locked-tools@` +The local `./_workflows/.github/actions/locked-tools` composite action accepts a comma-separated `tools` list, an optional reviewed `locks` file (default: the action's canonical lock), and `dest` (default: `${{ runner.temp }}/locked-tools`). Invoke it before configuring release @@ -316,8 +330,11 @@ between preparation, installation, and output publication; internal Go gates receive the same expected version/path/hash. Each phase is a one-line script call in the composite. See the [action contract](../.github/actions/locked-tools/README.md). -The action-local Go gate is environment-only. Callers supply the installed -action's version and lock-identity outputs; positional lockfile lookup is not supported. +The action-local Go gate is environment-only. It consumes the installed +action's version and lock-identity outputs, not a version re-read from a +checkout lock or positional lockfile arguments. Canonical implementations +and the single lock reside under `.github/actions/locked-tools/`; forwarding +wrappers under `scripts/` are not supported. | Tool / version | Lock platform | SHA256 or source commit | Committed artifact/source | | --- | --- | --- | --- | diff --git a/scripts/test-tool-integrity.py b/scripts/test-tool-integrity.py index df11c1c..271adf3 100755 --- a/scripts/test-tool-integrity.py +++ b/scripts/test-tool-integrity.py @@ -17,8 +17,13 @@ ROOT = Path(__file__).resolve().parent.parent ACTION = ROOT / ".github/actions/locked-tools" -GUARD_ACTION = "ConductorOne/github-workflows/.github/actions/ca-trust-guard@4d06ff31da09d2012770879c2c43c54dec24fe76" -TOOLS_ACTION = "ConductorOne/github-workflows/.github/actions/locked-tools@562a1ea6fa6abd0460950dfc95c7f845aa5f1448" +GUARD_ACTION = "./_workflows/.github/actions/ca-trust-guard" +TOOLS_ACTION = "./_workflows/.github/actions/locked-tools" +GO_GATE_ENV = { + "EXPECTED_GO_VERSION": "${{ fromJSON(steps.tools.outputs.versions || '{}').go }}", + "EXPECTED_LOCK_SHA256": "${{ steps.tools.outputs.lock_sha256 }}", + "LOCK_PATH": "${{ steps.tools.outputs.lock_path }}", +} class IntegrityTests(unittest.TestCase): @@ -453,7 +458,7 @@ def test_action_go_outputs_feed_environment_gate(self) -> None: self.assertEqual(outputs["lock_path"], plan["lock_path"]) self.assertEqual(outputs["lock_sha256"], plan["lock_sha256"]) self.env["PATH"] = f"{self.root / 'dest/go/bin'}:{self.env['PATH']}" - self.env["EXPECTED_GO_VERSION"] = outputs["go"] + self.env["EXPECTED_GO_VERSION"] = json.loads(outputs["versions"])["go"] self.run_script("verify-locked-go.sh", "version") def test_action_prepare_owns_single_go_version_sanity(self) -> None: @@ -629,12 +634,19 @@ def test_composite_metadata_and_single_canonical_lock(self) -> None: self.assertEqual(step["env"]["LOCK_PATH"], "${{ steps.prepare.outputs.lock_path }}") self.assertEqual(step["env"]["EXPECTED_GO_VERSION"], "${{ steps.prepare.outputs.go_version }}") + def test_release_workflow_has_no_remote_self_actions_or_duplicate_bootstrap(self) -> None: + text = (ROOT / ".github/workflows/release.yaml").read_text() + self.assertNotIn("ConductorOne/github-workflows/.github/actions/", text) + self.assertNotIn("verify-locked-go.sh version", text) + self.assertNotIn("gon-source.sh", text) + for legacy in ("_workflows/scripts/install-tool", "_workflows/scripts/gon-source", "_workflows/scripts/verify-locked-go"): + self.assertNotIn(legacy, text) + def test_workflow_security_wiring(self) -> None: workflow = json.loads(subprocess.check_output(["yq", ".", str(ROOT / ".github/workflows/release.yaml")], text=True)) jobs = workflow["jobs"] self.assertEqual(jobs["determine-workflows-ref"]["outputs"]["ref"], "${{ job.workflow_sha }}") - for legacy in ("_workflows/scripts/install-tool", "_workflows/scripts/gon-source", "_workflows/scripts/verify-locked-go"): - self.assertNotIn(legacy, json.dumps(workflow)) + self.assertEqual(jobs["validate-inputs"]["permissions"], {"contents": "read"}) expected_tools = { "validate-inputs": "", "determine-workflows-ref": "", @@ -648,45 +660,63 @@ def test_workflow_security_wiring(self) -> None: "notify-release-failure": "", } self.assertEqual(set(jobs), set(expected_tools)) - guard_count = tool_count = 0 + checkout_count = guard_count = tool_count = gate_count = 0 for job_name, job in jobs.items(): with self.subTest(job=job_name): steps = job["steps"] - guards = [index for index, step in enumerate(steps) if "ca-trust-guard@" in step.get("uses", "")] - tools = [index for index, step in enumerate(steps) if "/locked-tools@" in step.get("uses", "")] + guards = [index for index, step in enumerate(steps) if step.get("uses") == GUARD_ACTION] + tools = [index for index, step in enumerate(steps) if step.get("uses") == TOOLS_ACTION] + workflow_checkouts = [index for index, step in enumerate(steps) + if step.get("with", {}).get("path") == "_workflows"] if job_name == "notify-release-failure": + self.assertEqual(workflow_checkouts, []) self.assertEqual(guards, [], "guard failures must not suppress failure reporting") self.assertEqual(tools, []) self.assertEqual(job["if"], "failure()") continue - self.assertEqual(guards, [0]) - self.assertEqual(steps[0]["uses"], GUARD_ACTION) - self.assertEqual(steps[0].get("with", {}), {}, "use enforce/true/true/fail defaults") - self.assertNotIn("if", steps[0]) - self.assertNotIn("continue-on-error", steps[0]) + self.assertEqual(workflow_checkouts, [0]) + bootstrap = steps[0] + self.assertRegex(bootstrap["uses"], r"^actions/checkout@[0-9a-f]{40}$") + self.assertEqual(bootstrap["with"], { + "path": "_workflows", + "repository": "ConductorOne/github-workflows", + "ref": "${{ job.workflow_sha }}", + "persist-credentials": False, + }) + self.assertNotIn("if", bootstrap) + self.assertNotIn("continue-on-error", bootstrap) + self.assertEqual(job["permissions"]["contents"], "read") + checkout_count += 1 + self.assertEqual(guards, [1]) + self.assertEqual(steps[1].get("with", {}), {}, "use enforce/true/true/fail defaults") + self.assertNotIn("if", steps[1]) + self.assertNotIn("continue-on-error", steps[1]) guard_count += 1 - prefix_length = 1 + prefix_length = 2 if expected_tools[job_name]: - self.assertEqual(tools, [1]) - self.assertEqual(steps[1]["uses"], TOOLS_ACTION) - self.assertEqual(steps[1]["with"], {"tools": expected_tools[job_name]}) - self.assertNotIn("continue-on-error", steps[1]) + self.assertEqual(tools, [2]) + self.assertEqual(steps[2]["id"], "tools") + self.assertEqual(steps[2]["with"], {"tools": expected_tools[job_name]}) + self.assertNotIn("continue-on-error", steps[2]) self.assertEqual(job["env"]["GOTOOLCHAIN"], "local") if job_name == "release-preflight": - self.assertEqual(steps[1]["if"], "inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true") + self.assertEqual(steps[2]["if"], "inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true") else: - self.assertNotIn("if", steps[1]) - prefix_length = 2 + self.assertNotIn("if", steps[2]) + prefix_length = 3 tool_count += 1 else: self.assertEqual(tools, []) - checkouts = [index for index, step in enumerate(steps) if step.get("uses", "").startswith("actions/checkout@")] - verifications = [index for index, step in enumerate(steps) if "verify-release-checkout.sh" in step.get("run", "")] + caller_checkouts = [index for index, step in enumerate(steps) + if step.get("with", {}).get("path") in ("_caller", "_connector")] + checkouts = workflow_checkouts + caller_checkouts + verifications = [index for index, step in enumerate(steps) + if "verify-release-checkout.sh" in step.get("run", "")] credentials = [index for index, step in enumerate(steps) if "secrets." in str(step) or "github.token" in str(step) or "core.getIDToken" in str(step) or "configure-aws-credentials@" in step.get("uses", "")] - self.assertTrue(all(index >= prefix_length for index in checkouts)) - if checkouts and verifications: + self.assertTrue(all(index >= prefix_length for index in caller_checkouts)) + if verifications: self.assertLess(max(checkouts), min(verifications)) if credentials: self.assertLess(prefix_length - 1, min(credentials)) @@ -702,29 +732,31 @@ def test_workflow_security_wiring(self) -> None: if "/locked-tools/verify-locked-go.sh module " in step.get("run", "")] if expected_tools[job_name]: self.assertEqual(len(module_gates), 1) + gate = steps[module_gates[0]] + self.assertEqual(gate["env"], GO_GATE_ENV) self.assertLess(max(checkouts), module_gates[0]) if verifications: self.assertLess(max(verifications), module_gates[0]) if credentials: self.assertLess(module_gates[0], min(credentials)) modules = "_workflows/go.mod" if job_name in ("publish-release-manifest", "record-registry-api") else "_caller/go.mod _workflows/go.mod" - self.assertEqual(steps[module_gates[0]]["run"], + self.assertEqual(gate["run"], "bash _workflows/.github/actions/locked-tools/verify-locked-go.sh module " + modules) if job_name == "release-preflight": - self.assertEqual(steps[module_gates[0]]["if"], steps[1]["if"]) + self.assertEqual(gate["if"], steps[2]["if"]) + gate_count += 1 for step in steps: - run = step.get("run", "") self.assertNotEqual(step.get("with", {}).get("repository"), "ConductorOne/gon") if step.get("with", {}).get("path") in ("_caller", "_connector"): self.assertEqual(step["with"]["ref"], "${{ github.sha }}") - if step.get("with", {}).get("path") == "_workflows": - self.assertIn(step["with"]["ref"], ("${{ job.workflow_sha }}", "${{ needs.determine-workflows-ref.outputs.ref }}")) - if "verify-release-checkout.sh" in run: + if "verify-release-checkout.sh" in step.get("run", ""): self.assertEqual(step["env"]["EXPECTED_COMMIT"], "${{ github.sha }}") if "cosign-installer@" in step.get("uses", ""): self.assertEqual(step["with"]["cosign-release"], "v2.5.2") self.assertRegex(step["uses"], r"@[0-9a-f]{40}$") - self.assertEqual((guard_count, tool_count), (9, 6)) + ids = [step["id"] for step in steps if "id" in step] + self.assertEqual(len(ids), len(set(ids))) + self.assertEqual((checkout_count, guard_count, tool_count, gate_count), (9, 9, 6, 6)) for wrapper in ("install-tool.sh", "install-tool.ps1", "gon-source.sh", "verify-locked-go.sh"): self.assertFalse((ROOT / "scripts" / wrapper).exists()) From 5b145dbbba57e71e4eb55a937bea8fe32b866a5c Mon Sep 17 00:00:00 2001 From: Steve Gontzes Date: Thu, 22 Oct 2026 02:32:07 +0000 Subject: [PATCH 4/4] Document authenticated workflow bootstrap checkout accurately Explain the read-scoped GitHub token, non-persistence, pre-guard fetch, and workflow-commit integrity binding without changing checkout inputs or tests. Co-authored-by: c1-squire-dev[bot] --- docs/release-workflow.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/release-workflow.md b/docs/release-workflow.md index a82faca..2d60455 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -236,9 +236,14 @@ Every GoReleaser invocation receives `GORELEASER_CURRENT_TAG` from the workflow ### Release Job Bootstrap Nine release jobs first check out `_workflows` at `${{ job.workflow_sha }}` -with a SHA-pinned checkout action, `persist-credentials: false`, and no token -input. This read-only infrastructure checkout provides the workflow's own -action implementations and canonical lock; it does not execute caller code. +with a SHA-pinned checkout action and `persist-credentials: false`. This fetch +necessarily precedes the guard and, like the runner's own action downloads, +authenticates to github.com with the job's read-scoped `GITHUB_TOKEN` +(actions/checkout requires a token); the token is not persisted, the host is not +behind the incident intermediates, and content integrity is bound to +`job.workflow_sha`. This read-only infrastructure checkout provides the +workflow's own action implementations and canonical lock; it does not execute +caller code. The job then runs `./_workflows/.github/actions/ca-trust-guard` with its default `enforce`/`true`/`true`/`fail` inputs. Six tool-using jobs next run `./_workflows/.github/actions/locked-tools` with `id: tools`, before caller