From ae316420d83cd4a0260716c60e7539cc4791fc41 Mon Sep 17 00:00:00 2001 From: monkseekee Date: Sat, 18 Jul 2026 11:55:54 +0800 Subject: [PATCH] fix(release): enforce one-review public promotion --- .../workflows/required-hmg-public-quality.yml | 24 ++++++ .../reusable-hmg-public-promotion.yml | 78 ++++++++++++++++--- .../workflows/reusable-hmg-public-quality.yml | 29 +++++-- GOVERNANCE.md | 4 +- README.md | 4 +- 5 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/required-hmg-public-quality.yml diff --git a/.github/workflows/required-hmg-public-quality.yml b/.github/workflows/required-hmg-public-quality.yml new file mode 100644 index 0000000..abf01d8 --- /dev/null +++ b/.github/workflows/required-hmg-public-quality.yml @@ -0,0 +1,24 @@ +name: Required HMG Public Quality + +on: + pull_request: + merge_group: + +permissions: + contents: read + +concurrency: + group: required-hmg-public-quality-${{ github.event.pull_request.number || github.event.merge_group.head_sha }} + cancel-in-progress: true + +jobs: + public-quality: + if: github.repository == 'HMG-AI/HMG-public' + name: hmg-public-quality + uses: ./.github/workflows/reusable-hmg-public-quality.yml + with: + base_sha: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} + head_sha: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }} + head_ref: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} + permissions: + contents: read diff --git a/.github/workflows/reusable-hmg-public-promotion.yml b/.github/workflows/reusable-hmg-public-promotion.yml index 6c5143b..e2fbbbd 100644 --- a/.github/workflows/reusable-hmg-public-promotion.yml +++ b/.github/workflows/reusable-hmg-public-promotion.yml @@ -181,6 +181,55 @@ jobs: set -Eeuo pipefail echo "sha=$(git -C candidate rev-parse HEAD)" >> "${GITHUB_OUTPUT}" + - name: Synchronize the exact reviewed public export without credentials + shell: bash + run: | + set -Eeuo pipefail + readonly export_dir="${GITHUB_WORKSPACE}/source/export" + readonly candidate_dir="${GITHUB_WORKSPACE}/candidate" + if [[ ! -d "${export_dir}" || ! -f "${export_dir}/README.md" ]]; then + echo "::error::The exact source checkout does not contain a complete public export" + exit 2 + fi + + bash source/scripts/check-export-leak.sh + + # Target governance and all previously reviewed provenance records are + # target-owned. They survive the source-owned exact export mirror. + rsync -a --delete \ + --exclude=target/ \ + --exclude=node_modules/ \ + --exclude=__pycache__/ \ + --exclude='*.pyc' \ + --exclude=.DS_Store \ + --exclude=Cachedir.tag \ + --exclude=CACHEDIR.TAG \ + --exclude=.git/ \ + --exclude=.github/ \ + --exclude=commitlint.config.mjs \ + --exclude=release-provenance/ \ + "${export_dir}/" \ + "${candidate_dir}/" + + openapi_file="${candidate_dir}/openapi/hmg-server.yaml" + if [[ -f "${openapi_file}" ]]; then + cleaned_openapi="${RUNNER_TEMP}/hmg-public-openapi.yaml" + sed -E \ + -e '/disable_adaptive_recall:/d' \ + -e '/adaptive cheap-path gate/d' \ + -e '/disable_embedding_noise_gate:/d' \ + -e '/embedding noise prototype gate/d' \ + -e '/embedding_noise_gate_suppressed_vector:/d' \ + -e '/disable_symbol_fusion_boost:/d' \ + -e '/symbol-query weighted RRF/d' \ + -e '/disable_seed_length_penalty:/d' \ + -e '/length-based penalty on seed-stage/d' \ + "${openapi_file}" > "${cleaned_openapi}" + mv "${cleaned_openapi}" "${openapi_file}" + fi + + bash source/scripts/check-export-leak.sh --dir="${candidate_dir}" + - name: Enforce target-owned governance boundary shell: bash run: | @@ -286,7 +335,7 @@ jobs: echo "asset_dir=${asset_dir}" >> "${GITHUB_OUTPUT}" echo "asset_set_sha256=${asset_set_sha256}" >> "${GITHUB_OUTPUT}" - - name: Add only minimal binary release provenance to the candidate + - name: Add signed-release metadata beside the exact public export shell: bash env: ARTIFACT_NAME: ${{ steps.source-run.outputs.artifact_name }} @@ -294,13 +343,14 @@ jobs: RELEASE_TAG: ${{ inputs.release_tag }} SOURCE_RUN_URL: ${{ steps.source-run.outputs.url }} SOURCE_SHA: ${{ steps.source.outputs.sha }} + TARGET_BASE_SHA: ${{ steps.target-base.outputs.sha }} run: | set -Eeuo pipefail readonly metadata_directory='candidate/release-provenance' readonly metadata_relative_path="release-provenance/${RELEASE_TAG}.json" readonly metadata_path="candidate/${metadata_relative_path}" - if [[ -L "${metadata_directory}" || -e "${metadata_path}" || -L "${metadata_path}" ]]; then - echo "::error::Release provenance path already exists or is unsafe: ${metadata_relative_path}" + if [[ -L "${metadata_directory}" || -L "${metadata_path}" ]]; then + echo "::error::Release provenance path is unsafe: ${metadata_relative_path}" exit 2 fi mkdir -p "${metadata_directory}" @@ -311,10 +361,11 @@ jobs: --arg source_repository 'HMG-AI/HMG' \ --arg source_sha "${SOURCE_SHA}" \ --arg source_workflow_run "${SOURCE_RUN_URL}" \ + --arg target_base_sha "${TARGET_BASE_SHA}" \ --arg version "${RELEASE_TAG#v}" \ --argjson asset_count 17 \ '{ - schema_version: "hmg-public-binary-release-provenance/v1", + schema_version: "hmg-public-binary-release-provenance/v2", release_tag: $release_tag, version: $version, source_repository: $source_repository, @@ -322,16 +373,19 @@ jobs: source_workflow_run: $source_workflow_run, source_artifact: $artifact, asset_count: $asset_count, - asset_set_sha256: $asset_set_sha256 + asset_set_sha256: $asset_set_sha256, + target_base_sha: $target_base_sha }' > "${metadata_path}" - candidate_status="$( + provenance_status="$( git -C candidate status \ --porcelain=v1 \ - --untracked-files=all + --untracked-files=all \ + -- release-provenance )" - if [[ "${candidate_status}" != "?? ${metadata_relative_path}" ]]; then - echo "::error::Binary promotion may add only ${metadata_relative_path}" - printf '%s\n' "${candidate_status}" + if [[ "${provenance_status}" != "?? ${metadata_relative_path}" && \ + "${provenance_status}" != " M ${metadata_relative_path}" ]]; then + echo "::error::Promotion must preserve provenance history and add or replace only ${metadata_relative_path}" + printf '%s\n' "${provenance_status}" exit 2 fi @@ -873,12 +927,12 @@ jobs: pr_title="chore(public): promote HMG ${RELEASE_TAG}" printf -v pr_body '%s\n\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n%s' \ - 'Automated protected binary-only promotion from a verified HMG Actions artifact.' \ + 'Automated protected promotion of one exact HMG public export and its verified Actions artifact.' \ "- Source repository: \`HMG-AI/HMG\`" \ "- Source tag: \`${RELEASE_TAG}\`" \ "- Source SHA: \`${SOURCE_SHA}\`" \ "- Source public artifact build run: ${SOURCE_RUN_URL}" \ - '- Repository scope: audit metadata only; no source, website, or npm synchronization' \ + '- Repository scope: exact source export plus one signed release-provenance record; target governance and provenance history are preserved' \ "- Candidate tree: \`${CANDIDATE_TREE}\`" \ "- Draft asset set SHA-256: \`${ASSET_SET_SHA256}\`" \ "- Provenance key: \`${KEY_ID}\`" \ diff --git a/.github/workflows/reusable-hmg-public-quality.yml b/.github/workflows/reusable-hmg-public-quality.yml index 2a4b89a..28df804 100644 --- a/.github/workflows/reusable-hmg-public-quality.yml +++ b/.github/workflows/reusable-hmg-public-quality.yml @@ -181,13 +181,25 @@ jobs: fi fi readonly expected_path="release-provenance/${release_tag}.json" - changed="$(git -C candidate diff --name-only "${BASE_SHA}" "${HEAD_SHA}")" - if [[ "${changed}" != "${expected_path}" || \ - "$(git -C candidate diff --diff-filter=A --name-only "${BASE_SHA}" "${HEAD_SHA}")" != "${expected_path}" || \ + protected_changes="$( + git -C candidate diff --name-only "${BASE_SHA}" "${HEAD_SHA}" -- \ + .github \ + commitlint.config.mjs + )" + provenance_changes="$( + git -C candidate diff --name-only "${BASE_SHA}" "${HEAD_SHA}" -- \ + release-provenance + )" + if [[ -n "${protected_changes}" ]]; then + echo "::error::Automated promotion must preserve target-owned governance" + printf '%s\n' "${protected_changes}" + exit 2 + fi + if [[ "${provenance_changes}" != "${expected_path}" || \ ! -f "candidate/${expected_path}" || \ -L "candidate/${expected_path}" ]]; then - echo "::error::Automated binary promotion may add only ${expected_path}" - printf '%s\n' "${changed}" + echo "::error::Automated promotion must preserve provenance history and add or replace only ${expected_path}" + printf '%s\n' "${provenance_changes}" exit 2 fi @@ -310,6 +322,7 @@ jobs: --arg release_tag "${release_tag}" \ --arg source_sha "${source_sha}" \ --arg source_workflow_run "${workflow_run}" \ + --arg target_base_sha "${BASE_SHA}" \ --arg version "${release_tag#v}" ' keys == [ "asset_count", @@ -320,9 +333,10 @@ jobs: "source_repository", "source_sha", "source_workflow_run", + "target_base_sha", "version" ] and - .schema_version == "hmg-public-binary-release-provenance/v1" and + .schema_version == "hmg-public-binary-release-provenance/v2" and .release_tag == $release_tag and .version == $version and .source_repository == "HMG-AI/HMG" and @@ -330,7 +344,8 @@ jobs: .source_workflow_run == $source_workflow_run and .source_artifact == $artifact and .asset_count == 17 and - .asset_set_sha256 == $asset_set_sha256 + .asset_set_sha256 == $asset_set_sha256 and + .target_base_sha == $target_base_sha ' "${metadata_file}" >/dev/null set +e diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 0e16642..fcfa712 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -25,9 +25,9 @@ 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, and independently validates the exact 17-asset set. The candidate starts from protected `HMG-public/main` and may add only `release-provenance/v.json`, a secret-free binary-release audit record; it cannot synchronize HMG source, website, npm, or other product files. Preparation then computes the aggregate asset digest and full candidate Git tree. +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. 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 required target quality workflow has read-only contents permission, 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 stable version increase, 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. +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. The canonical promotion trailers are `HMG-Source-Repository`, `HMG-Source-Tag`, `HMG-Source-SHA`, `HMG-Workflow-Run`, `HMG-Asset-Set-SHA256`, `HMG-Candidate-Tree`, `HMG-Provenance-Key-ID`, and `HMG-Provenance-Signature-Ed25519`. The final `vMAJOR.MINOR.PATCH` tag must not exist before the protected promotion merges. Final tags are never moved or deleted, release assets are never overwritten, and a staging draft is transport state rather than review evidence. An interrupted staging asset in GitHub's `starter` state may be deleted by its immutable asset ID and uploaded again without clobber; every other non-`uploaded` state fails closed. diff --git a/README.md b/README.md index 9e17078..4ffa2ec 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ 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 promotion changes no product source, website, or npm content; its only repository change is `release-provenance/v.json`, a secret-free audit record that binds the HMG source SHA, Actions run, artifact name, and aggregate asset digest. An isolated job with no repository or Actions permissions signs the canonical provenance statement, and the target-scoped write token is exposed only while reconciling the deterministic 17-asset staging release and promotion 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 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. -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. It 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. Until the bootstrap pull requests are independently reviewed and merged, the exact-SHA HMG caller and HMG-public-specific required-workflow wrapper/ruleset remain intentionally absent and the replacement path remains `pending_not_active`. +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. Organization members can read the canonical team guide in [`HMG-Documents/github-governance`](https://github.com/HMG-AI/HMG-Documents/tree/main/github-governance).