From 7d4127fb96491c0256801e088d31b8465bcd5418 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 19 Jun 2026 22:28:35 +0000 Subject: [PATCH] fix(release): harden publish artifact handoff Normalize downloaded CI AOT artifact envelopes before WASIX publish validation. Allow publish and publish-dry-run to target an explicit release commit while running the latest release tooling, with CI artifact gates and release-please tags anchored to that commit. Keep GitHub release asset uploads immutable by failing on conflicting bytes instead of exposing an overwrite switch. --- .github/actions/collect-ci-summary/action.yml | 2 +- .../download-wasix-runtime-build-artifacts.sh | 10 +- .github/scripts/resolve-release-head.sh | 75 +++++++++++++++ .github/workflows/release.yml | 91 ++++++++++++------- docs/maintainers/release-setup.md | 15 +++ ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 ++++++++-------- .../assets/generated/asset-inputs.sha256 | 2 +- .../policy/assertions/assert-ci-workflows.mjs | 4 +- tools/policy/check-release-policy.py | 79 ++++++++++++---- tools/release/check_artifact_targets.py | 6 +- tools/release/check_release_metadata.py | 3 + tools/release/release.py | 10 -- tools/release/upload_github_release_assets.py | 20 +--- tools/xtask/src/asset_io.rs | 42 +++++++++ 15 files changed, 315 insertions(+), 126 deletions(-) create mode 100755 .github/scripts/resolve-release-head.sh diff --git a/.github/actions/collect-ci-summary/action.yml b/.github/actions/collect-ci-summary/action.yml index 55b5bc36..b363529f 100644 --- a/.github/actions/collect-ci-summary/action.yml +++ b/.github/actions/collect-ci-summary/action.yml @@ -12,5 +12,5 @@ runs: echo echo "- Moon projects: \`moon query projects\`" echo "- Moon tasks: \`moon query tasks\`" - echo "- Release plan: \`tools/release/release.py plan --from-product-tags --head-ref HEAD\`" + echo "- Release plan: \`tools/release/release.py plan --from-product-tags --head-ref \`" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/scripts/download-wasix-runtime-build-artifacts.sh b/.github/scripts/download-wasix-runtime-build-artifacts.sh index 09b0eb6a..79de43a9 100755 --- a/.github/scripts/download-wasix-runtime-build-artifacts.sh +++ b/.github/scripts/download-wasix-runtime-build-artifacts.sh @@ -2,13 +2,17 @@ set -euo pipefail : "${GITHUB_TOKEN:?GITHUB_TOKEN is required}" -: "${GITHUB_SHA:?GITHUB_SHA is required}" +release_sha="${RELEASE_HEAD_SHA:-${GITHUB_SHA:-}}" +if [[ -z "$release_sha" ]]; then + echo "RELEASE_HEAD_SHA or GITHUB_SHA is required" >&2 + exit 2 +fi -# Installs the portable and AOT WASIX runtime outputs from the selected same-SHA +# Installs the portable and AOT WASIX runtime outputs from the selected release # CI workflow whose artifact builder gate passed. This is a release artifact # handoff, not a release-time runtime rebuild. if [[ -n "${CI_RUN_ID:-}" ]]; then cargo run -p xtask -- assets download --run-id "$CI_RUN_ID" --required-job Builds --all-targets else - cargo run -p xtask -- assets download --sha "$GITHUB_SHA" --required-job Builds --all-targets + cargo run -p xtask -- assets download --sha "$release_sha" --required-job Builds --all-targets fi diff --git a/.github/scripts/resolve-release-head.sh b/.github/scripts/resolve-release-head.sh new file mode 100755 index 00000000..bc72e572 --- /dev/null +++ b/.github/scripts/resolve-release-head.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GITHUB_SHA:?GITHUB_SHA is required}" +: "${GITHUB_REF:?GITHUB_REF is required}" +: "${GITHUB_RUN_ID:?GITHUB_RUN_ID is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GITHUB_ENV:?GITHUB_ENV is required}" + +input="${INPUT_RELEASE_COMMIT:-}" +workflow_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" + +if [[ -z "$input" ]]; then + release_sha="$workflow_sha" +else + if [[ ! "$input" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "release_commit must be a full 40-character commit SHA, got: $input" >&2 + exit 2 + fi + release_sha="$(git rev-parse "${input}^{commit}")" + release_sha_lower="$(printf '%s' "$release_sha" | tr '[:upper:]' '[:lower:]')" + input_lower="$(printf '%s' "$input" | tr '[:upper:]' '[:lower:]')" + if [[ "$release_sha_lower" != "$input_lower" ]]; then + echo "release_commit resolved to $release_sha, not $input" >&2 + exit 2 + fi +fi + +if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then + echo "Releases must be run from main; got $GITHUB_REF" >&2 + exit 2 +fi + +uses_temporary_target_branch=false +target_branch="main" +if [[ "$release_sha" != "$workflow_sha" ]]; then + if ! git merge-base --is-ancestor "$release_sha" "$workflow_sha"; then + echo "release_commit $release_sha must be an ancestor of workflow commit $workflow_sha" >&2 + exit 2 + fi + + disallowed=() + while IFS= read -r path; do + [[ -n "$path" ]] || continue + case "$path" in + .github/actions/*|.github/scripts/*|.github/workflows/*|tools/policy/*|tools/release/*|tools/xtask/*|docs/maintainers/release-setup.md) + ;; + *) + disallowed+=("$path") + ;; + esac + done < <(git diff --name-only "$release_sha" "$workflow_sha" --) + + if [[ "${#disallowed[@]}" -gt 0 ]]; then + echo "release_commit can lag the workflow commit only across release-tooling changes." >&2 + echo "These intervening paths are not release tooling:" >&2 + printf ' %s\n' "${disallowed[@]}" >&2 + exit 2 + fi + + uses_temporary_target_branch=true + target_branch="release-target/${release_sha:0:12}-${GITHUB_RUN_ID}" +fi + +{ + echo "sha=$release_sha" + echo "workflow_sha=$workflow_sha" + echo "target_branch=$target_branch" + echo "uses_temporary_target_branch=$uses_temporary_target_branch" +} >> "$GITHUB_OUTPUT" +echo "RELEASE_HEAD_SHA=$release_sha" >> "$GITHUB_ENV" + +echo "workflow commit: $workflow_sha" +echo "release commit: $release_sha" +echo "release-please target branch: $target_branch" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3aa529ff..99f112b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,11 +13,11 @@ on: - prepare-release-pr - publish-dry-run - publish - replace_conflicting_assets: - description: Replace existing GitHub release assets with different bytes during an intentional publish repair + release_commit: + description: Optional full commit SHA to publish/dry-run instead of the workflow commit required: false - type: boolean - default: false + type: string + default: "" permissions: contents: read @@ -225,6 +225,12 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Resolve release commit + id: release_head + env: + INPUT_RELEASE_COMMIT: ${{ inputs.release_commit }} + run: .github/scripts/resolve-release-head.sh + - name: Set up Moon uses: ./.github/actions/setup-moon @@ -274,21 +280,38 @@ jobs: NODE pnpm --version + - name: Create release-please target branch + if: ${{ inputs.operation == 'publish' && steps.release_head.outputs.uses_temporary_target_branch == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh auth setup-git + git push origin "${RELEASE_HEAD_SHA}:refs/heads/${{ steps.release_head.outputs.target_branch }}" --force + - name: Create release-please GitHub releases id: release_please if: ${{ inputs.operation == 'publish' }} uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 with: token: ${{ secrets.GITHUB_TOKEN }} - target-branch: main + target-branch: ${{ steps.release_head.outputs.target_branch }} config-file: release-please-config.json manifest-file: .release-please-manifest.json skip-github-pull-request: true + - name: Remove release-please target branch + if: ${{ always() && inputs.operation == 'publish' && steps.release_head.outputs.uses_temporary_target_branch == 'true' }} + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh auth setup-git + git push origin ":refs/heads/${{ steps.release_head.outputs.target_branch }}" + - name: Plan product releases id: release_plan run: | - tools/release/release.py plan --from-product-tags --include-current-tags --head-ref HEAD --format github-output >> "$GITHUB_OUTPUT" + tools/release/release.py plan --from-product-tags --include-current-tags --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" - name: No package release planned if: ${{ steps.release_plan.outputs.has_release_changes != 'true' }} @@ -307,13 +330,13 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_GPG_PASSPHRASE }} run: tools/release/check_publish_environment.py --products-json "${PRODUCTS_JSON}" - - name: Require same-SHA CI build gate + - name: Require release-commit CI build gate id: ci_build_gate if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} - run: bash .github/scripts/require-workflow-success.sh CI "$GITHUB_SHA" 7200 --job Builds + run: bash .github/scripts/require-workflow-success.sh CI "$RELEASE_HEAD_SHA" 7200 --job Builds - name: Require exact-extension package build artifacts if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.has_extension_products == 'true' }} @@ -324,7 +347,7 @@ jobs: run: | bash .github/scripts/require-workflow-success.sh \ CI \ - "$GITHUB_SHA" \ + "$RELEASE_HEAD_SHA" \ 7200 \ --run-id "${CI_RUN_ID}" \ --job Builds \ @@ -342,22 +365,22 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} - run: tools/release/release.py check-registries --products-json "${PRODUCTS_JSON}" --head-ref HEAD + run: tools/release/release.py check-registries --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Check existing WASIX runtime release tag if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} id: wasix_runtime_existing_tag - run: tools/release/release.py publish --product liboliphaunt-wasix --step existing-tag --head-ref HEAD --format github-output >> "$GITHUB_OUTPUT" + run: tools/release/release.py publish --product liboliphaunt-wasix --step existing-tag --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" - name: Check existing WASIX Rust binding release tag if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_wasix_rust == 'true' }} id: wasix_rust_existing_tag - run: tools/release/release.py publish --product oliphaunt-wasix-rust --step existing-tag --head-ref HEAD --format github-output >> "$GITHUB_OUTPUT" + run: tools/release/release.py publish --product oliphaunt-wasix-rust --step existing-tag --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" - name: Check existing Rust SDK release tag if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_rust == 'true' }} id: rust_existing_tag - run: tools/release/release.py publish --product oliphaunt-rust --step existing-tag --head-ref HEAD --format github-output >> "$GITHUB_OUTPUT" + run: tools/release/release.py publish --product oliphaunt-rust --step existing-tag --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" - name: Download WASIX runtime build artifacts if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} @@ -375,7 +398,7 @@ jobs: run: | .github/scripts/download-build-artifacts.sh \ CI \ - "$GITHUB_SHA" \ + "$RELEASE_HEAD_SHA" \ target/oliphaunt-wasix/release-assets \ --run-id "$CI_RUN_ID" \ --job Builds \ @@ -390,7 +413,7 @@ jobs: run: | .github/scripts/download-build-artifacts.sh \ CI \ - "$GITHUB_SHA" \ + "$RELEASE_HEAD_SHA" \ target/extension-artifacts \ --run-id "$CI_RUN_ID" \ --job Builds \ @@ -414,7 +437,7 @@ jobs: local artifact="$2" .github/scripts/download-build-artifacts.sh \ CI \ - "$GITHUB_SHA" \ + "$RELEASE_HEAD_SHA" \ "target/sdk-artifacts/$product" \ --run-id "$CI_RUN_ID" \ --job Builds \ @@ -436,7 +459,7 @@ jobs: run: | .github/scripts/download-build-artifacts.sh \ CI \ - "$GITHUB_SHA" \ + "$RELEASE_HEAD_SHA" \ target/liboliphaunt/release-assets \ --run-id "$CI_RUN_ID" \ --job Builds \ @@ -468,7 +491,7 @@ jobs: local destination="$2" .github/scripts/download-build-artifacts.sh \ CI \ - "$GITHUB_SHA" \ + "$RELEASE_HEAD_SHA" \ "$destination" \ --run-id "$CI_RUN_ID" \ --job Builds \ @@ -497,7 +520,7 @@ jobs: run: | .github/scripts/download-build-artifacts.sh \ CI \ - "$GITHUB_SHA" \ + "$RELEASE_HEAD_SHA" \ target/oliphaunt-node-direct/npm-packages \ --run-id "$CI_RUN_ID" \ --job Builds \ @@ -512,20 +535,20 @@ jobs: OLIPHAUNT_BROKER_RELEASE_ASSET_INPUT_DIRS: ${{ github.workspace }}/target/oliphaunt-broker/release-assets OLIPHAUNT_NODE_ADDON_ASSET_INPUT_DIRS: ${{ github.workspace }}/target/oliphaunt-node-direct/release-assets PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} - run: tools/release/release.py publish-dry-run --products-json "${PRODUCTS_JSON}" --head-ref HEAD + run: tools/release/release.py publish-dry-run --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Publish liboliphaunt GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish ${{ inputs.replace_conflicting_assets && '--replace-conflicting-assets' || '' }} --product liboliphaunt-native --step github-release-assets --head-ref HEAD + run: tools/release/release.py publish --product liboliphaunt-native --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" - name: Publish selected extension GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PRODUCTS_JSON: ${{ steps.release_plan.outputs.extension_products_json }} - run: tools/release/release.py publish ${{ inputs.replace_conflicting_assets && '--replace-conflicting-assets' || '' }} --step github-release-assets --products-json "${PRODUCTS_JSON}" --head-ref HEAD + run: tools/release/release.py publish --step github-release-assets --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Attest selected extension release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' }} @@ -554,7 +577,7 @@ jobs: if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_swift == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-swift --step github-release --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-swift --step github-release --head-ref "$RELEASE_HEAD_SHA" - name: Publish Kotlin SDK to Maven Central if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_kotlin == 'true' }} @@ -565,38 +588,38 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.MAVEN_GPG_KEY_ID }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - run: tools/release/release.py publish --product oliphaunt-kotlin --step maven-central --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-kotlin --step maven-central --head-ref "$RELEASE_HEAD_SHA" - name: Publish React Native package to npm if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_react_native == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-react-native --step npm --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-react-native --step npm --head-ref "$RELEASE_HEAD_SHA" - name: Publish WASIX runtime crates to crates.io if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product liboliphaunt-wasix --step crates-io --head-ref HEAD + run: tools/release/release.py publish --product liboliphaunt-wasix --step crates-io --head-ref "$RELEASE_HEAD_SHA" - name: Publish WASIX Rust binding to crates.io if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_wasix_rust == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-wasix-rust --step crates-io --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-wasix-rust --step crates-io --head-ref "$RELEASE_HEAD_SHA" - name: Publish Rust SDK to crates.io if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_rust == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-rust --step crates-io --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-rust --step crates-io --head-ref "$RELEASE_HEAD_SHA" - name: Publish broker GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_broker == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} OLIPHAUNT_BROKER_RELEASE_ASSET_INPUT_DIRS: ${{ github.workspace }}/target/oliphaunt-broker/release-assets - run: tools/release/release.py publish ${{ inputs.replace_conflicting_assets && '--replace-conflicting-assets' || '' }} --product oliphaunt-broker --step github-release-assets --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-broker --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" - name: Attest broker release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_broker == 'true' }} @@ -612,7 +635,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} OLIPHAUNT_NODE_ADDON_ASSET_INPUT_DIRS: ${{ github.workspace }}/target/oliphaunt-node-direct/release-assets - run: tools/release/release.py publish ${{ inputs.replace_conflicting_assets && '--replace-conflicting-assets' || '' }} --product oliphaunt-node-direct --step github-release-assets --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-node-direct --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" - name: Attest Node direct release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_node_direct == 'true' }} @@ -627,19 +650,19 @@ jobs: if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_node_direct == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-node-direct --step npm --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-node-direct --step npm --head-ref "$RELEASE_HEAD_SHA" - name: Publish TypeScript packages to npm and JSR if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_js == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-js --step npm-jsr --head-ref HEAD + run: tools/release/release.py publish --product oliphaunt-js --step npm-jsr --head-ref "$RELEASE_HEAD_SHA" - name: Upload WASIX GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish ${{ inputs.replace_conflicting_assets && '--replace-conflicting-assets' || '' }} --product liboliphaunt-wasix --step github-release-assets --head-ref HEAD + run: tools/release/release.py publish --product liboliphaunt-wasix --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" - name: Attest WASIX release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} @@ -658,7 +681,7 @@ jobs: run: | gh auth setup-git git fetch --force --tags origin - tools/release/release.py verify-release --products-json "${PRODUCTS_JSON}" --head-ref HEAD + tools/release/release.py verify-release --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Run consumer shape gates if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index 0d93a215..b8806da9 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -85,6 +85,21 @@ tools/release/release.py plan --from-product-tags --include-current-tags --head- tools/release/release.py check ``` +For normal releases, leave the `Release` workflow's `release_commit` input +empty. If a publish or dry-run fails and a later `main` commit only fixes +release tooling, rerun `Release` from current `main` with `release_commit` set +to the full 40-character SHA that should be published. The workflow still runs +the latest release scripts, but it plans the release, selects CI artifacts, +checks product tags, and verifies publication against that selected release +commit. During `publish`, the workflow creates a temporary release-please target +branch at the selected commit so product tags and GitHub releases point at the +published source commit, then removes that temporary branch. + +Do not use `release_commit` to skip CI for product source, version, changelog, +or release metadata changes. The workflow rejects lagging release commits unless +the intervening files are release tooling such as `.github/workflows/`, +`.github/scripts/`, `tools/release/`, `tools/policy/`, or `tools/xtask/`. + ## crates.io Products: diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index 406ba171..0af59cc0 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610", + "sourceDigest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 5a326f2c..07000659 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610" + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:c836712ccd4a1f5d8c51092bd607e0e43d2968d76d8126cdb9252e9a84b1f610", + "source-digest": "sha256:27564e421f14e35ac3dc4ca476c4a8e5ab6d246608bdd0d5ffd185c5864fddb1", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 0b1c1a4f..3bbecf6c 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -7b87d6512c9f965d4147dd3027f20ead7b21b9dc34cc704583a1ce675df5d18e +a373e498434491b73068359005fd1b17d99095b02b11c298483bcf61e0fc18c2 diff --git a/tools/policy/assertions/assert-ci-workflows.mjs b/tools/policy/assertions/assert-ci-workflows.mjs index 69548a96..7b946dab 100755 --- a/tools/policy/assertions/assert-ci-workflows.mjs +++ b/tools/policy/assertions/assert-ci-workflows.mjs @@ -318,9 +318,9 @@ assertCheckoutRef(mobileBlocks, 'ios', mobileArtifactRef); rejectText(releasePath, 'require-workflow-success.sh Builds'); rejectText(releasePath, 'artifact-builders'); rejectText(releasePath, 'BUILDS_RUN_ID'); -requireText(releasePath, 'Require same-SHA CI build gate'); +requireText(releasePath, 'Require release-commit CI build gate'); requireText(releasePath, 'id: ci_build_gate'); -requireText(releasePath, 'require-workflow-success.sh CI "$GITHUB_SHA" 7200 --job Builds'); +requireText(releasePath, 'require-workflow-success.sh CI "$RELEASE_HEAD_SHA" 7200 --job Builds'); requireText(releasePath, 'CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }}'); requireText(releasePath, '--job Builds'); diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index ff86d400..57be891c 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -139,6 +139,11 @@ def assert_contains(path: str, snippet: str, message: str) -> None: fail(message) +def assert_not_contains(path: str, snippet: str, message: str) -> None: + if snippet in read_text(path): + fail(message) + + def workflow_job_blocks(path: str) -> dict[str, str]: text = read_text(path) jobs_section = text.split("\njobs:\n", 1)[1] if "\njobs:\n" in text else "" @@ -600,10 +605,30 @@ def check_ci_policy() -> None: "check_release_pr_coverage.py", "release checks must verify release-please version bumps cover Moon-selected products", ) - assert_contains( + for path in ( + ".github/workflows/release.yml", "tools/release/release.py", - "--replace-conflicting-assets", - "validated release publish entry point must expose intentional GitHub asset repair", + "tools/release/upload_github_release_assets.py", + ): + assert_not_contains( + path, + "replace_conflicting_assets", + "GitHub release asset replacement must stay a manual repair, not a release workflow switch", + ) + assert_not_contains( + path, + "replace-conflicting-assets", + "GitHub release asset replacement must stay a manual repair, not a release CLI switch", + ) + assert_not_contains( + "tools/release/upload_github_release_assets.py", + "--clobber", + "GitHub release asset upload must not overwrite existing assets", + ) + assert_contains( + "tools/release/upload_github_release_assets.py", + "delete the conflicting GitHub release asset manually", + "GitHub release asset byte conflicts must fail with manual repair guidance", ) @@ -623,21 +648,25 @@ def check_release_workflow_policy() -> None: if permission not in publish_block: fail(f"Release publish job must declare {permission}") release_workflow = read_text(".github/workflows/release.yml") - repair_expression = "inputs.replace_conflicting_assets && '--replace-conflicting-assets' || ''" - repair_interpolation = "${{ " + repair_expression + " }}" for snippet in ( - "replace_conflicting_assets:", - repair_expression, - f"publish {repair_interpolation} --product liboliphaunt-native --step github-release-assets", - f"publish {repair_interpolation} --step github-release-assets --products-json", + "release_commit:", + ".github/scripts/resolve-release-head.sh", + "id: release_head", + "RELEASE_HEAD_SHA", + "Create release-please target branch", + "target-branch: ${{ steps.release_head.outputs.target_branch }}", + "Remove release-please target branch", ): if snippet not in release_workflow: - fail(f"Release workflow must expose intentional GitHub asset repair: missing {snippet!r}") + fail(f"Release workflow must resolve and publish from an explicit release commit: missing {snippet!r}") assert_text_order( publish_block, [ - "Require same-SHA CI build gate", + "Resolve release commit", + "Create release-please GitHub releases", + "Plan product releases", + "Require release-commit CI build gate", "Download WASIX runtime build artifacts", "Download WASIX release assets", "Download exact-extension package artifacts", @@ -647,12 +676,12 @@ def check_release_workflow_policy() -> None: "Download Node direct optional npm packages", "Validate selected release product dry-runs", ], - "Release dry-run must validate same-SHA builder outputs before product dry-runs", + "Release dry-run must validate release-commit builder outputs before product dry-runs", ) for snippet in ( "id: ci_build_gate", - 'require-workflow-success.sh CI "$GITHUB_SHA" 7200 --job Builds', + 'require-workflow-success.sh CI "$RELEASE_HEAD_SHA" 7200 --job Builds', "CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }}", "--run-id \"$CI_RUN_ID\"", "--run-id \"${CI_RUN_ID}\"", @@ -671,6 +700,7 @@ def check_release_workflow_policy() -> None: "target/oliphaunt-broker/release-assets", "target/oliphaunt-node-direct/release-assets", "tools/release/release.py publish-dry-run --products-json", + '--head-ref "$RELEASE_HEAD_SHA"', ): if snippet not in publish_block: fail(f"Release workflow dry-run handoff is missing {snippet!r}") @@ -686,10 +716,10 @@ def check_release_workflow_policy() -> None: end_candidates = [candidate for candidate in (next_call, next_step) if candidate != -1] end = min(end_candidates) if end_candidates else len(publish_block) call_text = normalized_shell(publish_block[call.start():end]) - # Every release artifact download must come from the same-SHA CI + # Every release artifact download must come from the selected release # workflow and the builds aggregate, even when wrapped in shell # helper functions. - for required in ("CI", '"$GITHUB_SHA"', "--run-id", "--job Builds", "--artifact"): + for required in ("CI", '"$RELEASE_HEAD_SHA"', "--run-id", "--job Builds", "--artifact"): if required not in call_text: fail(f"Release artifact download must require {required}: {call_text[:240]}") @@ -708,13 +738,30 @@ def check_release_workflow_policy() -> None: if snippet not in require_workflow_script: fail(f"CI build gate must emit and validate selected run ids: missing {snippet!r}") + release_head_script = read_text(".github/scripts/resolve-release-head.sh") + for snippet in ( + "INPUT_RELEASE_COMMIT", + "40-character commit SHA", + "git merge-base --is-ancestor", + "release-target/", + "release-tooling changes", + ".github/workflows/*", + "tools/release/*", + "tools/xtask/*", + "RELEASE_HEAD_SHA", + ): + if snippet not in release_head_script: + fail(f"release commit resolver must pin safe publish-from-commit behavior: missing {snippet!r}") + wasix_download_script = read_text(".github/scripts/download-wasix-runtime-build-artifacts.sh") - for snippet in ("CI_RUN_ID", '--run-id "$CI_RUN_ID"', "--required-job Builds"): + for snippet in ("RELEASE_HEAD_SHA", "CI_RUN_ID", '--run-id "$CI_RUN_ID"', "--required-job Builds"): if snippet not in wasix_download_script: fail(f"WASIX runtime artifact handoff must consume the selected CI run id: missing {snippet!r}") guarded_publish_steps = { + "Create release-please target branch", "Create release-please GitHub releases", + "Remove release-please target branch", "Publish liboliphaunt GitHub release assets", "Publish selected extension GitHub release assets", "Attest selected extension release assets", diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index a944baed..54293d1a 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -952,8 +952,8 @@ def validate_ci_release_artifacts() -> None: ) require_text( ".github/workflows/release.yml", - "require-workflow-success.sh CI \"$GITHUB_SHA\" 7200 --job Builds", - "release workflow must require the same-SHA CI artifact builder gate instead of the whole workflow conclusion", + "require-workflow-success.sh CI \"$RELEASE_HEAD_SHA\" 7200 --job Builds", + "release workflow must require the selected release commit CI artifact builder gate instead of the whole workflow conclusion", ) require_text( ".github/workflows/release.yml", @@ -968,7 +968,7 @@ def validate_ci_release_artifacts() -> None: require_text( "tools/xtask/src/asset_io.rs", "run_has_required_job_success", - "xtask WASIX artifact downloads must support filtering same-SHA runs by required builder job", + "xtask WASIX artifact downloads must support filtering selected release runs by required builder job", ) if release.index("Download SDK package artifacts") > release.index("Validate selected release product dry-runs"): fail("release workflow must stage SDK artifacts before selected release product dry-runs") diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 32da9017..ff4c7332 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -116,6 +116,9 @@ def validate_release_setup_docs() -> None: "oliphaunt-broker", "consumer-shape --require-ready --products-json ''", "check-registries --products-json '' --head-ref HEAD", + "release_commit", + "full 40-character SHA that should be published", + "The workflow still runs the latest release scripts", "For the first public release, select every product", "manually bootstrap any first Cargo crates", "Manual registry bootstrap is a release-completion state", diff --git a/tools/release/release.py b/tools/release/release.py index 4f18d612..e70daaa8 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -24,7 +24,6 @@ ROOT = Path(__file__).resolve().parents[2] EXTENSION_PRODUCT_PREFIX = "oliphaunt-extension-" -REPLACE_CONFLICTING_GITHUB_ASSETS = False NODE_DIRECT_PACKAGE_DIRS = { "@oliphaunt/node-direct-darwin-arm64": ROOT / "src/runtimes/node-direct/packages/darwin-arm64", "@oliphaunt/node-direct-linux-x64-gnu": ROOT / "src/runtimes/node-direct/packages/linux-x64-gnu", @@ -437,8 +436,6 @@ def upload_github_release_assets(product: str, *, tag: str | None = None, assets ] for asset in assets or []: command.extend(["--asset", asset]) - if REPLACE_CONFLICTING_GITHUB_ASSETS: - command.append("--replace-conflicting-assets") run(command) @@ -1478,8 +1475,6 @@ def command_publish_dry_run(args: argparse.Namespace, passthrough: list[str]) -> def command_publish(args: argparse.Namespace, passthrough: list[str]) -> None: - global REPLACE_CONFLICTING_GITHUB_ASSETS - REPLACE_CONFLICTING_GITHUB_ASSETS = args.replace_conflicting_assets products = selected_products_from_passthrough(passthrough) if args.step == "github-release-assets" and not args.product and selected_extension_products(products): publish_selected_extension_release_assets(products, args.head_ref) @@ -1511,11 +1506,6 @@ def main(argv: list[str]) -> int: publish.add_argument("--step") publish.add_argument("--head-ref", default="HEAD") publish.add_argument("--format", choices=["text", "github-output"], default="text") - publish.add_argument( - "--replace-conflicting-assets", - action="store_true", - help="replace existing GitHub release assets whose bytes differ after normal release validation", - ) args, passthrough = parser.parse_known_args(argv) command = args.command diff --git a/tools/release/upload_github_release_assets.py b/tools/release/upload_github_release_assets.py index 136be7eb..b0a0aec1 100755 --- a/tools/release/upload_github_release_assets.py +++ b/tools/release/upload_github_release_assets.py @@ -81,8 +81,6 @@ def upload_release_assets( tag: str, repo: str, assets: list[str], - *, - replace_conflicting_assets: bool, ) -> None: if not release_exists(tag, repo): fail( @@ -114,14 +112,12 @@ def upload_release_assets( if local_sha == remote_sha: print(f"{product} GitHub release {tag} already has identical asset {asset_name}; skipping.") continue - if not replace_conflicting_assets: - fail( - f"{product} GitHub release {tag} already has different bytes for {asset_name}; " - "rerun with --replace-conflicting-assets only for an intentional repair" - ) - upload_assets.append(asset) + fail( + f"{product} GitHub release {tag} already has different bytes for {asset_name}; " + "delete the conflicting GitHub release asset manually before rerunning an intentional repair" + ) if upload_assets: - run_gh(["release", "upload", tag, *upload_assets, "--clobber", "--repo", repo]) + run_gh(["release", "upload", tag, *upload_assets, "--repo", repo]) else: print(f"{product} GitHub release {tag} already has all requested assets with matching checksums.") else: @@ -143,11 +139,6 @@ def parse_args(argv: list[str]) -> argparse.Namespace: default=[], help="asset file to upload; may be passed more than once", ) - parser.add_argument( - "--replace-conflicting-assets", - action="store_true", - help="replace existing GitHub release assets when their bytes differ from local staged assets", - ) return parser.parse_args(argv) @@ -164,7 +155,6 @@ def main(argv: list[str]) -> int: tag=args.tag or default_tag(args.product), repo=args.repo, assets=assets, - replace_conflicting_assets=args.replace_conflicting_assets, ) return 0 diff --git a/tools/xtask/src/asset_io.rs b/tools/xtask/src/asset_io.rs index 7cc5b8a6..66376377 100644 --- a/tools/xtask/src/asset_io.rs +++ b/tools/xtask/src/asset_io.rs @@ -235,6 +235,7 @@ fn download_assets_from_run(run_id: &str, targets: &[String]) -> Result<()> { target_download_dir.to_str().expect("download dir is utf-8"), ], )?; + normalize_downloaded_aot_artifact(target, &target_download_dir)?; } verify_downloaded_asset_fingerprint(&download_dir)?; install_downloaded_artifacts(&download_dir, targets)?; @@ -244,6 +245,47 @@ fn download_assets_from_run(run_id: &str, targets: &[String]) -> Result<()> { Ok(()) } +fn normalize_downloaded_aot_artifact(target: &str, artifact_dir: &Path) -> Result<()> { + let marker = artifact_dir.join("target-triple.txt"); + let files = artifact_dir.join("files"); + if !marker.exists() && !files.exists() { + return Ok(()); + } + + ensure_file(&marker)?; + ensure!( + files.is_dir(), + "downloaded AOT artifact envelope is missing files directory: {}", + files.display() + ); + let actual = fs::read_to_string(&marker) + .with_context(|| format!("read {}", marker.display()))? + .trim() + .to_owned(); + ensure_eq( + &actual, + target, + "downloaded AOT artifact target-triple marker", + )?; + + let normalized = artifact_dir.with_extension("normalized"); + if normalized.exists() { + fs::remove_dir_all(&normalized) + .with_context(|| format!("remove {}", normalized.display()))?; + } + copy_dir_all(&files, &normalized)?; + fs::remove_dir_all(artifact_dir) + .with_context(|| format!("remove {}", artifact_dir.display()))?; + fs::rename(&normalized, artifact_dir).with_context(|| { + format!( + "rename normalized AOT artifact {} -> {}", + normalized.display(), + artifact_dir.display() + ) + })?; + Ok(()) +} + fn download_assets_from_release(tag: &str, targets: &[String]) -> Result<()> { let download_dir = Path::new("target/oliphaunt-wasix/downloads").join(format!("release-{tag}")); if download_dir.exists() {