Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion .github/workflows/candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
outputs:
artifact-name: ${{ steps.identity.outputs.artifact_name }}
commit: ${{ steps.identity.outputs.commit }}
ci-run-id: ${{ steps.identity.outputs.ci_run_id }}
steps:
- name: Check out the exact successful CI commit
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Expand Down Expand Up @@ -94,6 +95,7 @@ jobs:
id: identity
env:
CI_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
HELM_CANDIDATE_CI_RUN_ID: ${{ github.event.workflow_run.id }}
run: |
set -euo pipefail
version="$(node -p 'require("./package.json").version')"
Expand All @@ -107,6 +109,7 @@ jobs:
sha256sum "$evidence/candidate.json" > "$evidence/manifest.sha256"
printf 'artifact_name=1helm-candidate-%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
printf 'commit=%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
printf 'ci_run_id=%s\n' "$HELM_CANDIDATE_CI_RUN_ID" >> "$GITHUB_OUTPUT"

- name: Attest archive provenance on the hosted builder
id: attest
Expand Down Expand Up @@ -168,8 +171,67 @@ jobs:
sudo -n /usr/local/sbin/1helm-candidate-install

- name: Publish private installation evidence in the job log
if: always()
run: |
test -r /var/lib/1helm-candidate/evidence/status.json
python3 /usr/local/lib/1helm-candidate/candidate-boundary.py summary \
/var/lib/1helm-candidate/evidence/status.json

- name: Retain exact private dress-rehearsal evidence
run: |
set -euo pipefail
install -d -m 0700 candidate-result
install -m 0600 /var/lib/1helm-candidate/evidence/status.json candidate-result/dress-rehearsal.json

- name: Upload private evidence for hosted promotion assembly
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: 1helm-dress-rehearsal-evidence-${{ needs.build.outputs.commit }}
path: candidate-result/dress-rehearsal.json
if-no-files-found: error
retention-days: 30

assemble-promotion:
name: Assemble honest Phase 3 promotion candidate
needs: [build, deploy]
if: needs.deploy.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: read
steps:
- name: Check out the exact candidate commit
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ needs.build.outputs.commit }}
fetch-depth: 1
persist-credentials: false

- name: Download exact built Linux candidate
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.build.outputs.artifact-name }}
path: candidate-download

- name: Download exact dress-rehearsal result
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: 1helm-dress-rehearsal-evidence-${{ needs.build.outputs.commit }}
path: rehearsal-download

- name: Assemble retained bytes without rebuilding
env:
HELM_CANDIDATE_DOWNLOAD: candidate-download
HELM_REHEARSAL_EVIDENCE: rehearsal-download/dress-rehearsal.json
HELM_PROMOTION_OUTPUT: promotion-candidate
HELM_PROJECT_ROOT: .
HELM_CANDIDATE_CI_RUN_ID: ${{ needs.build.outputs.ci-run-id }}
run: node scripts/candidate-promotion-skeleton.mjs

- name: Upload the exact Phase 3 promotion candidate
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: 1helm-promotion-candidate-${{ needs.build.outputs.commit }}
path: promotion-candidate/
if-no-files-found: error
retention-days: 30
211 changes: 211 additions & 0 deletions .github/workflows/promote-stable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
name: Promote exact candidate to Stable

on:
workflow_dispatch:
inputs:
candidate_workflow_run_id:
description: Exact successful Candidate dress rehearsal workflow run ID
required: true
type: string
candidate_artifact_id:
description: Exact immutable candidate artifact ID from that run
required: true
type: string
version:
description: Intended three-part semantic version (without v)
required: true
type: string
mode:
description: Dry-run validates without publishing; publish enters the protected gate
required: true
default: dry-run
type: choice
options: [dry-run, publish]
confirmation:
description: Publish only - PROMOTE EXACT CANDIDATE vX.Y.Z RUN N ARTIFACT N
required: false
type: string

permissions:
contents: read
actions: read

concurrency:
group: 1helm-stable-promotion
cancel-in-progress: false

jobs:
verify:
name: Verify exact candidate bytes (never publishes)
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
actions: read
outputs:
confirmation: ${{ steps.identity.outputs.confirmation }}
steps:
- name: Validate allowlisted dispatch inputs
id: identity
env:
CANDIDATE_RUN_ID: ${{ inputs.candidate_workflow_run_id }}
CANDIDATE_ARTIFACT_ID: ${{ inputs.candidate_artifact_id }}
VERSION: ${{ inputs.version }}
MODE: ${{ inputs.mode }}
run: |
set -euo pipefail
[[ "$CANDIDATE_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$CANDIDATE_ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
[[ "$MODE" == dry-run || "$MODE" == publish ]]
printf 'confirmation=PROMOTE EXACT CANDIDATE v%s RUN %s ARTIFACT %s\n' "$VERSION" "$CANDIDATE_RUN_ID" "$CANDIDATE_ARTIFACT_ID" >> "$GITHUB_OUTPUT"

- name: Refuse publish mode without the exact owner confirmation
if: inputs.mode == 'publish'
env:
OWNER_CONFIRMATION: ${{ inputs.confirmation }}
EXPECTED_CONFIRMATION: ${{ steps.identity.outputs.confirmation }}
run: test "$OWNER_CONFIRMATION" = "$EXPECTED_CONFIRMATION"

- name: Check out current main verification code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: refs/heads/main
fetch-depth: 0
persist-credentials: false

- name: Fetch exact trusted GitHub identities
env:
GH_TOKEN: ${{ github.token }}
CANDIDATE_RUN_ID: ${{ inputs.candidate_workflow_run_id }}
CANDIDATE_ARTIFACT_ID: ${{ inputs.candidate_artifact_id }}
run: |
set -euo pipefail
mkdir -m 0700 promotion-api promotion-bundle
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$CANDIDATE_RUN_ID" > promotion-api/run.json
gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$CANDIDATE_ARTIFACT_ID" > promotion-api/artifact.json

- name: Download only the exact candidate artifact ID
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
artifact-ids: ${{ inputs.candidate_artifact_id }}
path: promotion-bundle
merge-multiple: true
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ inputs.candidate_workflow_run_id }}

