From ad1cfd33c39160e27958f1cd93ce75c7da5438cc Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:35:47 +0100 Subject: [PATCH] fix(ci): qualify grouped release action pin consumers The grouped action upgrade leaves signing, network and publication consumer assertions on superseded pins. Align those contracts and check every consumer against reviewed immutable upstream manifests, retaining exact dispatch and release trust boundaries without claiming hosted execution. Change-source: pulse-maintainer --- .github/workflows/build-release-candidate.yml | 6 +- .github/workflows/compile-release-payload.yml | 2 +- .github/workflows/helm-ci.yml | 2 +- .github/workflows/helm-pages.yml | 2 +- .github/workflows/promote-floating-tags.yml | 4 +- .github/workflows/publish-docker.yml | 26 +- .github/workflows/publish-helm-chart.yml | 6 +- .../workflows/qualify-release-containers.yml | 2 +- .github/workflows/recover-demo-server.yml | 2 +- .../workflows/recover-release-activation.yml | 8 +- .github/workflows/security-scan.yml | 4 +- .github/workflows/signpath-test-signing.yml | 2 +- .github/workflows/update-demo-server.yml | 2 +- .../subsystems/deployment-installability.md | 19 ++ .../installtests/build_release_assets_test.go | 10 +- .../action_consumer_manifests.json | 225 ++++++++++++++++++ .../release_promotion_policy_support_test.py | 31 +++ .../release_promotion_policy_test.py | 46 +++- 18 files changed, 354 insertions(+), 45 deletions(-) create mode 100644 scripts/release_control/action_consumer_manifests.json diff --git a/.github/workflows/build-release-candidate.yml b/.github/workflows/build-release-candidate.yml index bebaa12c1..95f915092 100644 --- a/.github/workflows/build-release-candidate.yml +++ b/.github/workflows/build-release-candidate.yml @@ -462,7 +462,7 @@ jobs: - name: Submit SignPath Authenticode request if: ${{ inputs.windows_signing_backend == 'signpath' }} id: signpath - uses: signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2 + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} @@ -719,7 +719,7 @@ jobs: sudo apt-get install -y zip unzip - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 with: version: 'v3.15.2' @@ -937,7 +937,7 @@ jobs: # downloaded asset without access to GitHub's attestation API. - name: Attest complete release candidate id: attest_release_candidate - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-checksums: ${{ runner.temp }}/release-candidate-subjects.sha256 diff --git a/.github/workflows/compile-release-payload.yml b/.github/workflows/compile-release-payload.yml index 7c1b219bf..7799807bb 100644 --- a/.github/workflows/compile-release-payload.yml +++ b/.github/workflows/compile-release-payload.yml @@ -155,7 +155,7 @@ jobs: - name: Attest hosted qualification subjects id: attest_secure_runtime - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-checksums: ${{ runner.temp }}/secure-runtime-qualification/secure-runtime-compiler-subjects.sha256 diff --git a/.github/workflows/helm-ci.yml b/.github/workflows/helm-ci.yml index 5dd2f7dfa..24ef98aff 100644 --- a/.github/workflows/helm-ci.yml +++ b/.github/workflows/helm-ci.yml @@ -31,7 +31,7 @@ jobs: persist-credentials: false - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 with: version: v3.15.2 diff --git a/.github/workflows/helm-pages.yml b/.github/workflows/helm-pages.yml index 3ca3e13d3..de2464936 100644 --- a/.github/workflows/helm-pages.yml +++ b/.github/workflows/helm-pages.yml @@ -109,7 +109,7 @@ jobs: python3 scripts/write_github_output.py release_tag "${release_tag}" - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 with: version: v3.15.2 diff --git a/.github/workflows/promote-floating-tags.yml b/.github/workflows/promote-floating-tags.yml index f80a00584..972aed214 100644 --- a/.github/workflows/promote-floating-tags.yml +++ b/.github/workflows/promote-floating-tags.yml @@ -96,13 +96,13 @@ jobs: fi - name: Log in to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GHCR - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index a241e9c64..d23c07f0b 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -118,19 +118,19 @@ jobs: --source-sha "${EXPECTED_SOURCE_SHA}" - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GHCR - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -139,7 +139,7 @@ jobs: - name: Assemble and push Pulse server image (multi-arch) if: matrix.image == 'server' id: build_server_image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . target: runtime_prebuilt @@ -157,7 +157,7 @@ jobs: - name: Attest Pulse server image on Docker Hub if: matrix.image == 'server' - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: docker.io/rcourtman/pulse subject-digest: ${{ steps.build_server_image.outputs.digest }} @@ -166,7 +166,7 @@ jobs: - name: Attest Pulse server image on GHCR if: matrix.image == 'server' - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ghcr.io/${{ github.repository_owner }}/pulse subject-digest: ${{ steps.build_server_image.outputs.digest }} @@ -176,7 +176,7 @@ jobs: - name: Assemble and push Pulse control-plane image (multi-arch) if: matrix.image == 'control-plane' id: build_control_plane_image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . file: deploy/provider-msp/Dockerfile.control-plane @@ -195,7 +195,7 @@ jobs: - name: Attest Pulse control-plane image on Docker Hub if: matrix.image == 'control-plane' - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: docker.io/rcourtman/pulse-control-plane subject-digest: ${{ steps.build_control_plane_image.outputs.digest }} @@ -204,7 +204,7 @@ jobs: - name: Attest Pulse control-plane image on GHCR if: matrix.image == 'control-plane' - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ghcr.io/${{ github.repository_owner }}/pulse-control-plane subject-digest: ${{ steps.build_control_plane_image.outputs.digest }} @@ -249,16 +249,16 @@ jobs: persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GHCR - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/publish-helm-chart.yml b/.github/workflows/publish-helm-chart.yml index 9b785a382..15113ce11 100644 --- a/.github/workflows/publish-helm-chart.yml +++ b/.github/workflows/publish-helm-chart.yml @@ -108,7 +108,7 @@ jobs: echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 with: version: v3.15.2 @@ -175,7 +175,7 @@ jobs: # chart push authenticated through Helm, and authenticate the OCI # attestation client explicitly before it publishes provenance. - name: Authenticate OCI attestation client with GHCR - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -203,7 +203,7 @@ jobs: echo "chart_digest=${digests[0]}" >> "$GITHUB_OUTPUT" - name: Attest exact-version OCI chart - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ghcr.io/${{ github.repository_owner }}/pulse-chart/pulse subject-digest: ${{ steps.push.outputs.chart_digest }} diff --git a/.github/workflows/qualify-release-containers.yml b/.github/workflows/qualify-release-containers.yml index 95fc6212e..3bd1e1cd5 100644 --- a/.github/workflows/qualify-release-containers.yml +++ b/.github/workflows/qualify-release-containers.yml @@ -113,7 +113,7 @@ jobs: ' - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 with: version: v3.15.2 diff --git a/.github/workflows/recover-demo-server.yml b/.github/workflows/recover-demo-server.yml index d15713071..c482081c5 100644 --- a/.github/workflows/recover-demo-server.yml +++ b/.github/workflows/recover-demo-server.yml @@ -71,7 +71,7 @@ jobs: - name: Connect to the governed demo network id: tailscale - uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4 + uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} diff --git a/.github/workflows/recover-release-activation.yml b/.github/workflows/recover-release-activation.yml index 370aa0f17..8ee25fe50 100644 --- a/.github/workflows/recover-release-activation.yml +++ b/.github/workflows/recover-release-activation.yml @@ -179,7 +179,7 @@ jobs: echo "[OK] GitHub's stored RC packet still matches the immutable source-run manifest." - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 with: version: v3.15.2 @@ -196,16 +196,16 @@ jobs: "${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GHCR - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index b40377503..9cfd24ec1 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -44,13 +44,13 @@ jobs: - name: Set up Helm if: ${{ github.event.schedule != '17 */6 * * *' }} - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 with: version: v3.15.2 - name: Set up Docker Buildx if: ${{ github.event.schedule != '17 */6 * * *' }} - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Resolve the advertised stable release id: release diff --git a/.github/workflows/signpath-test-signing.yml b/.github/workflows/signpath-test-signing.yml index 2f57ebfbc..054815e6e 100644 --- a/.github/workflows/signpath-test-signing.yml +++ b/.github/workflows/signpath-test-signing.yml @@ -104,7 +104,7 @@ jobs: - name: Submit SignPath test-signing request id: signpath - uses: signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2 + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} diff --git a/.github/workflows/update-demo-server.yml b/.github/workflows/update-demo-server.yml index f76e4230a..663be34f7 100644 --- a/.github/workflows/update-demo-server.yml +++ b/.github/workflows/update-demo-server.yml @@ -363,7 +363,7 @@ jobs: - name: Tailscale id: tailscale - uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4 + uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 5130c6efd..4f31bda5a 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -5407,3 +5407,22 @@ Retained provider delivery verification establishes archive SHA256 a valid pinned SSH signature and the extracted request expression with absent and synthetic email. It does not establish installed onboarding, server acceptance or legal identity. See the qualification evidence below. + +### Pinned release action consumer compatibility + +The grouped release actions use immutable revisions recorded in +`scripts/release_control/action_consumer_manifests.json`: Attest 4.2.2, +Helm setup 5.0.1, Docker build/push 7.3.0, login 4.6.0, Buildx/QEMU +setup 4.3.0, SignPath v2 and Tailscale 4.1.3. The snapshot records the +upstream action manifest hash, accepted inputs/outputs and Node 24 entry points. +It is reviewed input, not runtime attestation or permission to publish. + +`test_reviewed_action_manifests_cover_all_release_consumers` checks every +workflow consumer against those upstream inputs and immutable revisions. +Helm remains 3.15.2; attestation keeps checksum subjects; signing keeps the +existing organisation/token interface. Existing signing, checksum, Docker +promotion and exact-dispatch contract tests remain required. Action upgrades +must not alter permissions, environments, source identity, rollback, signing +backend selection or release qualification. Local contract success does not +establish hosted action execution, signature acceptance, image publication or +production deployment. diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 8ca59fcef..972b5aaba 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -157,7 +157,7 @@ func TestSecurityScanRevalidatesLatestStableDelivery(t *testing.T) { `github.event_name != 'workflow_run' || !contains(github.event.workflow_run.display_title, '-')`, "release-continuity:", "Latest stable release continuity", - "docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5", + "docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e", `"repos/${REPOSITORY}/releases/latest"`, `scripts/release_control/release_continuity.py release`, "release-diagnostic.json", @@ -1747,7 +1747,7 @@ func TestReleaseCandidateRequiresPlatformNativeAgentSigning(t *testing.T) { `sign-windows-agent:`, `collect-windows-signing:`, `windows_signing_backend:`, - `signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2`, + `signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2`, `github-artifact-id: ${{ steps.upload-unsigned-windows.outputs.artifact-id }}`, `wait-for-completion: false`, `windows-signing-request.json`, @@ -1852,7 +1852,7 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) { `does not trust the configured release signing key.`, `id-token: write`, `attestations: write`, - `uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`, + `uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2`, } containerJob := workflowJobBlock(t, string(qualifierWorkflowBytes), "qualify") for _, needle := range []string{ @@ -2272,7 +2272,7 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) { workflow := string(workflowBytes) + "\n" + string(profileBytes) required := []string{ `- name: Tailscale`, - `uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4`, + `uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3`, `oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}`, `oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}`, `tags: tag:infra`, @@ -3189,7 +3189,7 @@ func TestPublishHelmChartReachableViaWorkflowCall(t *testing.T) { `name: Verify public GHCR chart identity and provenance`, `helm registry logout ghcr.io || true`, `name: Authenticate OCI attestation client with GHCR`, - `uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0`, + `uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0`, `registry: ghcr.io`, `username: ${{ github.actor }}`, `password: ${{ github.token }}`, diff --git a/scripts/release_control/action_consumer_manifests.json b/scripts/release_control/action_consumer_manifests.json new file mode 100644 index 000000000..114486075 --- /dev/null +++ b/scripts/release_control/action_consumer_manifests.json @@ -0,0 +1,225 @@ +{ + "actions/attest": { + "sha": "1e69f48acb82d1966a394da916b4c1698aa569d6", + "manifest_sha256": "9e4a1b808433f9ec87120b534e11fc35469a039bdbdc62b019441444c9ad0449", + "inputs": [ + "create-storage-record", + "github-token", + "predicate", + "predicate-path", + "predicate-type", + "push-to-registry", + "sbom-path", + "show-summary", + "subject-checksums", + "subject-digest", + "subject-name", + "subject-path", + "subject-version" + ], + "outputs": [ + "attestation-id", + "attestation-url", + "bundle-path", + "storage-record-ids" + ], + "runs": { + "using": "node24", + "main": "./dist/index.js" + } + }, + "azure/setup-helm": { + "sha": "9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310", + "manifest_sha256": "a80a29bbe87c77663a47a75d4163cf54f0bd24ae2485b868dd81d6758ec29647", + "inputs": [ + "downloadBaseURL", + "token", + "version" + ], + "outputs": [ + "helm-path" + ], + "runs": { + "using": "node24", + "main": "lib/index.js" + } + }, + "docker/build-push-action": { + "sha": "53b7df96c91f9c12dcc8a07bcb9ccacbed38856a", + "manifest_sha256": "5c1ade75252eabe541b96cc39a2165a07c8850f9bcd3f9b75f5394fbdfb215fd", + "inputs": [ + "add-hosts", + "allow", + "annotations", + "attests", + "build-args", + "build-contexts", + "builder", + "cache-from", + "cache-to", + "call", + "cgroup-parent", + "context", + "file", + "github-token", + "labels", + "load", + "network", + "no-cache", + "no-cache-filters", + "outputs", + "platforms", + "provenance", + "pull", + "push", + "sbom", + "secret-envs", + "secret-files", + "secrets", + "shm-size", + "ssh", + "tags", + "target", + "ulimit" + ], + "outputs": [ + "digest", + "imageid", + "metadata" + ], + "runs": { + "using": "node24", + "main": "dist/index.cjs", + "post": "dist/index.cjs" + } + }, + "docker/login-action": { + "sha": "dbcb813823bdd20940b903addbd779551569679f", + "manifest_sha256": "e65e6bdc57fc10adcee036a393229aae6b9a9f451b69528f4ff50d4b0e93f156", + "inputs": [ + "ecr", + "logout", + "password", + "registry", + "registry-auth", + "scope", + "username" + ], + "outputs": [], + "runs": { + "using": "node24", + "main": "dist/index.cjs", + "post": "dist/index.cjs" + } + }, + "docker/setup-buildx-action": { + "sha": "37fe631027851001ddb9b187196cc803df7f5f0e", + "manifest_sha256": "9c55229011a0566758962f36c74309c8ab06f8905a5997591a6a46319401ab38", + "inputs": [ + "append", + "buildkitd-config", + "buildkitd-config-inline", + "buildkitd-flags", + "cache-binary", + "cleanup", + "driver", + "driver-opts", + "endpoint", + "keep-state", + "name", + "platforms", + "use", + "version" + ], + "outputs": [ + "driver", + "endpoint", + "flags", + "name", + "nodes", + "platforms", + "status" + ], + "runs": { + "using": "node24", + "main": "dist/index.cjs", + "post": "dist/index.cjs" + } + }, + "docker/setup-qemu-action": { + "sha": "1f40c72289eff860ee54a304f1438e3cff362e0a", + "manifest_sha256": "9c74c218c37b889976b0af59861f3fceaec87bd12da00bf6dc37ded77d8954f8", + "inputs": [ + "cache-image", + "image", + "platforms", + "reset" + ], + "outputs": [ + "platforms" + ], + "runs": { + "using": "node24", + "main": "dist/index.cjs", + "post": "dist/index.cjs" + } + }, + "signpath/github-action-submit-signing-request": { + "sha": "c92b958760219087e01f8d67a1669ed57afe2627", + "manifest_sha256": "d0b52fdfa234d87c1ff4d21c06a91d2b6e0dd45e1a8866ad3b58cbdeb9aef326", + "inputs": [ + "api-token", + "artifact-configuration-slug", + "connector-url", + "download-signed-artifact-timeout-in-seconds", + "github-artifact-id", + "github-token", + "organization-id", + "output-artifact-directory", + "parameters", + "project-slug", + "service-unavailable-timeout-in-seconds", + "signing-policy-slug", + "skip-decompress", + "wait-for-completion", + "wait-for-completion-timeout-in-seconds" + ], + "outputs": [ + "signed-artifact-download-url", + "signing-request-id", + "signing-request-web-url", + "signpath-api-url" + ], + "runs": { + "using": "node24", + "main": "index.js" + } + }, + "tailscale/github-action": { + "sha": "780049a30b6ff5c378a9e7b389d15ece7a204888", + "manifest_sha256": "d63e612f47dad1520586efec4b79f2c7f0e0aadabeeca53709aa616202e43c6c", + "inputs": [ + "args", + "audience", + "authkey", + "hostname", + "oauth-client-id", + "oauth-secret", + "ping", + "retry", + "sha256sum", + "statedir", + "tags", + "tailscaled-args", + "timeout", + "use-cache", + "version" + ], + "outputs": [], + "runs": { + "using": "node24", + "main": "dist/index.js", + "post": "dist/logout/index.js" + } + } +} diff --git a/scripts/release_control/release_promotion_policy_support_test.py b/scripts/release_control/release_promotion_policy_support_test.py index ad964b878..19bcc9d5b 100644 --- a/scripts/release_control/release_promotion_policy_support_test.py +++ b/scripts/release_control/release_promotion_policy_support_test.py @@ -10,6 +10,37 @@ from release_promotion_policy_support import ( class ReleasePromotionPolicySupportTest(unittest.TestCase): + def test_action_inputs_do_not_satisfy_dispatch_requirements(self) -> None: + # Signing action inputs are nested consumer data, not callable workflow + # inputs. A pin upgrade must not make this dispatch guard accept them. + content = """ +on: + workflow_dispatch: + inputs: + version: + type: string +jobs: + sign: + runs-on: ubuntu-24.04 + steps: + - uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 + with: + organization-id: fixture + api-token: fixture + expected_head_sha: fixture +""" + self.assertEqual(parse_workflow_dispatch_inputs(content), ("version",)) + with patch("release_promotion_policy_support.branch_workflow_text", return_value=content), patch( + "release_promotion_policy_support.origin_default_branch", return_value="main" + ): + self.assertEqual( + missing_workflow_dispatch_inputs( + workflow_path=".github/workflows/signpath-test-signing.yml", + required_inputs=("version", "expected_head_sha"), + ), + ("main", ("expected_head_sha",)), + ) + def test_parse_workflow_dispatch_inputs_reads_top_level_inputs(self) -> None: content = """ name: Example diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 192c69d33..f0f7a9d89 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -302,6 +302,40 @@ STAGED_GOVERNANCE_INPUT_ERRORS = ( class ReleasePromotionPolicyTest(unittest.TestCase): + + def test_reviewed_action_manifests_cover_all_release_consumers(self) -> None: + # Snapshot is derived from each immutable upstream action.yml, not from + # our consumers: unknown inputs therefore fail rather than being blessed. + manifests = read_json("scripts/release_control/action_consumer_manifests.json") + seen = set() + for path in (REPO_ROOT / ".github/workflows").glob("*.yml"): + workflow = yaml.safe_load(read(str(path.relative_to(REPO_ROOT)))) + for job in workflow.get("jobs", {}).values(): + for step in job.get("steps", []): + action, _, revision = step.get("uses", "").partition("@") + if action not in manifests: + continue + with self.subTest(workflow=path.name, action=action): + manifest = manifests[action] + self.assertEqual(revision, manifest["sha"]) + self.assertEqual(manifest["runs"]["using"], "node24") + self.assertRegex(manifest["manifest_sha256"], r"^[0-9a-f]{64}$") + self.assertLessEqual(set(step.get("with", {})), set(manifest["inputs"])) + seen.add(action) + if action == "azure/setup-helm": + self.assertEqual(str(step["with"]["version"]).lstrip("v"), "3.15.2") + if action == "actions/attest": + if path.name in {"publish-docker.yml", "publish-helm-chart.yml"}: + self.assertIn("subject-name", step["with"]) + self.assertIn("subject-digest", step["with"]) + self.assertTrue(step["with"]["push-to-registry"]) + else: + self.assertIn("subject-checksums", step["with"]) + if action == "signpath/github-action-submit-signing-request": + self.assertIn("api-token", step["with"]) + self.assertIn("organization-id", step["with"]) + self.assertEqual(seen, set(manifests)) + def setUp(self) -> None: if USE_STAGED_GOVERNANCE and not REQUIRES_STAGED_GOVERNANCE_INPUTS: self.skipTest("staged slice does not touch the promotion-proof surface") @@ -1546,7 +1580,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): workflow, ) self.assertIn( - "signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2", + "signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2", workflow, ) self.assertIn("signedArtifactsPublished = $false", workflow) @@ -1924,7 +1958,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("windows_signing_backend: signpath", content) self.assertIn('if [[ "$REQUIRE_WINDOWS_SIGNING" == "true" ]]', candidate_workflow) self.assertIn("inputs.require_windows_signing", candidate_workflow) - self.assertIn("signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2", candidate_workflow) + self.assertIn("signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2", candidate_workflow) self.assertIn("github-artifact-id: ${{ steps.upload-unsigned-windows.outputs.artifact-id }}", candidate_workflow) self.assertIn("windows-signing-evidence.json", candidate_workflow) for signpath_setting in ( @@ -1959,7 +1993,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("bash .github/scripts/setup-demo-ssh.sh", update_demo_workflow) self.assertIn("bash .github/scripts/check-demo-reachability.sh", update_demo_workflow) self.assertIn("ping: ${{ secrets.DEMO_SERVER_HOST }}", update_demo_workflow) - self.assertIn("tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4", update_demo_workflow) + self.assertIn("tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3", update_demo_workflow) self.assertIn("uses: ./.github/workflows/update-demo-server.yml", deploy_demo_workflow) self.assertIn("verify_only: true", deploy_demo_workflow) self.assertIn('MAX_SSH_SETUP_ATTEMPTS="${DEMO_SSH_SETUP_ATTEMPTS:-3}"', demo_ssh_helper) @@ -2003,7 +2037,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("id-token: write", candidate_workflow) self.assertIn("attestations: write", candidate_workflow) self.assertIn( - "uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4", + "uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2", candidate_workflow, ) self.assertIn( @@ -2318,7 +2352,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("fail-fast: false", publish) self.assertIn("if: matrix.image == 'server'", publish) self.assertIn("if: matrix.image == 'control-plane'", publish) - self.assertIn("uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4", publish) + self.assertIn("uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2", publish) self.assertIn("subject-name: docker.io/rcourtman/pulse", publish) self.assertIn("subject-name: ghcr.io/${{ github.repository_owner }}/pulse", publish) # pulse-agent ships as release-asset binaries, not as a Docker image @@ -2362,7 +2396,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("tag: latest", dry_run_workflow) self.assertIn("verify_only: true", dry_run_workflow) self.assertIn("Verify Current Stable Demo Path (No Mutation)", dry_run_workflow) - self.assertIn("tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4", demo) + self.assertIn("tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3", demo) self.assertIn("oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}", demo) self.assertIn("oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}", demo) self.assertIn("ping: ${{ secrets.DEMO_SERVER_HOST }}", demo)