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
This commit is contained in:
pulse-triage[bot]
2026-09-10 00:35:47 +01:00
parent a61f1c7c82
commit ad1cfd33c3
18 changed files with 354 additions and 45 deletions
@@ -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
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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 }}
+13 -13
View File
@@ -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 }}
+3 -3
View File
@@ -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 }}
@@ -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
+1 -1
View File
@@ -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 }}
@@ -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 }}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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 }}
@@ -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.
@@ -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 }}`,
@@ -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"
}
}
}
@@ -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
@@ -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)