- name: Bind trusted candidate API records and derive the exact CI run ID
id: candidate_api
env:
HELM_PROMOTION_BUNDLE: promotion-bundle
HELM_PROMOTION_RUN_JSON: promotion-api/run.json
HELM_PROMOTION_ARTIFACT_JSON: promotion-api/artifact.json
run: node scripts/prepare-promotion-bundle.mjs

- name: Fetch and bind the exact trusted CI run
env:
GH_TOKEN: ${{ github.token }}
CI_RUN_ID: ${{ steps.candidate_api.outputs.ci_run_id }}
HELM_PROMOTION_BUNDLE: promotion-bundle
HELM_PROMOTION_RUN_JSON: promotion-api/run.json
HELM_PROMOTION_ARTIFACT_JSON: promotion-api/artifact.json
HELM_PROMOTION_CI_JSON: promotion-api/ci.json
run: |
set -euo pipefail
[[ "$CI_RUN_ID" =~ ^[0-9]+$ ]]
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$CI_RUN_ID" > promotion-api/ci.json
node scripts/prepare-promotion-bundle.mjs

- name: Cryptographically verify Linux provenance on the hosted verifier
env:
GH_TOKEN: ${{ github.token }}
HELM_PROMOTION_BUNDLE: promotion-bundle
run: node scripts/verify-promotion-attestation.mjs

- name: Verify current main, absent tag, and absent release
id: repository
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
main_commit="$(git rev-parse refs/remotes/origin/main)"
candidate_commit="$(node -p 'require("./promotion-bundle/promotion.json").commit')"
git merge-base --is-ancestor "$candidate_commit" refs/remotes/origin/main
node scripts/github-promotion-gates.mjs version-absent "$VERSION"
printf 'main_commit=%s\n' "$main_commit" >> "$GITHUB_OUTPUT"

- name: Verify complete evidence, exact bytes, and no-rebuild promotion outputs
env:
VERSION: ${{ inputs.version }}
CANDIDATE_RUN_ID: ${{ inputs.candidate_workflow_run_id }}
CANDIDATE_ARTIFACT_ID: ${{ inputs.candidate_artifact_id }}
HELM_PROMOTION_MAIN_COMMIT: ${{ steps.repository.outputs.main_commit }}
HELM_PROMOTION_MAIN_CONTAINS_CANDIDATE: "1"
HELM_PROMOTION_TAG_ABSENT: "1"
HELM_PROMOTION_RELEASE_ABSENT: "1"
HELM_PROMOTION_LINUX_ATTESTATION_VERIFIED: "1"
run: |
set -euo pipefail
node scripts/promotion-status.mjs \
--bundle promotion-bundle \
--version "$VERSION" \
--candidate-run "$CANDIDATE_RUN_ID" \
--candidate-artifact "$CANDIDATE_ARTIFACT_ID" \
--write-verified promotion-bundle

- name: Retain verified exact bytes only for an explicitly confirmed publish request
if: >-
inputs.mode == 'publish' &&
inputs.confirmation == steps.identity.outputs.confirmation
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: verified-stable-promotion-${{ inputs.candidate_artifact_id }}
path: |
promotion-bundle/1Helm-${{ inputs.version }}-arm64.dmg
promotion-bundle/1Helm-${{ inputs.version }}-mac-arm64.zip
promotion-bundle/1Helm-${{ inputs.version }}-linux-node.tgz
promotion-bundle/1Helm-${{ inputs.version }}-stable.json
promotion-bundle/1Helm-${{ inputs.version }}-release-notes.md
promotion-bundle/verified-promotion.json
if-no-files-found: error
retention-days: 1

publish:
name: Protected owner approval - publish Stable
needs: verify
if: >-
inputs.mode == 'publish' &&
inputs.confirmation == needs.verify.outputs.confirmation
runs-on: ubuntu-latest
timeout-minutes: 10
environment: Stable publication
permissions:
contents: write
actions: read
steps:
- name: Check out current main for the guarded tag push
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: refs/heads/main
fetch-depth: 0
persist-credentials: true

- name: Download the complete verified promotion output
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: verified-stable-promotion-${{ inputs.candidate_artifact_id }}
path: verified-promotion

- name: Publish annotated immutable tag and one complete GitHub Release
env:
GH_TOKEN: ${{ github.token }}
STABLE_PUBLICATION_ENABLED: ${{ secrets.STABLE_PUBLICATION_ENABLED }}
HELM_PROMOTION_BUNDLE: verified-promotion
HELM_PROMOTION_MODE: ${{ inputs.mode }}
HELM_PROMOTION_VERSION: ${{ inputs.version }}
HELM_PROMOTION_RUN_ID: ${{ inputs.candidate_workflow_run_id }}
HELM_PROMOTION_ARTIFACT_ID: ${{ inputs.candidate_artifact_id }}
HELM_PROMOTION_CONFIRMATION: ${{ inputs.confirmation }}
run: node scripts/publish-promotion.mjs
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Stable desktop releases now use a fail-closed manual promotion gate that
verifies and republishes exact retained candidate bytes, keeps publication
behind explicit owner approval, and serves digest-validated last-known-good
website metadata without a follow-up digest commit.

## [0.0.41] - 2026-08-03

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