mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Preflight immutable release publication
This commit is contained in:
@@ -265,9 +265,45 @@ jobs:
|
||||
|
||||
echo "[OK] Promotion policy validated for ${TAG}"
|
||||
|
||||
# Repository release immutability is configuration outside this commit. Prove
|
||||
# that prerequisite on a GitHub-hosted runner before starting compilation,
|
||||
# signing, private staging, or draft assembly. Activation repeats the same
|
||||
# check immediately before publication so later setting drift still fails
|
||||
# closed. Inert draft-only and historical-backfill runs do not publish and
|
||||
# therefore do not require this repository setting.
|
||||
publication_trust_preflight:
|
||||
name: Publication Trust Preflight
|
||||
needs: prepare
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout release trust control
|
||||
if: ${{ github.event.inputs.draft_only != 'true' && needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Require immutable release publication capability
|
||||
if: ${{ github.event.inputs.draft_only != 'true' && needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${GH_TOKEN:-}" ]; then
|
||||
echo "::error::WORKFLOW_PAT with repository Administration (read) is required to prove release immutability."
|
||||
exit 1
|
||||
fi
|
||||
./scripts/check-github-release-immutability.sh "${GITHUB_REPOSITORY}"
|
||||
|
||||
- name: Confirm inert release mode
|
||||
if: ${{ github.event.inputs.draft_only == 'true' || needs.prepare.outputs.historical_asset_backfill_only == 'true' }}
|
||||
run: echo "Publication trust preflight is not required for an inert draft-only or historical-backfill run."
|
||||
|
||||
build_release_candidate:
|
||||
name: Build Immutable Release Candidate
|
||||
needs: prepare
|
||||
needs:
|
||||
- prepare
|
||||
- publication_trust_preflight
|
||||
if: ${{ needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
|
||||
permissions:
|
||||
actions: write
|
||||
@@ -297,7 +333,9 @@ jobs:
|
||||
# Build the embed bundle independently so backend and smoke lanes can start
|
||||
# without waiting for the full frontend quality suite.
|
||||
frontend_bundle:
|
||||
needs: prepare
|
||||
needs:
|
||||
- prepare
|
||||
- publication_trust_preflight
|
||||
if: ${{ needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
|
||||
runs-on: ${{ !contains(inputs.version, '-') && 'ubuntu-24.04' || fromJSON('["self-hosted","Linux","X64","pulse-pve-build"]') }}
|
||||
timeout-minutes: 10
|
||||
@@ -330,7 +368,9 @@ jobs:
|
||||
|
||||
# Frontend checks run independently from the bundle and backend lanes.
|
||||
frontend_checks:
|
||||
needs: prepare
|
||||
needs:
|
||||
- prepare
|
||||
- publication_trust_preflight
|
||||
if: ${{ needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
@@ -367,7 +407,9 @@ jobs:
|
||||
|
||||
windows_install_command_smoke:
|
||||
name: Windows PowerShell 5.1 Install Command Smoke
|
||||
needs: prepare
|
||||
needs:
|
||||
- prepare
|
||||
- publication_trust_preflight
|
||||
if: ${{ needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
|
||||
runs-on: windows-2025
|
||||
timeout-minutes: 15
|
||||
@@ -702,7 +744,9 @@ jobs:
|
||||
retention-days: 14
|
||||
|
||||
release_note_visuals:
|
||||
needs: prepare
|
||||
needs:
|
||||
- prepare
|
||||
- publication_trust_preflight
|
||||
if: ${{ needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
@@ -1188,6 +1232,7 @@ jobs:
|
||||
backfill_release_assets:
|
||||
needs:
|
||||
- prepare
|
||||
- publication_trust_preflight
|
||||
if: ${{ needs.prepare.outputs.historical_asset_backfill_only == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
@@ -1348,6 +1393,7 @@ jobs:
|
||||
release_readiness:
|
||||
needs:
|
||||
- prepare
|
||||
- publication_trust_preflight
|
||||
- build_release_candidate
|
||||
- qualify_release_containers
|
||||
- frontend_bundle
|
||||
@@ -1362,7 +1408,7 @@ jobs:
|
||||
- install_sh_smoke
|
||||
- publish_helm_chart
|
||||
- stage_private_pro_runtime
|
||||
if: ${{ always() && needs.prepare.result == 'success' && needs.build_release_candidate.result == 'success' && needs.qualify_release_containers.result == 'success' && needs.frontend_bundle.result == 'success' && needs.frontend_checks.result == 'success' && needs.windows_install_command_smoke.result == 'success' && needs.backend_tests.result == 'success' && needs.release_smoke.result == 'success' && (needs.integration_tests.result == 'success' || needs.integration_tests.result == 'skipped') && needs.create_release.result == 'success' && needs.publish_docker.result == 'success' && needs.validate_release_assets.result == 'success' && needs.install_sh_smoke.result == 'success' && needs.publish_helm_chart.result == 'success' && ( !startsWith(needs.prepare.outputs.version, '6.') || needs.stage_private_pro_runtime.result == 'success' ) && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' }}
|
||||
if: ${{ always() && needs.prepare.result == 'success' && needs.publication_trust_preflight.result == 'success' && needs.build_release_candidate.result == 'success' && needs.qualify_release_containers.result == 'success' && needs.frontend_bundle.result == 'success' && needs.frontend_checks.result == 'success' && needs.windows_install_command_smoke.result == 'success' && needs.backend_tests.result == 'success' && needs.release_smoke.result == 'success' && (needs.integration_tests.result == 'success' || needs.integration_tests.result == 'skipped') && needs.create_release.result == 'success' && needs.publish_docker.result == 'success' && needs.validate_release_assets.result == 'success' && needs.install_sh_smoke.result == 'success' && needs.publish_helm_chart.result == 'success' && ( !startsWith(needs.prepare.outputs.version, '6.') || needs.stage_private_pro_runtime.result == 'success' ) && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
@@ -1376,7 +1422,8 @@ jobs:
|
||||
stage_private_pro_runtime:
|
||||
needs:
|
||||
- prepare
|
||||
if: ${{ always() && needs.prepare.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' && startsWith(needs.prepare.outputs.version, '6.') }}
|
||||
- publication_trust_preflight
|
||||
if: ${{ always() && needs.prepare.result == 'success' && needs.publication_trust_preflight.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' && startsWith(needs.prepare.outputs.version, '6.') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
outputs:
|
||||
@@ -1904,6 +1951,7 @@ jobs:
|
||||
name: Release Activation Commit Verdict
|
||||
needs:
|
||||
- prepare
|
||||
- publication_trust_preflight
|
||||
- release_smoke
|
||||
- windows_install_command_smoke
|
||||
- create_release
|
||||
@@ -1931,6 +1979,7 @@ jobs:
|
||||
TAG: ${{ needs.prepare.outputs.tag }}
|
||||
EXPECTED_COMMIT: ${{ needs.create_release.outputs.target_commitish }}
|
||||
RELEASE_ID: ${{ needs.create_release.outputs.release_id }}
|
||||
PUBLICATION_TRUST_RESULT: ${{ needs.publication_trust_preflight.result }}
|
||||
CREATE_RESULT: ${{ needs.create_release.result }}
|
||||
SMOKE_RESULT: ${{ needs.release_smoke.result }}
|
||||
WINDOWS_INSTALL_COMMAND_RESULT: ${{ needs.windows_install_command_smoke.result }}
|
||||
@@ -1959,6 +2008,7 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
require_result "publication trust preflight" "$PUBLICATION_TRUST_RESULT" success
|
||||
require_result "release smoke" "$SMOKE_RESULT" success
|
||||
require_result "Windows install command smoke" "$WINDOWS_INSTALL_COMMAND_RESULT" success
|
||||
require_result "release staging" "$CREATE_RESULT" success
|
||||
|
||||
@@ -89,7 +89,10 @@ Normal stable publication and stable dry runs select `signpath` directly.
|
||||
- Every new release is assembled and validated as a draft. Its activation
|
||||
marker is uploaded and digest-checked before publication; GitHub must then
|
||||
report the published release as immutable, protecting its tag and complete
|
||||
asset set from replacement.
|
||||
asset set from replacement. A GitHub-hosted publication preflight proves the
|
||||
repository setting before compilation, signing, private staging, or draft
|
||||
assembly begins, and activation repeats that setting check immediately before
|
||||
publication to catch later drift.
|
||||
- Customer-facing image aliases, Helm indexes, paid-runtime pointers, and demo
|
||||
environments are not promoted until `gh release verify <tag> --repo
|
||||
rcourtman/Pulse` validates GitHub's signed release attestation and `gh release
|
||||
|
||||
@@ -2774,6 +2774,7 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
|
||||
}
|
||||
createJob := workflowJobBlock(t, createWorkflow, "create_release")
|
||||
prepareJob := workflowJobBlock(t, createWorkflow, "prepare")
|
||||
publicationPreflightJob := workflowJobBlock(t, createWorkflow, "publication_trust_preflight")
|
||||
frontendBundleJob := workflowJobBlock(t, createWorkflow, "frontend_bundle")
|
||||
backendJob := workflowJobBlock(t, createWorkflow, "backend_tests")
|
||||
integrationJob := workflowJobBlock(t, createWorkflow, "integration_tests")
|
||||
@@ -2782,6 +2783,7 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
|
||||
readinessJob := workflowJobBlock(t, createWorkflow, "release_readiness")
|
||||
dispatchJob := workflowJobBlock(t, createWorkflow, "dispatch_release_convergence")
|
||||
activationJob := workflowJobBlock(t, createWorkflow, "activate_release")
|
||||
commitVerdictJob := workflowJobBlock(t, createWorkflow, "release_commit_verdict")
|
||||
leaseJob := workflowJobBlock(t, convergenceWorkflow, "acquire_customer_promotion_lease")
|
||||
privatePromotionJob := workflowJobBlock(t, convergenceWorkflow, "promote_private_pro_runtime")
|
||||
floatingJob := workflowJobBlock(t, convergenceWorkflow, "promote_floating_tags")
|
||||
@@ -2803,13 +2805,37 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
|
||||
if strings.Contains(prepareJob, "sparse-checkout") {
|
||||
t.Fatal("release preparation must leave a complete worktree for the following compile job")
|
||||
}
|
||||
for _, needle := range []string{
|
||||
`runs-on: ubuntu-24.04`,
|
||||
`GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}`,
|
||||
`./scripts/check-github-release-immutability.sh "${GITHUB_REPOSITORY}"`,
|
||||
`github.event.inputs.draft_only != 'true'`,
|
||||
`historical_asset_backfill_only != 'true'`,
|
||||
} {
|
||||
if !strings.Contains(publicationPreflightJob, needle) {
|
||||
t.Fatalf("publication trust preflight missing early immutable-setting contract: %s", needle)
|
||||
}
|
||||
}
|
||||
for label, job := range map[string]string{
|
||||
"release candidate": workflowJobBlock(t, createWorkflow, "build_release_candidate"),
|
||||
"frontend bundle": frontendBundleJob,
|
||||
"frontend checks": workflowJobBlock(t, createWorkflow, "frontend_checks"),
|
||||
"Windows smoke": workflowJobBlock(t, createWorkflow, "windows_install_command_smoke"),
|
||||
"release-note visuals": workflowJobBlock(t, createWorkflow, "release_note_visuals"),
|
||||
"private Pro staging": privateStageJob,
|
||||
} {
|
||||
if !strings.Contains(job, "- publication_trust_preflight") {
|
||||
t.Fatalf("%s must wait for publication trust preflight", label)
|
||||
}
|
||||
}
|
||||
|
||||
for _, needle := range []string{
|
||||
`fromJSON('["self-hosted","Linux","X64","pulse-pve-compile"]')`,
|
||||
`GITHUB_WORKFLOW_SHA`,
|
||||
`ref: ${{ inputs.source_sha }}`,
|
||||
`PULSE_RELEASE_BUILD_JOBS: "2"`,
|
||||
`./scripts/build-release-binaries.sh "${{ inputs.version }}"`,
|
||||
`VERSION: ${{ inputs.version }}`,
|
||||
`./scripts/build-release-binaries.sh "${VERSION}" "$RUNNER_TEMP/release-compiled"`,
|
||||
`release-compiled-${{ inputs.source_sha }}-${{ inputs.version }}-${{ inputs.request_id }}`,
|
||||
} {
|
||||
if !strings.Contains(compileJob, needle) {
|
||||
@@ -2886,8 +2912,9 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, needle := range []string{
|
||||
`./scripts/build-release.sh "${{ inputs.version }}"`,
|
||||
`scripts/validate-release.sh "${{ inputs.version }}" --skip-docker`,
|
||||
`VERSION: ${{ inputs.version }}`,
|
||||
`./scripts/build-release.sh "${VERSION}"`,
|
||||
`scripts/validate-release.sh "${VERSION}" --skip-docker`,
|
||||
`scripts/release_candidate_manifest.py create`,
|
||||
`compression-level: 0`,
|
||||
`retention-days: 1`,
|
||||
@@ -2956,6 +2983,7 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, dependency := range []string{
|
||||
"- publication_trust_preflight",
|
||||
"- create_release",
|
||||
"- publish_docker",
|
||||
"- validate_release_assets",
|
||||
@@ -2967,6 +2995,13 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
|
||||
t.Fatalf("immutable release readiness missing dependency: %s", dependency)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(readinessJob, `needs.publication_trust_preflight.result == 'success'`) {
|
||||
t.Fatal("immutable release readiness must require successful publication trust preflight")
|
||||
}
|
||||
if !strings.Contains(commitVerdictJob, "- publication_trust_preflight") ||
|
||||
!strings.Contains(commitVerdictJob, `require_result "publication trust preflight"`) {
|
||||
t.Fatal("release commit verdict must surface publication trust preflight failure")
|
||||
}
|
||||
for _, forbiddenDependency := range []string{
|
||||
"- publish_helm_pages",
|
||||
"- promote_floating_tags",
|
||||
|
||||
@@ -347,6 +347,9 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
def test_release_commit_dispatches_durable_convergence_before_activation(self) -> None:
|
||||
workflow = read(".github/workflows/create-release.yml")
|
||||
convergence = read(".github/workflows/release-convergence.yml")
|
||||
publication_preflight = workflow_job_block(
|
||||
workflow, "publication_trust_preflight"
|
||||
)
|
||||
readiness = workflow_job_block(workflow, "release_readiness")
|
||||
dispatch = workflow_job_block(workflow, "dispatch_release_convergence")
|
||||
activation = workflow_job_block(workflow, "activate_release")
|
||||
@@ -354,7 +357,29 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
recovery = read(".github/workflows/recover-release-activation.yml")
|
||||
recovery_activation = workflow_job_block(recovery, "recover_activation")
|
||||
|
||||
self.assertIn("runs-on: ubuntu-24.04", publication_preflight)
|
||||
self.assertIn("secrets.WORKFLOW_PAT", publication_preflight)
|
||||
self.assertIn(
|
||||
"check-github-release-immutability.sh", publication_preflight
|
||||
)
|
||||
self.assertIn("github.event.inputs.draft_only != 'true'", publication_preflight)
|
||||
self.assertIn(
|
||||
"historical_asset_backfill_only != 'true'", publication_preflight
|
||||
)
|
||||
for early_job_name in (
|
||||
"build_release_candidate",
|
||||
"frontend_bundle",
|
||||
"frontend_checks",
|
||||
"windows_install_command_smoke",
|
||||
"release_note_visuals",
|
||||
"stage_private_pro_runtime",
|
||||
):
|
||||
with self.subTest(early_job_name=early_job_name):
|
||||
early_job = workflow_job_block(workflow, early_job_name)
|
||||
self.assertIn("- publication_trust_preflight", early_job)
|
||||
|
||||
for dependency in (
|
||||
"publication_trust_preflight",
|
||||
"create_release",
|
||||
"publish_docker",
|
||||
"validate_release_assets",
|
||||
@@ -377,6 +402,13 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn("needs: acquire_customer_promotion_lease", mutable)
|
||||
|
||||
self.assertIn("- release_readiness", activation)
|
||||
self.assertIn(
|
||||
"needs.publication_trust_preflight.result == 'success'", readiness
|
||||
)
|
||||
self.assertIn("- publication_trust_preflight", commit_verdict)
|
||||
self.assertIn(
|
||||
'require_result "publication trust preflight"', commit_verdict
|
||||
)
|
||||
self.assertIn("- dispatch_release_convergence", activation)
|
||||
self.assertNotIn("- release_readiness", dispatch)
|
||||
self.assertIn("- create_release", dispatch)
|
||||
|
||||
Reference in New Issue
Block a user