From 8f516a014bdd0bf63158553547e343665db57e59 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:09:36 +0100 Subject: [PATCH 1/2] Bind release dispatches to the admitted commit A workflow dispatch by branch can resolve after that branch moves, allowing an unreviewed tip to enter the release pipeline. Require every publishing dispatch to name its expected source SHA and make the workflow reject a different source or workflow commit before checkout. Change-source: pulse-maintainer (cherry picked from commit a461fc9c0acb09bc1fd48228a8c32c5303ad4bc8) (cherry picked from commit f503b134425783af4b6102a9b79bae9379757754) --- .github/workflows/create-release.yml | 20 +++++++++++++++++++ .../subsystems/deployment-installability.md | 7 +++++++ .../installtests/build_release_assets_test.go | 4 ++++ .../release_promotion_policy_test.py | 4 ++++ scripts/trigger-release.sh | 3 +++ scripts/trigger-stable-patch.sh | 3 +++ 6 files changed, 41 insertions(+) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index f5bd3fda5..4bead5286 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -9,6 +9,10 @@ on: description: 'Version number (e.g., 4.30.0)' required: true type: string + expected_source_sha: + description: 'Exact 40-character commit SHA admitted for this release' + required: true + type: string release_notes: description: 'Release notes (markdown)' required: true @@ -108,6 +112,22 @@ jobs: visual_capture_count: ${{ steps.visual_plan.outputs.capture_count }} visual_comparison_tag: ${{ steps.visual_plan.outputs.comparison_tag }} steps: + - name: Verify admitted source commit + env: + EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }} + run: | + set -euo pipefail + if [[ ! "${EXPECTED_SOURCE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::expected_source_sha must be an exact 40-character commit SHA" + exit 1 + fi + if [[ "${GITHUB_SHA}" != "${EXPECTED_SOURCE_SHA}" || \ + "${GITHUB_WORKFLOW_SHA}" != "${EXPECTED_SOURCE_SHA}" ]]; then + echo "::error::Release dispatch expected ${EXPECTED_SOURCE_SHA}, but GitHub resolved source ${GITHUB_SHA} and workflow ${GITHUB_WORKFLOW_SHA}." + exit 1 + fi + echo "[OK] Release dispatch is bound to ${EXPECTED_SOURCE_SHA}" + - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 49525a70b..b6e71af38 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -2168,6 +2168,13 @@ artifact-selection behaviour. trigger, promotion resolver, rendered release body, current upgrade guide, or current release packet that routes systemd/LXC rollback through the Unified Agent installer, and must retain explicit Docker image guidance. +17. Bind every publishing release dispatch to the exact commit admitted by the + caller. `.github/workflows/create-release.yml` must require a full + 40-character `expected_source_sha` and, before checkout, reject the run + unless both `GITHUB_SHA` and `GITHUB_WORKFLOW_SHA` equal that commit. + `scripts/trigger-release.sh` and `scripts/trigger-stable-patch.sh` must send + the exact remote candidate SHA they already verified; branch ancestry or a + later branch tip is not equivalent release admission. ## Current State diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 521db80c8..f72ce74a9 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -933,6 +933,10 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) { convergenceWorkflow := string(convergenceContent) required := []string{ `historical_asset_backfill_only:`, + `expected_source_sha:`, + `EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }}`, + `"${GITHUB_SHA}" != "${EXPECTED_SOURCE_SHA}"`, + `"${GITHUB_WORKFLOW_SHA}" != "${EXPECTED_SOURCE_SHA}"`, `description: 'Repair an already-published release packet in place without rebuilding binaries'`, `SYFT_VERSION="1.42.4"`, `SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"`, diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 8899c3918..0b712a641 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -1694,6 +1694,10 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("build_rollback_section", renderer) self.assertIn("promotion metadata out of customer notes", renderer) self.assertIn("historical_asset_backfill_only:", content) + self.assertIn("expected_source_sha:", content) + self.assertIn('EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }}', content) + self.assertIn('"${GITHUB_SHA}" != "${EXPECTED_SOURCE_SHA}"', content) + self.assertIn('"${GITHUB_WORKFLOW_SHA}" != "${EXPECTED_SOURCE_SHA}"', content) self.assertIn("Repair an already-published release packet in place without rebuilding binaries", content) self.assertIn("draft: true", content) self.assertIn("activate_release:", content) diff --git a/scripts/trigger-release.sh b/scripts/trigger-release.sh index 0d6acd7f4..79a901e79 100755 --- a/scripts/trigger-release.sh +++ b/scripts/trigger-release.sh @@ -120,6 +120,7 @@ python3 scripts/check-workflow-dispatch-inputs.py \ --workflow-path .github/workflows/create-release.yml \ --branch "$CURRENT_BRANCH" \ --require version \ + --require expected_source_sha \ --require release_notes \ --require release_screenshot_plan \ --require promoted_from_tag \ @@ -372,6 +373,7 @@ echo "Triggering release workflow..." if [ -n "$NOTES_FILE" ]; then jq -n \ --arg version "$VERSION" \ + --arg expected_source_sha "$LOCAL" \ --rawfile release_notes "$NOTES_FILE" \ --rawfile release_screenshot_plan "$VISUAL_PLAN_FILE" \ --arg rollback_version "$ROLLBACK_VERSION" \ @@ -387,6 +389,7 @@ if [ -n "$NOTES_FILE" ]; then --arg mobile_release_evidence "$MOBILE_RELEASE_EVIDENCE" \ '{ version: $version, + expected_source_sha: $expected_source_sha, release_notes: $release_notes, release_screenshot_plan: $release_screenshot_plan, rollback_version: $rollback_version, diff --git a/scripts/trigger-stable-patch.sh b/scripts/trigger-stable-patch.sh index c4f759894..efe146cd8 100755 --- a/scripts/trigger-stable-patch.sh +++ b/scripts/trigger-stable-patch.sh @@ -203,6 +203,7 @@ else --workflow-path .github/workflows/create-release.yml \ --branch "$CURRENT_BRANCH" \ --require version \ + --require expected_source_sha \ --require release_notes \ --require release_screenshot_plan \ --require promoted_from_tag \ @@ -219,6 +220,7 @@ else jq -n \ --arg version "$VERSION" \ + --arg expected_source_sha "$LOCAL_SHA" \ --rawfile release_notes "$NOTES_FILE" \ --rawfile release_screenshot_plan "$VISUAL_PLAN_FILE" \ --arg promoted_from_tag "" \ @@ -234,6 +236,7 @@ else --arg mobile_release_evidence "$MOBILE_RELEASE_EVIDENCE" \ '{ version: $version, + expected_source_sha: $expected_source_sha, release_notes: $release_notes, release_screenshot_plan: $release_screenshot_plan, promoted_from_tag: $promoted_from_tag, From b03041cdb6f99e2c18e4ecd58d7d93856dcc803c Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:50:23 +0100 Subject: [PATCH 2/2] Test release dispatch identity guard against commit drift The candidate publisher rejected its admitted SHA input. Execute the backported workflow guard in focused tests so accepting the input cannot silently permit source or workflow drift before checkout. Change-source: pulse-maintainer --- .../release_promotion_policy_test.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 0b712a641..216cc4598 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -1595,6 +1595,31 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("did not produce a valid promotion metadata envelope", workflow) self.assertIn("Do not use this artifact to clear", workflow) + def test_release_dispatch_rejects_source_or_workflow_drift_before_checkout(self) -> None: + workflow = yaml.safe_load(read(".github/workflows/create-release.yml")) + steps = workflow["jobs"]["prepare"]["steps"] + self.assertEqual(steps[0]["name"], "Verify admitted source commit") + self.assertIn("actions/checkout@", steps[1]["uses"]) + expected = "a" * 40 + other = "b" * 40 + for admitted, source, workflow_sha, succeeds in ( + (expected, expected, expected, True), + (expected, other, expected, False), + (expected, expected, other, False), + (expected, other, other, False), + ("", expected, expected, False), + ("main", expected, expected, False), + ("a" * 39, expected, expected, False), + ): + with self.subTest(admitted=admitted, source=source, workflow=workflow_sha): + result = subprocess.run( + ["bash", "-c", steps[0]["run"]], + env={"PATH": os.defpath, "EXPECTED_SOURCE_SHA": admitted, + "GITHUB_SHA": source, "GITHUB_WORKFLOW_SHA": workflow_sha}, + capture_output=True, text=True, check=False, + ) + self.assertEqual(result.returncode == 0, succeeds, result.stdout + result.stderr) + def test_release_workflow_enforces_rc_lineage_soak_and_v5_notice(self) -> None: content = read(".github/workflows/create-release.yml") update_demo_workflow = read(".github/workflows/update-demo-server.yml")