Isolate PC compilation from SignPath workflow

This commit is contained in:
rcourtman
2026-08-23 15:02:33 +01:00
parent 32d7b22996
commit 6869612c66
8 changed files with 273 additions and 113 deletions
+107 -80
View File
@@ -74,79 +74,103 @@ permissions:
contents: read
jobs:
compile-release-payload:
name: Compile Exact-SHA Release Payload
# This credential-free worker is the trusted high-capacity compiler for
# every channel. Its output is immutable after upload and is consumed by
# artifact id and digest; hosted jobs verify and package it without a
# second compilation. It is not in the dependency chain leading to the
# separately hosted SignPath request.
runs-on: ${{ fromJSON('["self-hosted","Linux","X64","pulse-pve-compile"]') }}
timeout-minutes: 30
obtain-release-payload:
name: Obtain Isolated PC Release Payload
# The self-hosted compiler runs in a separate workflow run. This release
# and SignPath workflow therefore contains only GitHub-hosted jobs, while
# the expensive payload still compiles once on the trusted PC.
runs-on: ubuntu-24.04
timeout-minutes: 40
permissions:
actions: write
contents: read
outputs:
artifact_id: ${{ steps.upload_compiled.outputs.artifact-id }}
artifact_digest: ${{ steps.upload_compiled.outputs.artifact-digest }}
artifact_name: release-compiled-${{ github.sha }}-${{ inputs.version }}
artifact_id: ${{ steps.wait.outputs.artifact_id }}
artifact_digest: ${{ steps.wait.outputs.artifact_digest }}
artifact_name: ${{ steps.wait.outputs.artifact_name }}
compiler_run_id: ${{ steps.dispatch.outputs.compiler_run_id }}
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Validate candidate identity
run: |
set -euo pipefail
test "$(tr -d '\n' < VERSION)" = "${{ inputs.version }}"
test "$(git rev-parse HEAD)" = "${GITHUB_SHA}"
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
cache: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
- name: Verify compiler capacity
shell: bash
run: |
set -euo pipefail
available_memory_kib="$(awk '$1 == "MemAvailable:" {print $2}' /proc/meminfo)"
minimum_memory_kib="$((6 * 1024 * 1024))"
available_disk_bytes="$(df --output=avail -B1 "$RUNNER_TEMP" | tail -n 1 | tr -d ' ')"
minimum_disk_bytes="$((10 * 1024 * 1024 * 1024))"
if (( available_memory_kib < minimum_memory_kib )); then
echo "::error::Release compiler has less than 6 GiB available memory."
exit 1
fi
if (( available_disk_bytes < minimum_disk_bytes )); then
echo "::error::Release compiler has less than 10 GiB free under RUNNER_TEMP."
exit 1
fi
echo "Release compiler capacity: $((available_memory_kib / 1024)) MiB memory, $((available_disk_bytes / 1024 / 1024 / 1024)) GiB disk."
- name: Compile credential-free release payload
- name: Dispatch exact-SHA compiler workflow
id: dispatch
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
# Two bounded workers avoid the compiler-process loss seen when four
# Go links competed with the frontend build on the same VM.
PULSE_RELEASE_BUILD_JOBS: "2"
run: ./scripts/build-release-binaries.sh "${{ inputs.version }}" "$RUNNER_TEMP/release-compiled"
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
SOURCE_SHA: ${{ github.sha }}
SOURCE_REF: ${{ github.ref_name }}
REQUEST_ID: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
set -euo pipefail
[[ "${GITHUB_REF}" == refs/heads/* ]]
dispatch_payload="$(jq -nc \
--arg ref "${SOURCE_REF}" \
--arg version "${VERSION}" \
--arg source_sha "${SOURCE_SHA}" \
--arg request_id "${REQUEST_ID}" \
'{ref: $ref, inputs: {version: $version, source_sha: $source_sha, request_id: $request_id}}')"
dispatch_json="$(gh api \
--method POST \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2026-03-10' \
"repos/${GITHUB_REPOSITORY}/actions/workflows/compile-release-payload.yml/dispatches" \
--input - <<<"${dispatch_payload}")"
compiler_run_id="$(jq -er '.workflow_run_id | select(type == "number")' <<<"${dispatch_json}")"
compiler_run_url="$(jq -er '.html_url | select(type == "string" and length > 0)' <<<"${dispatch_json}")"
echo "compiler_run_id=${compiler_run_id}" >> "$GITHUB_OUTPUT"
echo "Compiler workflow: ${compiler_run_url}"
- name: Upload exact-SHA compiled payload
id: upload_compiled
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-compiled-${{ github.sha }}-${{ inputs.version }}
path: ${{ runner.temp }}/release-compiled/
if-no-files-found: error
retention-days: 1
compression-level: 0
overwrite: true
- name: Wait for immutable compiler artifact
id: wait
env:
GH_TOKEN: ${{ github.token }}
COMPILER_RUN_ID: ${{ steps.dispatch.outputs.compiler_run_id }}
VERSION: ${{ inputs.version }}
SOURCE_SHA: ${{ github.sha }}
SOURCE_REF: ${{ github.ref_name }}
REQUEST_ID: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
set -euo pipefail
deadline="$((SECONDS + 35 * 60))"
while true; do
run_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2026-03-10' \
"repos/${GITHUB_REPOSITORY}/actions/runs/${COMPILER_RUN_ID}")"
status="$(jq -er '.status' <<<"${run_json}")"
if [[ "${status}" == "completed" ]]; then
break
fi
if (( SECONDS >= deadline )); then
echo "::error::Compiler workflow ${COMPILER_RUN_ID} did not complete within 35 minutes."
exit 1
fi
echo "Compiler workflow ${COMPILER_RUN_ID} is ${status}."
sleep 10
done
if ! jq -e \
--argjson run_id "${COMPILER_RUN_ID}" \
--arg source_ref "${SOURCE_REF}" \
--arg source_sha "${SOURCE_SHA}" \
'.id == $run_id and .event == "workflow_dispatch" and .path == ".github/workflows/compile-release-payload.yml" and .head_branch == $source_ref and .head_sha == $source_sha and .conclusion == "success"' \
<<<"${run_json}" >/dev/null; then
run_summary="$(jq -c '{id, event, path, head_branch, head_sha, status, conclusion, html_url}' <<<"${run_json}")"
echo "::error::Compiler workflow identity or result verification failed: ${run_summary}"
exit 1
fi
artifact_name="release-compiled-${SOURCE_SHA}-${VERSION}-${REQUEST_ID}"
artifacts_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2026-03-10' \
"repos/${GITHUB_REPOSITORY}/actions/runs/${COMPILER_RUN_ID}/artifacts?per_page=100")"
artifact_json="$(jq -ce \
--arg artifact_name "${artifact_name}" \
'[.artifacts[] | select(.name == $artifact_name and .expired == false and .size_in_bytes > 0)] | if length == 1 then .[0] else error("expected exactly one compiler artifact") end' \
<<<"${artifacts_json}")"
artifact_id="$(jq -er '.id | select(type == "number")' <<<"${artifact_json}")"
artifact_digest="$(jq -er '.digest | select(test("^sha256:[0-9a-f]{64}$")) | sub("^sha256:"; "")' <<<"${artifact_json}")"
echo "artifact_id=${artifact_id}" >> "$GITHUB_OUTPUT"
echo "artifact_digest=${artifact_digest}" >> "$GITHUB_OUTPUT"
echo "artifact_name=${artifact_name}" >> "$GITHUB_OUTPUT"
signing-configuration:
name: Verify Native Signing Configuration
@@ -597,8 +621,8 @@ jobs:
build:
name: Build and Validate Release Candidate
needs: [compile-release-payload, sign-macos-agent, collect-windows-signing]
if: ${{ always() && needs.compile-release-payload.result == 'success' && (!inputs.require_macos_signing || needs.sign-macos-agent.result == 'success') && (!inputs.require_windows_signing || needs.collect-windows-signing.result == 'success') }}
needs: [obtain-release-payload, sign-macos-agent, collect-windows-signing]
if: ${{ always() && needs.obtain-release-payload.result == 'success' && (!inputs.require_macos_signing || needs.sign-macos-agent.result == 'success') && (!inputs.require_windows_signing || needs.collect-windows-signing.result == 'success') }}
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
@@ -668,9 +692,10 @@ jobs:
- name: Verify and download exact compiled artifact
env:
GH_TOKEN: ${{ github.token }}
EXPECTED_ARTIFACT_ID: ${{ needs.compile-release-payload.outputs.artifact_id }}
EXPECTED_ARTIFACT_DIGEST: ${{ needs.compile-release-payload.outputs.artifact_digest }}
EXPECTED_ARTIFACT_NAME: ${{ needs.compile-release-payload.outputs.artifact_name }}
EXPECTED_ARTIFACT_ID: ${{ needs.obtain-release-payload.outputs.artifact_id }}
EXPECTED_ARTIFACT_DIGEST: ${{ needs.obtain-release-payload.outputs.artifact_digest }}
EXPECTED_ARTIFACT_NAME: ${{ needs.obtain-release-payload.outputs.artifact_name }}
EXPECTED_COMPILER_RUN_ID: ${{ needs.obtain-release-payload.outputs.compiler_run_id }}
run: |
set -euo pipefail
[[ "${EXPECTED_ARTIFACT_ID}" =~ ^[0-9]+$ ]]
@@ -682,7 +707,7 @@ jobs:
--argjson artifact_id "${EXPECTED_ARTIFACT_ID}" \
--arg artifact_name "${EXPECTED_ARTIFACT_NAME}" \
--arg artifact_digest "sha256:${EXPECTED_ARTIFACT_DIGEST}" \
--argjson run_id "${GITHUB_RUN_ID}" \
--argjson run_id "${EXPECTED_COMPILER_RUN_ID}" \
--arg source_sha "${GITHUB_SHA}" \
'.id == $artifact_id and .name == $artifact_name and .expired == false and .size_in_bytes > 0 and .digest == $artifact_digest and .workflow_run.id == $run_id and .workflow_run.head_sha == $source_sha' \
"${artifact_json}" >/dev/null
@@ -693,9 +718,10 @@ jobs:
- name: Verify exact-SHA compiled payload
env:
EXPECTED_ARTIFACT_ID: ${{ needs.compile-release-payload.outputs.artifact_id }}
EXPECTED_ARTIFACT_DIGEST: ${{ needs.compile-release-payload.outputs.artifact_digest }}
EXPECTED_ARTIFACT_NAME: ${{ needs.compile-release-payload.outputs.artifact_name }}
EXPECTED_ARTIFACT_ID: ${{ needs.obtain-release-payload.outputs.artifact_id }}
EXPECTED_ARTIFACT_DIGEST: ${{ needs.obtain-release-payload.outputs.artifact_digest }}
EXPECTED_ARTIFACT_NAME: ${{ needs.obtain-release-payload.outputs.artifact_name }}
EXPECTED_COMPILER_RUN_ID: ${{ needs.obtain-release-payload.outputs.compiler_run_id }}
run: |
set -euo pipefail
python3 scripts/release_candidate_manifest.py verify-local \
@@ -710,10 +736,11 @@ jobs:
--arg artifact_name "${EXPECTED_ARTIFACT_NAME}" \
--arg artifact_sha256 "${EXPECTED_ARTIFACT_DIGEST}" \
--arg payload_manifest_sha256 "${payload_manifest_sha256}" \
--argjson workflow_run_id "${GITHUB_RUN_ID}" \
--argjson compiler_workflow_run_id "${EXPECTED_COMPILER_RUN_ID}" \
--argjson release_workflow_run_id "${GITHUB_RUN_ID}" \
--arg version "${{ inputs.version }}" \
--arg source_sha "${GITHUB_SHA}" \
'{schema_version: 1, trust_boundary: "trusted-self-hosted-compiler", verified_on: "github-hosted", artifact_id: $artifact_id, artifact_name: $artifact_name, artifact_sha256: $artifact_sha256, payload_manifest_sha256: $payload_manifest_sha256, workflow_run_id: $workflow_run_id, version: $version, source_sha: $source_sha}' \
'{schema_version: 2, trust_boundary: "separate-trusted-self-hosted-compiler-workflow", verified_on: "github-hosted", artifact_id: $artifact_id, artifact_name: $artifact_name, artifact_sha256: $artifact_sha256, payload_manifest_sha256: $payload_manifest_sha256, compiler_workflow_run_id: $compiler_workflow_run_id, release_workflow_run_id: $release_workflow_run_id, version: $version, source_sha: $source_sha}' \
> release-candidate-manifest/compiled-payload-verification.json
- name: Download signed macOS binaries
@@ -0,0 +1,96 @@
name: Compile Release Payload on Trusted PC
run-name: Compile v${{ inputs.version }} payload for request ${{ inputs.request_id }}
on:
workflow_dispatch:
inputs:
version:
description: 'Version number without the leading v'
required: true
type: string
source_sha:
description: 'Exact public Pulse source commit to compile'
required: true
type: string
request_id:
description: 'Parent release workflow run and attempt identifier'
required: true
type: string
permissions:
contents: read
jobs:
compile-release-payload:
name: Compile Exact-SHA Release Payload
runs-on: ${{ fromJSON('["self-hosted","Linux","X64","pulse-pve-compile"]') }}
timeout-minutes: 30
steps:
- name: Validate isolated compiler request
env:
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}
REQUEST_ID: ${{ inputs.request_id }}
run: |
set -euo pipefail
[[ "${EXPECTED_SOURCE_SHA}" =~ ^[0-9a-f]{40}$ ]]
[[ "${REQUEST_ID}" =~ ^[0-9]+-[0-9]+$ ]]
test "${GITHUB_SHA}" = "${EXPECTED_SOURCE_SHA}"
test "${GITHUB_WORKFLOW_SHA}" = "${EXPECTED_SOURCE_SHA}"
- name: Checkout exact release source
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
ref: ${{ inputs.source_sha }}
- name: Validate candidate identity
run: |
set -euo pipefail
test "$(tr -d '\n' < VERSION)" = "${{ inputs.version }}"
test "$(git rev-parse HEAD)" = "${{ inputs.source_sha }}"
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
cache: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
- name: Verify compiler capacity
shell: bash
run: |
set -euo pipefail
available_memory_kib="$(awk '$1 == "MemAvailable:" {print $2}' /proc/meminfo)"
minimum_memory_kib="$((6 * 1024 * 1024))"
available_disk_bytes="$(df --output=avail -B1 "$RUNNER_TEMP" | tail -n 1 | tr -d ' ')"
minimum_disk_bytes="$((10 * 1024 * 1024 * 1024))"
if (( available_memory_kib < minimum_memory_kib )); then
echo "::error::Release compiler has less than 6 GiB available memory."
exit 1
fi
if (( available_disk_bytes < minimum_disk_bytes )); then
echo "::error::Release compiler has less than 10 GiB free under RUNNER_TEMP."
exit 1
fi
echo "Release compiler capacity: $((available_memory_kib / 1024)) MiB memory, $((available_disk_bytes / 1024 / 1024 / 1024)) GiB disk."
- name: Compile credential-free release payload
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
PULSE_RELEASE_BUILD_JOBS: "2"
run: ./scripts/build-release-binaries.sh "${{ inputs.version }}" "$RUNNER_TEMP/release-compiled"
- name: Upload exact-SHA compiled payload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-compiled-${{ inputs.source_sha }}-${{ inputs.version }}-${{ inputs.request_id }}
path: ${{ runner.temp }}/release-compiled/
if-no-files-found: error
retention-days: 1
compression-level: 0
overwrite: true
+1 -1
View File
@@ -234,7 +234,7 @@ jobs:
needs: prepare
if: ${{ needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
permissions:
actions: read
actions: write
contents: read
uses: ./.github/workflows/build-release-candidate.yml
secrets: inherit
+1 -1
View File
@@ -72,7 +72,7 @@ jobs:
name: Build Immutable Release Candidate
if: ${{ inputs.version != '' }}
permissions:
actions: read
actions: write
contents: read
uses: ./.github/workflows/build-release-candidate.yml
secrets: inherit
+1 -1
View File
@@ -10638,7 +10638,7 @@
},
{
"id": "trusted-pc-single-build-artifact-handoff",
"summary": "The project owner designated the dedicated PVE PC runners as trusted, credential-free compilers for public and private release payloads across RC, stable, and patch channels. Each payload is built once, uploaded as an immutable GitHub Actions artifact, and consumed without recompilation only after a GitHub-hosted job verifies the exact artifact id, server-recorded archive SHA-256, workflow run, head SHA, version/source inner manifest, and complete file digests. Signing and publication credentials remain hosted-only, and the SignPath request dependency chain remains entirely GitHub-hosted.",
"summary": "The project owner designated the dedicated PVE PC runners as trusted, credential-free compilers for public and private release payloads across RC, stable, and patch channels. Each payload is built once in a separately dispatched workflow run, uploaded as an immutable GitHub Actions artifact, and consumed without recompilation only after a GitHub-hosted job verifies the exact artifact id, server-recorded archive SHA-256, isolated compiler workflow run, head SHA, version/source inner manifest, and complete file digests. Signing and publication credentials remain hosted-only, and the complete SignPath workflow run contains only GitHub-hosted jobs rather than relying on a sibling-job interpretation of the Foundation rule.",
"kind": "release-policy",
"decided_at": "2026-08-23",
"subsystem_ids": [
@@ -539,13 +539,19 @@ upgrade, update, release, or artifact-selection behavior.
backend and browser-smoke lanes can start as soon as the bundle is available.
Cross-platform compilation for every release channel runs once on the
dedicated, credential-free PVE compiler identity using only public embedding
keys. The compiler must produce one exact-version, exact-source-SHA manifest
keys in a separately dispatched GitHub Actions workflow run. The release
candidate and SignPath workflow run must contain only GitHub-hosted jobs;
its hosted handoff job may dispatch and wait for the isolated compiler run,
but the self-hosted job must never appear in the signing run. The compiler
workflow must execute from the same exact workflow SHA as the parent release
and must produce one exact-version, exact-source-SHA manifest
covering the complete frontend and binary payload. That manifest may cover
canonical relative paths in the payload tree but must reject absolute,
traversing, or noncanonical names. The upload step must expose GitHub's
immutable artifact id and archive SHA-256 digest. The hosted candidate job
must retrieve that exact id, fail closed unless GitHub's artifact API binds
its name, digest, workflow-run id, and head SHA to the current release run,
its name, digest, isolated compiler workflow-run id, and head SHA to the
exact release source,
verify the downloaded archive digest itself, and then verify the inner
payload manifest
before applying required native binaries, packaging, update-signing, SBOM
@@ -1640,17 +1646,22 @@ decision is limited to `v6.1.2`, retains the exact-SHA and integrity controls,
and requires the public release notes to disclose that Windows binaries are
not Authenticode-signed and may show Unknown Publisher.
Every caller of the reusable release-candidate builder must delegate
`actions: read` alongside `contents: read`; the Windows signing job reads the
exact uploaded artifact through the GitHub Actions API, and GitHub validates
that nested permission even when a prerelease skips Authenticode signing.
`actions: write` alongside `contents: read`; only the hosted compiler-handoff
job receives that effective permission so it can dispatch the isolated PC
workflow, while the Windows signing and final assembly jobs narrow themselves
back to `actions: read`. GitHub validates that nested permission even when a
prerelease skips Authenticode signing.
SignPath Foundation also requires every job leading up to an open-source
signing request to execute on GitHub-hosted runners. Stable release
preparation, the parallel frontend bundle, backend qualification, signing
configuration, and the Windows build/submission dependency chain therefore
remain GitHub-hosted regardless of an unsigned-Windows exception. The
credential-free PVE compiler is an independent sibling and never supplies the
artifact submitted to SignPath; only the hosted final assembler joins its
separately verified output after native signing. Rehearsals `32631653966` and
credential-free PVE compiler executes in an entirely separate workflow run and
never supplies the artifact submitted to SignPath. The hosted release workflow
dispatches it by exact source SHA, validates the returned compiler-run identity,
and only the hosted final assembler joins its separately verified output after
native signing. This keeps every job in the SignPath workflow run hosted rather
than relying on a sibling-job interpretation of SignPath's OSS rule. Rehearsals `32631653966` and
`32635525554` lost different matrix compiler processes on the PVE runner under
four-way compiler and frontend pressure. The single-build compiler now admits
only a worker with explicit memory/disk headroom, limits the matrix to two
@@ -2307,6 +2307,10 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
if err != nil {
t.Fatalf("read build-release-candidate.yml: %v", err)
}
compilerBytes, err := os.ReadFile(repoFile(".github", "workflows", "compile-release-payload.yml"))
if err != nil {
t.Fatalf("read compile-release-payload.yml: %v", err)
}
validationBytes, err := os.ReadFile(repoFile(".github", "workflows", "validate-release-assets.yml"))
if err != nil {
t.Fatalf("read validate-release-assets.yml: %v", err)
@@ -2326,6 +2330,7 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
createWorkflow := string(createBytes)
candidateWorkflow := string(candidateBytes)
compilerWorkflow := string(compilerBytes)
compileScriptBytes, err := os.ReadFile(repoFile("scripts", "build-release-binaries.sh"))
if err != nil {
t.Fatalf("read build-release-binaries.sh: %v", err)
@@ -2350,7 +2355,8 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
floatingJob := workflowJobBlock(t, convergenceWorkflow, "promote_floating_tags")
helmPagesJob := workflowJobBlock(t, convergenceWorkflow, "publish_helm_pages")
demoJob := workflowJobBlock(t, convergenceWorkflow, "update_stable_demo")
compileJob := workflowJobBlock(t, candidateWorkflow, "compile-release-payload")
compileJob := workflowJobBlock(t, compilerWorkflow, "compile-release-payload")
obtainPayloadJob := workflowJobBlock(t, candidateWorkflow, "obtain-release-payload")
candidateBuildJob := workflowJobBlock(t, candidateWorkflow, "build")
for _, needle := range []string{
@@ -2368,11 +2374,11 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
for _, needle := range []string{
`fromJSON('["self-hosted","Linux","X64","pulse-pve-compile"]')`,
`artifact_id: ${{ steps.upload_compiled.outputs.artifact-id }}`,
`artifact_digest: ${{ steps.upload_compiled.outputs.artifact-digest }}`,
`GITHUB_WORKFLOW_SHA`,
`ref: ${{ inputs.source_sha }}`,
`PULSE_RELEASE_BUILD_JOBS: "2"`,
`./scripts/build-release-binaries.sh "${{ inputs.version }}"`,
`release-compiled-${{ github.sha }}-${{ inputs.version }}`,
`release-compiled-${{ inputs.source_sha }}-${{ inputs.version }}-${{ inputs.request_id }}`,
} {
if !strings.Contains(compileJob, needle) {
t.Fatalf("compiled release payload job missing exact-SHA contract: %s", needle)
@@ -2381,6 +2387,21 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
if strings.Contains(compileJob, "PULSE_UPDATE_SIGNING_KEY") {
t.Fatal("release compilation job must not receive private update-signing material")
}
if strings.Contains(candidateWorkflow, `runs-on: ${{ fromJSON('["self-hosted"`) {
t.Fatal("SignPath release workflow must not contain a self-hosted runner job")
}
for _, needle := range []string{
`runs-on: ubuntu-24.04`,
`actions: write`,
`actions/workflows/compile-release-payload.yml/dispatches`,
`X-GitHub-Api-Version: 2026-03-10`,
`compiler_run_id: ${{ steps.dispatch.outputs.compiler_run_id }}`,
`.path == ".github/workflows/compile-release-payload.yml"`,
} {
if !strings.Contains(obtainPayloadJob, needle) {
t.Fatalf("hosted compiler handoff job missing isolated-workflow contract: %s", needle)
}
}
for label, job := range map[string]string{
"frontend bundle": frontendBundleJob,
"backend tests": backendJob,
@@ -2414,16 +2435,17 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
}
}
for _, needle := range []string{
`needs.compile-release-payload.result == 'success'`,
`needs.obtain-release-payload.result == 'success'`,
`actions: read`,
`EXPECTED_ARTIFACT_ID: ${{ needs.compile-release-payload.outputs.artifact_id }}`,
`EXPECTED_ARTIFACT_DIGEST: ${{ needs.compile-release-payload.outputs.artifact_digest }}`,
`EXPECTED_ARTIFACT_ID: ${{ needs.obtain-release-payload.outputs.artifact_id }}`,
`EXPECTED_ARTIFACT_DIGEST: ${{ needs.obtain-release-payload.outputs.artifact_digest }}`,
`EXPECTED_COMPILER_RUN_ID: ${{ needs.obtain-release-payload.outputs.compiler_run_id }}`,
`actions/artifacts/${EXPECTED_ARTIFACT_ID}`,
`.workflow_run.head_sha == $source_sha`,
`sha256sum --check --`,
`scripts/release_candidate_manifest.py verify-local`,
`compiled-payload-verification.json`,
`trusted-self-hosted-compiler`,
`separate-trusted-self-hosted-compiler-workflow`,
`PULSE_RELEASE_COMPILED_PAYLOAD_DIR`,
} {
if !strings.Contains(candidateBuildJob, needle) {
@@ -1270,12 +1270,12 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertRegex(
workflow,
r"(?ms)^ build_release_candidate:\n.*?^ permissions:\n"
r" actions: read\n contents: read\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
r" actions: write\n contents: read\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
)
self.assertRegex(
release_workflow,
r"(?ms)^ build_release_candidate:\n.*?^ permissions:\n"
r" actions: read\n contents: read\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
r" actions: write\n contents: read\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
)
self.assertIn("Definitive Dry-Run Verdict", workflow)
self.assertIn('require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success', workflow)
@@ -1327,6 +1327,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
demo_reachability_helper = read(".github/scripts/check-demo-reachability.sh")
validation_workflow = read(".github/workflows/validate-release-assets.yml")
candidate_workflow = read(".github/workflows/build-release-candidate.yml")
compiler_workflow = read(".github/workflows/compile-release-payload.yml")
qualifier_workflow = read(".github/workflows/qualify-release-containers.yml")
docker_build = workflow_job_block(qualifier_workflow, "qualify")
release_validator = read("scripts/validate-release.sh")
@@ -1456,31 +1457,34 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertNotIn("pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}", docker_build)
self.assertIn("Validate installer signing key pins", candidate_workflow)
self.assertIn("timeout-minutes: 60", candidate_workflow)
self.assertNotRegex(candidate_workflow, r"(?m)^\s+runs-on:.*self-hosted")
self.assertIn(
'runs-on: ${{ fromJSON(\'["self-hosted","Linux","X64","pulse-pve-compile"]\') }}',
compiler_workflow,
)
self.assertIn("GITHUB_WORKFLOW_SHA", compiler_workflow)
self.assertIn("ref: ${{ inputs.source_sha }}", compiler_workflow)
self.assertIn("PULSE_RELEASE_BUILD_JOBS: \"2\"", compiler_workflow)
self.assertIn("actions/workflows/compile-release-payload.yml/dispatches", candidate_workflow)
self.assertIn("X-GitHub-Api-Version: 2026-03-10", candidate_workflow)
self.assertIn("compiler_run_id: ${{ steps.dispatch.outputs.compiler_run_id }}", candidate_workflow)
self.assertIn('.path == ".github/workflows/compile-release-payload.yml"', candidate_workflow)
self.assertIn(
"EXPECTED_ARTIFACT_ID: ${{ needs.obtain-release-payload.outputs.artifact_id }}",
candidate_workflow,
)
self.assertIn(
"artifact_id: ${{ steps.upload_compiled.outputs.artifact-id }}",
"EXPECTED_ARTIFACT_DIGEST: ${{ needs.obtain-release-payload.outputs.artifact_digest }}",
candidate_workflow,
)
self.assertIn(
"artifact_digest: ${{ steps.upload_compiled.outputs.artifact-digest }}",
candidate_workflow,
)
self.assertIn("PULSE_RELEASE_BUILD_JOBS: \"2\"", candidate_workflow)
self.assertIn(
"EXPECTED_ARTIFACT_ID: ${{ needs.compile-release-payload.outputs.artifact_id }}",
candidate_workflow,
)
self.assertIn(
"EXPECTED_ARTIFACT_DIGEST: ${{ needs.compile-release-payload.outputs.artifact_digest }}",
"EXPECTED_COMPILER_RUN_ID: ${{ needs.obtain-release-payload.outputs.compiler_run_id }}",
candidate_workflow,
)
self.assertIn(".workflow_run.head_sha == $source_sha", candidate_workflow)
self.assertIn("sha256sum --check --", candidate_workflow)
self.assertIn("compiled-payload-verification.json", candidate_workflow)
self.assertIn("trusted-self-hosted-compiler", candidate_workflow)
self.assertIn("separate-trusted-self-hosted-compiler-workflow", candidate_workflow)
self.assertIn("Verify Native Signing Configuration", candidate_workflow)
self.assertEqual(candidate_workflow.count("needs: signing-configuration"), 2)
self.assertIn("require_windows_signing: ${{ needs.prepare.outputs.require_windows_signing == 'true' }}", content)