From 85986b93f5a3d95997e0ff6b61cacd6ab3634e13 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Thu, 20 Aug 2026 11:25:33 -0400 Subject: [PATCH 1/2] ci: prevent stale-head releases --- .github/workflows/canvas-nodom-ladder.yml | 6 +++--- .github/workflows/paper.yml | 8 +++---- .github/workflows/release.yml | 11 ++++++++++ tests/test_release_contract.py | 26 ++++++++++++++++++++++- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/.github/workflows/canvas-nodom-ladder.yml b/.github/workflows/canvas-nodom-ladder.yml index 7a3b7a13..35670883 100644 --- a/.github/workflows/canvas-nodom-ladder.yml +++ b/.github/workflows/canvas-nodom-ladder.yml @@ -25,9 +25,9 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" @@ -89,7 +89,7 @@ jobs: - name: Upload evidence if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: canvas-nodom-ladder-qualification path: runs/canvas-ladder/results.json diff --git a/.github/workflows/paper.yml b/.github/workflows/paper.yml index 17a3fb69..ba5c74f4 100644 --- a/.github/workflows/paper.yml +++ b/.github/workflows/paper.yml @@ -27,7 +27,7 @@ jobs: # RDP workflow but did not reach this file. timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check paper constants against benchmark artifacts run: python paper/check_artifacts.py - name: Install TeX @@ -65,12 +65,12 @@ jobs: timeout-minutes: 10 run: make -C paper - name: Upload full report PDF - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: openadapt-paper path: paper/build/main.pdf - name: Upload workshop PDF - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: openadapt-paper-workshop path: paper/workshop/build/main.pdf @@ -90,7 +90,7 @@ jobs: contents: write # create/overwrite the release asset on the stable tag steps: - name: Download built full report PDF - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: openadapt-paper path: pdf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95c78f6d..4eedc2dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,6 +80,17 @@ jobs: --wait-seconds 2700 \ --poll-seconds 10 + - name: Require dispatched head to remain current protected main + if: steps.check_skip.outputs.skip != 'true' + run: | + set -euo pipefail + git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main + current_main="$(git rev-parse refs/remotes/origin/main)" + if [ "$current_main" != "$GITHUB_SHA" ]; then + echo "Refusing stale release dispatch: main is $current_main, dispatched head is $GITHUB_SHA." + exit 1 + fi + - name: Set up Python if: steps.check_skip.outputs.skip != 'true' uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 diff --git a/tests/test_release_contract.py b/tests/test_release_contract.py index 525f772b..00cc5e82 100644 --- a/tests/test_release_contract.py +++ b/tests/test_release_contract.py @@ -391,6 +391,22 @@ def test_release_workflow_uses_pinned_actions() -> None: assert "# v10.6.1" in workflow +def test_all_workflows_use_pinned_actions() -> None: + """A mutable action tag can change release or qualification code in place.""" + mutable: list[str] = [] + workflows_dir = ROOT / ".github/workflows" + paths = sorted((*workflows_dir.glob("*.yml"), *workflows_dir.glob("*.yaml"))) + for path in paths: + workflow = path.read_text() + for match in re.finditer( + r"^\s*uses:\s+\S+@([^\s#]+)", workflow, flags=re.MULTILINE + ): + if re.fullmatch(r"[0-9a-f]{40}", match.group(1)) is None: + mutable.append(f"{path.name}:{match.group(1)}") + + assert mutable == [] + + def test_semantic_release_requires_dispatched_exact_head_production_evidence() -> None: workflow = (ROOT / ".github/workflows/release.yml").read_text() @@ -399,6 +415,9 @@ def test_semantic_release_requires_dispatched_exact_head_production_evidence() - workflow.index("\n auto-release:") : workflow.index("\n manual-publish:") ] wait_index = auto.index("- name: Wait for exact-head full-matrix qualification") + current_main_index = auto.index( + "- name: Require dispatched head to remain current protected main" + ) release_index = auto.index("- name: Python Semantic Release") assert " push:" not in triggers @@ -407,12 +426,17 @@ def test_semantic_release_requires_dispatched_exact_head_production_evidence() - assert "- publish-existing-ref" in triggers assert "inputs.operation == 'semantic-release'" in auto assert "actions: read # inspect exact-head full-matrix CI" in auto - assert wait_index < release_index + assert wait_index < current_main_index < release_index assert "python3 scripts/check_release_ci.py" in auto assert '--repository "$GITHUB_REPOSITORY"' in auto assert '--sha "$GITHUB_SHA"' in auto assert "--wait-seconds 2700" in auto assert "--poll-seconds 10" in auto + assert ( + "git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main" in auto + ) + assert 'current_main="$(git rev-parse refs/remotes/origin/main)"' in auto + assert '[ "$current_main" != "$GITHUB_SHA" ]' in auto assert "gh api" not in auto gate = (ROOT / "scripts/check_release_ci.py").read_text() assert "require_production_qualification" in gate From 20ea6ef97d2ad4c7b956945c3c38369cef487870 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Thu, 20 Aug 2026 11:33:30 -0400 Subject: [PATCH 2/2] chore: refresh public artifact inventory --- public-artifacts.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public-artifacts.json b/public-artifacts.json index e43a91af..b582b994 100644 --- a/public-artifacts.json +++ b/public-artifacts.json @@ -89,7 +89,7 @@ }, { "path": ".github/workflows/canvas-nodom-ladder.yml", - "sha256": "29afaaa85b3284e5645111cf3493633940015ad419b8d8e6cbbc93966e20d982" + "sha256": "db5bdaf110b2d8acc15003eb5442ba9c2714a809fad3ac087cdc6ff5547e05a4" }, { "path": ".github/workflows/ci.yml", @@ -109,7 +109,7 @@ }, { "path": ".github/workflows/paper.yml", - "sha256": "6446edb96cddea116abe931611360122d8c4e3cba32e55c5ceab1f1e5209302a" + "sha256": "52e1d8d83c6d6b23be3fb99dee6d974056c465e1ba05e2a755a6a10262998307" }, { "path": ".github/workflows/quickstart-lifecycle.yml", @@ -121,7 +121,7 @@ }, { "path": ".github/workflows/release.yml", - "sha256": "20e760b316f9597b0cf8dc125574d8fdbaec86ca1bb34b6a6fb52fa86043a303" + "sha256": "7f5197dd1d7545185840a360d6dcea582d3c5eff0c916c9818b4cecc9a3dbc4d" }, { "path": ".github/workflows/validate-claims.yml",