Skip to content

Commit 4db08f8

Browse files
gitcommit90claude
andauthored
Add exact-artifact stable promotion (#69)
* build: add exact stable promotion gates Promote verified candidate bytes without rebuilding, gate publication behind explicit owner approval, and replace source-coded fallback digests with validated stable manifests. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> * fix: parse promotion changelog headings literally Avoid dynamic regular expressions for owner-supplied version input and cover metacharacter handling. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> --------- Signed-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> Co-authored-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0850975 commit 4db08f8

23 files changed

Lines changed: 1585 additions & 107 deletions

.github/workflows/candidate.yml

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
outputs:
3434
artifact-name: ${{ steps.identity.outputs.artifact_name }}
3535
commit: ${{ steps.identity.outputs.commit }}
36+
ci-run-id: ${{ steps.identity.outputs.ci_run_id }}
3637
steps:
3738
- name: Check out the exact successful CI commit
3839
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
@@ -94,6 +95,7 @@ jobs:
9495
id: identity
9596
env:
9697
CI_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
98+
HELM_CANDIDATE_CI_RUN_ID: ${{ github.event.workflow_run.id }}
9799
run: |
98100
set -euo pipefail
99101
version="$(node -p 'require("./package.json").version')"
@@ -107,6 +109,7 @@ jobs:
107109
sha256sum "$evidence/candidate.json" > "$evidence/manifest.sha256"
108110
printf 'artifact_name=1helm-candidate-%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
109111
printf 'commit=%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
112+
printf 'ci_run_id=%s\n' "$HELM_CANDIDATE_CI_RUN_ID" >> "$GITHUB_OUTPUT"
110113
111114
- name: Attest archive provenance on the hosted builder
112115
id: attest
@@ -168,8 +171,67 @@ jobs:
168171
sudo -n /usr/local/sbin/1helm-candidate-install
169172
170173
- name: Publish private installation evidence in the job log
171-
if: always()
172174
run: |
173175
test -r /var/lib/1helm-candidate/evidence/status.json
174176
python3 /usr/local/lib/1helm-candidate/candidate-boundary.py summary \
175177
/var/lib/1helm-candidate/evidence/status.json
178+
179+
- name: Retain exact private dress-rehearsal evidence
180+
run: |
181+
set -euo pipefail
182+
install -d -m 0700 candidate-result
183+
install -m 0600 /var/lib/1helm-candidate/evidence/status.json candidate-result/dress-rehearsal.json
184+
185+
- name: Upload private evidence for hosted promotion assembly
186+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
187+
with:
188+
name: 1helm-dress-rehearsal-evidence-${{ needs.build.outputs.commit }}
189+
path: candidate-result/dress-rehearsal.json
190+
if-no-files-found: error
191+
retention-days: 30
192+
193+
assemble-promotion:
194+
name: Assemble honest Phase 3 promotion candidate
195+
needs: [build, deploy]
196+
if: needs.deploy.result == 'success'
197+
runs-on: ubuntu-latest
198+
timeout-minutes: 10
199+
permissions:
200+
contents: read
201+
actions: read
202+
steps:
203+
- name: Check out the exact candidate commit
204+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
205+
with:
206+
ref: ${{ needs.build.outputs.commit }}
207+
fetch-depth: 1
208+
persist-credentials: false
209+
210+
- name: Download exact built Linux candidate
211+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
212+
with:
213+
name: ${{ needs.build.outputs.artifact-name }}
214+
path: candidate-download
215+
216+
- name: Download exact dress-rehearsal result
217+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
218+
with:
219+
name: 1helm-dress-rehearsal-evidence-${{ needs.build.outputs.commit }}
220+
path: rehearsal-download
221+
222+
- name: Assemble retained bytes without rebuilding
223+
env:
224+
HELM_CANDIDATE_DOWNLOAD: candidate-download
225+
HELM_REHEARSAL_EVIDENCE: rehearsal-download/dress-rehearsal.json
226+
HELM_PROMOTION_OUTPUT: promotion-candidate
227+
HELM_PROJECT_ROOT: .
228+
HELM_CANDIDATE_CI_RUN_ID: ${{ needs.build.outputs.ci-run-id }}
229+
run: node scripts/candidate-promotion-skeleton.mjs
230+
231+
- name: Upload the exact Phase 3 promotion candidate
232+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
233+
with:
234+
name: 1helm-promotion-candidate-${{ needs.build.outputs.commit }}
235+
path: promotion-candidate/
236+
if-no-files-found: error
237+
retention-days: 30
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
name: Promote exact candidate to Stable
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
candidate_workflow_run_id:
7+
description: Exact successful Candidate dress rehearsal workflow run ID
8+
required: true
9+
type: string
10+
candidate_artifact_id:
11+
description: Exact immutable candidate artifact ID from that run
12+
required: true
13+
type: string
14+
version:
15+
description: Intended three-part semantic version (without v)
16+
required: true
17+
type: string
18+
mode:
19+
description: Dry-run validates without publishing; publish enters the protected gate
20+
required: true
21+
default: dry-run
22+
type: choice
23+
options: [dry-run, publish]
24+
confirmation:
25+
description: Publish only - PROMOTE EXACT CANDIDATE vX.Y.Z RUN N ARTIFACT N
26+
required: false
27+
type: string
28+
29+
permissions:
30+
contents: read
31+
actions: read
32+
33+
concurrency:
34+
group: 1helm-stable-promotion
35+
cancel-in-progress: false
36+
37+
jobs:
38+
verify:
39+
name: Verify exact candidate bytes (never publishes)
40+
runs-on: ubuntu-latest
41+
timeout-minutes: 20
42+
permissions:
43+
contents: read
44+
actions: read
45+
outputs:
46+
confirmation: ${{ steps.identity.outputs.confirmation }}
47+
steps:
48+
- name: Validate allowlisted dispatch inputs
49+
id: identity
50+
env:
51+
CANDIDATE_RUN_ID: ${{ inputs.candidate_workflow_run_id }}
52+
CANDIDATE_ARTIFACT_ID: ${{ inputs.candidate_artifact_id }}
53+
VERSION: ${{ inputs.version }}
54+
MODE: ${{ inputs.mode }}
55+
run: |
56+
set -euo pipefail
57+
[[ "$CANDIDATE_RUN_ID" =~ ^[0-9]+$ ]]
58+
[[ "$CANDIDATE_ARTIFACT_ID" =~ ^[0-9]+$ ]]
59+
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
60+
[[ "$MODE" == dry-run || "$MODE" == publish ]]
61+
printf 'confirmation=PROMOTE EXACT CANDIDATE v%s RUN %s ARTIFACT %s\n' "$VERSION" "$CANDIDATE_RUN_ID" "$CANDIDATE_ARTIFACT_ID" >> "$GITHUB_OUTPUT"
62+
63+
- name: Refuse publish mode without the exact owner confirmation
64+
if: inputs.mode == 'publish'
65+
env:
66+
OWNER_CONFIRMATION: ${{ inputs.confirmation }}
67+
EXPECTED_CONFIRMATION: ${{ steps.identity.outputs.confirmation }}
68+
run: test "$OWNER_CONFIRMATION" = "$EXPECTED_CONFIRMATION"
69+
70+
- name: Check out current main verification code
71+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
72+
with:
73+
ref: refs/heads/main
74+
fetch-depth: 0
75+
persist-credentials: false
76+
77+
- name: Fetch exact trusted GitHub identities
78+
env:
79+
GH_TOKEN: ${{ github.token }}
80+
CANDIDATE_RUN_ID: ${{ inputs.candidate_workflow_run_id }}
81+
CANDIDATE_ARTIFACT_ID: ${{ inputs.candidate_artifact_id }}
82+
run: |
83+
set -euo pipefail
84+
mkdir -m 0700 promotion-api promotion-bundle
85+
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$CANDIDATE_RUN_ID" > promotion-api/run.json
86+
gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$CANDIDATE_ARTIFACT_ID" > promotion-api/artifact.json
87+
88+
- name: Download only the exact candidate artifact ID
89+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
90+
with:
91+
artifact-ids: ${{ inputs.candidate_artifact_id }}
92+
path: promotion-bundle
93+
merge-multiple: true
94+
github-token: ${{ github.token }}
95+
repository: ${{ github.repository }}
96+
run-id: ${{ inputs.candidate_workflow_run_id }}
97+
98+
- name: Bind trusted candidate API records and derive the exact CI run ID
99+
id: candidate_api
100+
env:
101+
HELM_PROMOTION_BUNDLE: promotion-bundle
102+
HELM_PROMOTION_RUN_JSON: promotion-api/run.json
103+
HELM_PROMOTION_ARTIFACT_JSON: promotion-api/artifact.json
104+
run: node scripts/prepare-promotion-bundle.mjs
105+
106+
- name: Fetch and bind the exact trusted CI run
107+
env:
108+
GH_TOKEN: ${{ github.token }}
109+
CI_RUN_ID: ${{ steps.candidate_api.outputs.ci_run_id }}
110+
HELM_PROMOTION_BUNDLE: promotion-bundle
111+
HELM_PROMOTION_RUN_JSON: promotion-api/run.json
112+
HELM_PROMOTION_ARTIFACT_JSON: promotion-api/artifact.json
113+
HELM_PROMOTION_CI_JSON: promotion-api/ci.json
114+
run: |
115+
set -euo pipefail
116+
[[ "$CI_RUN_ID" =~ ^[0-9]+$ ]]
117+
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$CI_RUN_ID" > promotion-api/ci.json
118+
node scripts/prepare-promotion-bundle.mjs
119+
120+
- name: Cryptographically verify Linux provenance on the hosted verifier
121+
env:
122+
GH_TOKEN: ${{ github.token }}
123+
HELM_PROMOTION_BUNDLE: promotion-bundle
124+
run: node scripts/verify-promotion-attestation.mjs
125+
126+
- name: Verify current main, absent tag, and absent release
127+
id: repository
128+
env:
129+
GH_TOKEN: ${{ github.token }}
130+
VERSION: ${{ inputs.version }}
131+
run: |
132+
set -euo pipefail
133+
main_commit="$(git rev-parse refs/remotes/origin/main)"
134+
candidate_commit="$(node -p 'require("./promotion-bundle/promotion.json").commit')"
135+
git merge-base --is-ancestor "$candidate_commit" refs/remotes/origin/main
136+
node scripts/github-promotion-gates.mjs version-absent "$VERSION"
137+
printf 'main_commit=%s\n' "$main_commit" >> "$GITHUB_OUTPUT"
138+
139+
- name: Verify complete evidence, exact bytes, and no-rebuild promotion outputs
140+
env:
141+
VERSION: ${{ inputs.version }}
142+
CANDIDATE_RUN_ID: ${{ inputs.candidate_workflow_run_id }}
143+
CANDIDATE_ARTIFACT_ID: ${{ inputs.candidate_artifact_id }}
144+
HELM_PROMOTION_MAIN_COMMIT: ${{ steps.repository.outputs.main_commit }}
145+
HELM_PROMOTION_MAIN_CONTAINS_CANDIDATE: "1"
146+
HELM_PROMOTION_TAG_ABSENT: "1"
147+
HELM_PROMOTION_RELEASE_ABSENT: "1"
148+
HELM_PROMOTION_LINUX_ATTESTATION_VERIFIED: "1"
149+
run: |
150+
set -euo pipefail
151+
node scripts/promotion-status.mjs \
152+
--bundle promotion-bundle \
153+
--version "$VERSION" \
154+
--candidate-run "$CANDIDATE_RUN_ID" \
155+
--candidate-artifact "$CANDIDATE_ARTIFACT_ID" \
156+
--write-verified promotion-bundle
157+
158+
- name: Retain verified exact bytes only for an explicitly confirmed publish request
159+
if: >-
160+
inputs.mode == 'publish' &&
161+
inputs.confirmation == steps.identity.outputs.confirmation
162+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
163+
with:
164+
name: verified-stable-promotion-${{ inputs.candidate_artifact_id }}
165+
path: |
166+
promotion-bundle/1Helm-${{ inputs.version }}-arm64.dmg
167+
promotion-bundle/1Helm-${{ inputs.version }}-mac-arm64.zip
168+
promotion-bundle/1Helm-${{ inputs.version }}-linux-node.tgz
169+
promotion-bundle/1Helm-${{ inputs.version }}-stable.json
170+
promotion-bundle/1Helm-${{ inputs.version }}-release-notes.md
171+
promotion-bundle/verified-promotion.json
172+
if-no-files-found: error
173+
retention-days: 1
174+
175+
publish:
176+
name: Protected owner approval - publish Stable
177+
needs: verify
178+
if: >-
179+
inputs.mode == 'publish' &&
180+
inputs.confirmation == needs.verify.outputs.confirmation
181+
runs-on: ubuntu-latest
182+
timeout-minutes: 10
183+
environment: Stable publication
184+
permissions:
185+
contents: write
186+
actions: read
187+
steps:
188+
- name: Check out current main for the guarded tag push
189+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
190+
with:
191+
ref: refs/heads/main
192+
fetch-depth: 0
193+
persist-credentials: true
194+
195+
- name: Download the complete verified promotion output
196+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
197+
with:
198+
name: verified-stable-promotion-${{ inputs.candidate_artifact_id }}
199+
path: verified-promotion
200+
201+
- name: Publish annotated immutable tag and one complete GitHub Release
202+
env:
203+
GH_TOKEN: ${{ github.token }}
204+
STABLE_PUBLICATION_ENABLED: ${{ secrets.STABLE_PUBLICATION_ENABLED }}
205+
HELM_PROMOTION_BUNDLE: verified-promotion
206+
HELM_PROMOTION_MODE: ${{ inputs.mode }}
207+
HELM_PROMOTION_VERSION: ${{ inputs.version }}
208+
HELM_PROMOTION_RUN_ID: ${{ inputs.candidate_workflow_run_id }}
209+
HELM_PROMOTION_ARTIFACT_ID: ${{ inputs.candidate_artifact_id }}
210+
HELM_PROMOTION_CONFIRMATION: ${{ inputs.confirmation }}
211+
run: node scripts/publish-promotion.mjs

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Stable desktop releases now use a fail-closed manual promotion gate that
13+
verifies and republishes exact retained candidate bytes, keeps publication
14+
behind explicit owner approval, and serves digest-validated last-known-good
15+
website metadata without a follow-up digest commit.
16+
1017
## [0.0.41] - 2026-08-03
1118

1219
### Fixed

docs/GOVERNANCE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ contract as the slice hardens.
8686
every user-visible fix and feature accepted for that release, using the same
8787
numbered ledger as the originating request when one exists. A short summary
8888
can introduce that ledger but cannot replace it.
89+
- Desktop Stable publication uses only the manual promotion workflow. It
90+
verifies and republishes exact retained candidate bytes without rebuilding,
91+
requires an explicit identity-bound owner confirmation and approval in the
92+
protected **Stable publication** Environment, and refuses any existing tag or
93+
Release. Repository automation does not create or configure that Environment.
94+
- Every promoted Release includes a digest-qualified machine-readable Stable
95+
manifest. The site retains the last manifest it validated and must fail closed
96+
instead of inventing metadata. Tags and Release assets are never rewritten;
97+
rollback uses a new version or a supported installed-updater rollback policy.
8998
- macOS verification must use the exact publicly downloaded artifact, preserve
9099
Application Support, and prove signature/ticket/Gatekeeper, launch, version,
91100
loopback behavior, and retained state on the retained release host.

0 commit comments

Comments
 (0)