From 5ac6a037be3b149d5979fc4623fafe3acdbd1611 Mon Sep 17 00:00:00 2001 From: monkseekee Date: Sat, 18 Jul 2026 14:03:38 +0800 Subject: [PATCH] fix(release): recover exact legacy promotions --- .../reusable-hmg-public-promotion.yml | 42 +++++++++++++++---- GOVERNANCE.md | 2 +- README.md | 2 +- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.github/workflows/reusable-hmg-public-promotion.yml b/.github/workflows/reusable-hmg-public-promotion.yml index e2fbbbd..839e59d 100644 --- a/.github/workflows/reusable-hmg-public-promotion.yml +++ b/.github/workflows/reusable-hmg-public-promotion.yml @@ -706,22 +706,48 @@ jobs: pr_json="$(gh api \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2026-03-10' \ - "/repos/${TARGET_REPOSITORY}/pulls?state=all&head=HMG-AI:${head_branch}&base=main&per_page=20")" + "/repos/${TARGET_REPOSITORY}/pulls?state=open&head=HMG-AI:${head_branch}&base=main&per_page=20")" pr_count="$(jq 'length' <<<"${pr_json}")" if ((pr_count > 1)); then - echo "::error::More than one pull request exists for ${head_branch}" + echo "::error::More than one open pull request exists for ${head_branch}" exit 2 fi pr_state="$(jq -r '.[0].state // empty' <<<"${pr_json}")" - pr_merged="$(jq -r '.[0].merged_at // empty' <<<"${pr_json}")" pr_number="$(jq -r '.[0].number // empty' <<<"${pr_json}")" - if [[ "${pr_state}" == "closed" ]]; then - if [[ -n "${pr_merged}" ]]; then + + closed_pr_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "/repos/${TARGET_REPOSITORY}/pulls?state=closed&head=HMG-AI:${head_branch}&base=main&per_page=20")" + merged_pr_count="$(jq '[.[] | select(.merged_at != null)] | length' <<<"${closed_pr_json}")" + abandoned_prs="$(jq -r '[.[] | select(.merged_at == null) | .number] | join(",")' <<<"${closed_pr_json}")" + if [[ -n "${abandoned_prs}" ]]; then + echo "::error::Promotion PR(s) ${abandoned_prs} were closed without merging; manual triage is required" + exit 2 + fi + if ((merged_pr_count > 0)); then + legacy_provenance="$(git show "HEAD:release-provenance/${RELEASE_TAG}.json" 2>/dev/null || true)" + if ! jq -e \ + --arg asset_set_sha256 "${ASSET_SET_SHA256}" \ + --arg release_tag "${RELEASE_TAG}" \ + --arg source_run_url "${SOURCE_RUN_URL}" \ + --arg source_sha "${SOURCE_SHA}" ' + .schema_version == "hmg-public-binary-release-provenance/v1" and + .release_tag == $release_tag and + .source_repository == "HMG-AI/HMG" and + .source_sha == $source_sha and + .source_workflow_run == $source_run_url and + .asset_count == 17 and + .asset_set_sha256 == $asset_set_sha256 + ' <<<"${legacy_provenance}" >/dev/null; then echo "::error::This promotion is already merged; recover the exact publisher run instead" - else - echo "::error::Promotion PR ${pr_number} was closed without merging; manual triage is required" + exit 2 fi - exit 2 + if ((merged_pr_count != 1)); then + echo "::error::Legacy recovery requires exactly one prior merged promotion pull request" + exit 2 + fi + echo "::notice::Recovering one exact legacy v1 promotion receipt through a new reviewed v2 promotion" fi set +e diff --git a/GOVERNANCE.md b/GOVERNANCE.md index fcfa712..a5a5acd 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -25,7 +25,7 @@ The organization required quality workflow also runs an independent, read-only w The HMG public promotion trust boundary is split into three fail-closed stages: -1. The source-side preparation job proves the exact SHA from a successful `Build public release artifacts (no publish)` workflow-dispatch run is reachable from protected `HMG/main`, binds its frozen version to the canonical stable release tag, independently validates the exact 17-asset set, and mirrors that SHA's reviewed `export/` tree into a candidate based on protected `HMG-public/main`. Target-owned `.github` policy, `commitlint.config.mjs`, and all historical `release-provenance/` records are excluded from deletion and must remain byte-for-byte unchanged; the candidate adds `release-provenance/v.json`, or deterministically replaces that same record only when recovering a pre-publication failure. Provenance schema v2 binds the immutable target base SHA, so recovery never needs a cleanup pull request and cannot silently reuse a receipt from a different target tree. Preparation then computes the aggregate asset digest and signs the full source-plus-provenance candidate Git tree. An unchanged retry reuses the same deterministic branch commit and existing approval; after merge, publisher retries resume from that same approved commit and never create a second release pull request. +1. The source-side preparation job proves the exact SHA from a successful `Build public release artifacts (no publish)` workflow-dispatch run is reachable from protected `HMG/main`, binds its frozen version to the canonical stable release tag, independently validates the exact 17-asset set, and mirrors that SHA's reviewed `export/` tree into a candidate based on protected `HMG-public/main`. Target-owned `.github` policy, `commitlint.config.mjs`, and all historical `release-provenance/` records are excluded from deletion and must remain byte-for-byte unchanged; the candidate adds `release-provenance/v.json`, or deterministically replaces that same record only when recovering a pre-publication failure. Provenance schema v2 binds the immutable target base SHA, so recovery never needs a cleanup pull request and cannot silently reuse a receipt from a different target tree. Preparation then computes the aggregate asset digest and signs the full source-plus-provenance candidate Git tree. An unchanged retry reuses the same deterministic branch commit and existing approval; after a v2 promotion merges, publisher retries resume from that same approved commit and never create a second release pull request. A legacy v1 receipt may be replaced through exactly one additional reviewed promotion only when the final tag is absent, the source SHA, workflow run, and exact-17 digest still match, no promotion PR was abandoned, and exactly one earlier promotion PR merged; this migration exception closes incomplete pre-v2 promotions without weakening normal v2 replay rules. 2. An isolated job with no repository or Actions permissions accepts only those validated scalar values, verifies the configured Ed25519 private-key fingerprint, and signs the canonical provenance statement. The target promotion commit records exactly the eight governed provenance trailers. The target-scoped write token is not available until the last job reconciles the deterministic staging transport and same-repository pull request. 3. The repository-specific `required-hmg-public-quality.yml` wrapper has read-only contents permission and calls `reusable-hmg-public-quality.yml` from the same immutable organization commit. A dedicated organization ruleset must target only `HMG-public` and pin that wrapper to a reviewed full SHA; the generic organization quality ruleset remains independently required and unchanged. The target gate binds reusable inputs to the `pull_request` or `merge_group` event payload, shellchecks all target-owned release policy scripts, runs the target classifier and publisher policy fixtures, and classifies any HMG trailers across `BASE..HEAD`. The script fixtures run for every candidate, including ordinary governance pull requests, without secrets or persisted checkout credentials. A release pull request must be one promotion commit directly on its event base; a governed merge group must contain exactly one complete promotion commit and the event-head tree must equal the signed tree, so a changed base or another queued change fails closed. Partial, duplicate, or unknown HMG trailers fail. The gate also verifies the reviewed public-key ID, detached signature, target governance boundary, public builds, and absence of the final tag. It never receives a write credential and never reads the write-visible staging draft. After protected merge, the `hmg-public-release` environment publisher re-verifies the signed commit and exact staging bytes before creating the final tag once and publishing an immutable release. diff --git a/README.md b/README.md index 4ffa2ec..2a285a1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Changes to governance must use a pull request, be reviewed by the governance COD The organization required quality workflow applies a read-only security policy to every added, copied, modified, renamed, or type-changed root workflow in a pull request or merge group. The checker itself is loaded from the exact immutable commit identified by `github.workflow_sha`; it never executes policy code from the candidate branch and checks out both repositories without persisted credentials. Changed workflows must use explicit least-privilege permissions, immutable action and container references, safe checkout settings, and must not directly interpolate untrusted event values into executable scripts or attempt an obvious direct write to `main`. Pull-request-like events cannot receive write permissions or custom checkout credentials and must use an approved literal GitHub-hosted runner label; dynamic expressions, arrays, and runner groups fail closed. This is a transitional changed-workflows-only gate so existing workflow debt does not block unrelated changes. The organization ruleset remains the authoritative control against direct default-branch writes; after the existing debt is remediated, this policy will move to repository-wide enforcement. -Automated cross-repository publication must use a target-repository branch and pull request. The HMG public release contract is centralized in `reusable-hmg-public-promotion.yml`; it never writes `HMG-public/main`, never submits a review or acts as a bypass actor, and may register rebase auto-merge after reconciling the deterministic branch and pull request. GitHub completes that merge only after every target rule and independent human review is satisfied. The workflow downloads the exact 17-asset output of a successful `Build public release artifacts (no publish)` run on protected HMG `main`, independently verifies the source-bound release set, and starts the candidate from protected `HMG-public/main`. The same deterministic candidate mirrors that exact source SHA's reviewed `export/` tree and adds `release-provenance/v.json`, a secret-free audit record that binds the HMG source SHA, Actions run, artifact name, aggregate asset digest, and immutable target base SHA. Target-owned `.github` policy, `commitlint.config.mjs`, and all historical provenance records are excluded from the source mirror and must remain unchanged. A pre-publication recovery may deterministically replace only the same version's provenance record; it never needs a cleanup pull request. An isolated job with no repository or Actions permissions signs the full candidate tree, and the target-scoped write token is exposed only while reconciling the deterministic 17-asset staging release and single promotion pull request. An unchanged retry reuses the same branch commit and approval; after merge, publisher retries resume from that approved commit without a new pull request. +Automated cross-repository publication must use a target-repository branch and pull request. The HMG public release contract is centralized in `reusable-hmg-public-promotion.yml`; it never writes `HMG-public/main`, never submits a review or acts as a bypass actor, and may register rebase auto-merge after reconciling the deterministic branch and pull request. GitHub completes that merge only after every target rule and independent human review is satisfied. The workflow downloads the exact 17-asset output of a successful `Build public release artifacts (no publish)` run on protected HMG `main`, independently verifies the source-bound release set, and starts the candidate from protected `HMG-public/main`. The same deterministic candidate mirrors that exact source SHA's reviewed `export/` tree and adds `release-provenance/v.json`, a secret-free audit record that binds the HMG source SHA, Actions run, artifact name, aggregate asset digest, and immutable target base SHA. Target-owned `.github` policy, `commitlint.config.mjs`, and all historical provenance records are excluded from the source mirror and must remain unchanged. A pre-publication recovery may deterministically replace only the same version's provenance record; it never needs a cleanup pull request. An isolated job with no repository or Actions permissions signs the full candidate tree, and the target-scoped write token is exposed only while reconciling the deterministic 17-asset staging release and promotion pull request. An unchanged retry reuses the same branch commit and approval; after a v2 promotion merges, publisher retries resume from that approved commit without a new pull request. One additional reviewed promotion is allowed only to migrate an exact, unpublished legacy v1 receipt whose source and asset digest still match and whose sole earlier promotion PR merged. Every automated promotion commit carries exactly eight governed trailers: source repository, stable source tag, source SHA, successful source workflow run, aggregate asset digest, candidate Git tree, reviewed Ed25519 key ID, and detached Ed25519 signature. `reusable-hmg-public-quality.yml` is the read-only, centrally governed build, drift, leak, stable-version, event-binding, candidate-tree, and signature gate. `required-hmg-public-quality.yml` is its repository-specific required-workflow wrapper and calls the reusable workflow from the same immutable organization commit. It must be attached by a dedicated organization ruleset whose repository target includes only `HMG-public` and whose workflow reference is pinned to a reviewed full SHA; the generic organization quality ruleset remains separate and unchanged. The gate shellchecks the target-owned release scripts and runs their classifier and publisher policy fixtures for every candidate, including ordinary governance pull requests. It also classifies HMG trailers across the immutable event commit range, so a merge-queue ref cannot bypass provenance checks; a governed merge group must contain exactly one complete promotion commit and its event-head tree must still equal the signed tree. It deliberately cannot read the write-visible staging draft and receives no write credential. Before merge it proves the final `vMAJOR.MINOR.PATCH` tag is absent; after merge, the environment-protected target publisher independently verifies the signed commit and staging bytes, creates the final tag once, publishes the release, and relies on immutable-release plus tag rules to prevent mutation. Production callers and required-workflow rulesets must pin these files to reviewed full commit SHAs.