From 0125c6c4f059b33028b643ab137473391febcba3 Mon Sep 17 00:00:00 2001 From: SivanCola <32437197+SivanCola@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:47:55 +0800 Subject: [PATCH] Add explicit Preview release recovery Problem: A failed Preview publisher cannot consume a workflow fix by rerunning the original Actions attempt, while the immutable Preview tag must not be moved to the repaired main-v2 commit. Root cause: The documented all-surface Preview recovery path still required the Preview tag to equal current main-v2, making recovery of an ancestor candidate impossible. Fix: Add an opt-in Preview recovery input, keep the control workflow pinned to protected current main-v2, allow only a historical Preview tag that remains on main-v2 history, and propagate the recovery authorization to the CLI publisher. Verification: - bash scripts/release-workflows.test.sh - bash -n scripts/resolve-preview-release.sh scripts/release-workflows.test.sh - git diff --check --- .github/workflows/release-preview.yml | 7 +++++++ .github/workflows/release.yml | 19 +++++++++++++++--- scripts/release-workflows.test.sh | 21 ++++++++++++++++++++ scripts/resolve-preview-release.sh | 28 +++++++++++++++++++++++---- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-preview.yml b/.github/workflows/release-preview.yml index f8a09ca5d3..afb8cc4855 100644 --- a/.github/workflows/release-preview.yml +++ b/.github/workflows/release-preview.yml @@ -11,6 +11,11 @@ on: description: "Existing protected CLI Preview tag, e.g. v1.19.0-preview.3" required: true type: string + recovery: + description: "Recover an existing partially published Preview from main-v2 history" + required: false + default: false + type: boolean concurrency: # Every Preview version advances the same public CLI, Desktop, and npm @@ -46,6 +51,7 @@ jobs: id: release env: RELEASE_TAG: ${{ inputs.tag }} + ALLOW_PREVIEW_RECOVERY: ${{ inputs.recovery }} run: bash scripts/resolve-preview-release.sh - name: Validate exact reviewed Preview notes env: @@ -123,6 +129,7 @@ jobs: approved_sha: ${{ needs.authorize.outputs.sha }} orchestrated: true orchestrator: preview + allow_preview_recovery: ${{ inputs.recovery }} secrets: inherit npm: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78cbf09fdc..3fdcb81d1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,11 @@ on: required: false default: stable type: string + allow_preview_recovery: + description: "Allow an approved Preview tag from main-v2 history" + required: false + default: false + type: boolean permissions: contents: write # create the release and upload archives @@ -92,6 +97,9 @@ jobs: env: RELEASE_TAG: ${{ steps.release.outputs.tag }} RELEASE_CHANNEL: ${{ steps.release.outputs.channel }} + IN_ORCHESTRATED: ${{ inputs.orchestrated }} + IN_ORCHESTRATOR: ${{ inputs.orchestrator }} + ALLOW_PREVIEW_RECOVERY: ${{ inputs.allow_preview_recovery }} run: | set -euo pipefail git fetch origin main-v2 @@ -100,9 +108,14 @@ jobs: echo "::error::$RELEASE_TAG points to $sha, which is not on main-v2 history" exit 1 fi - if [ "$RELEASE_CHANNEL" = "preview" ] && [ "$sha" != "$(git rev-parse origin/main-v2)" ]; then - echo "::error::CLI Preview must tag current main-v2; $RELEASE_TAG points to $sha" - exit 1 + if [ "$ALLOW_PREVIEW_RECOVERY" = "true" ]; then + if [ "$IN_ORCHESTRATED" != "true" ] || [ "$IN_ORCHESTRATOR" != "preview" ] || [ "$RELEASE_CHANNEL" != "preview" ]; then + echo "::error::Preview recovery requires the approved Preview orchestrator" + exit 1 + fi + elif [ "$RELEASE_CHANNEL" = "preview" ] && [ "$sha" != "$(git rev-parse origin/main-v2)" ]; then + echo "::error::CLI Preview must tag current main-v2; $RELEASE_TAG points to $sha" + exit 1 fi echo "sha=$sha" >> "$GITHUB_OUTPUT" - name: Verify existing protected tag diff --git a/scripts/release-workflows.test.sh b/scripts/release-workflows.test.sh index a4cefc2e3d..f29f061cee 100755 --- a/scripts/release-workflows.test.sh +++ b/scripts/release-workflows.test.sh @@ -41,6 +41,9 @@ if grep -Fq 'group: preview-release-${{ inputs.tag }}' "$repo_root/.github/workf exit 1 fi grep -Eq '^ environment: canary$' "$repo_root/.github/workflows/release-preview.yml" +grep -Eq '^ recovery:$' "$repo_root/.github/workflows/release-preview.yml" +grep -Fq 'ALLOW_PREVIEW_RECOVERY: ${{ inputs.recovery }}' "$repo_root/.github/workflows/release-preview.yml" +grep -Fq 'allow_preview_recovery: ${{ inputs.recovery }}' "$repo_root/.github/workflows/release-preview.yml" grep -Eq '^ signpath-preflight:$' "$repo_root/.github/workflows/release-preview.yml" grep -Eq 'signing_preflight: true' "$repo_root/.github/workflows/release-preview.yml" grep -Eq 'signing_preflight_verified: true' "$repo_root/.github/workflows/release-preview.yml" @@ -71,6 +74,8 @@ grep -Eq 'GORELEASER_CURRENT_TAG:.*needs\.resolve\.outputs\.tag' \ grep -Eq 'bash scripts/resolve-cli-release\.sh' "$repo_root/.github/workflows/release.yml" grep -Eq 'git merge-base --is-ancestor.*origin/main-v2' "$repo_root/.github/workflows/release.yml" grep -Eq 'CLI Preview must tag current main-v2' "$repo_root/.github/workflows/release.yml" +grep -Fq 'ALLOW_PREVIEW_RECOVERY: ${{ inputs.allow_preview_recovery }}' "$repo_root/.github/workflows/release.yml" +grep -Eq 'Preview recovery requires the approved Preview orchestrator' "$repo_root/.github/workflows/release.yml" grep -Eq "channel == 'stable'.*HOMEBREW_TAP_TOKEN" "$repo_root/.github/workflows/release.yml" grep -Eq "needs\.build\.result == 'success'" "$repo_root/.github/workflows/release-desktop.yml" grep -Eq "needs\.publish\.result == 'success'" "$repo_root/.github/workflows/release-desktop.yml" @@ -998,6 +1003,22 @@ git clone -q "$test_root/remote.git" "$test_root/repo" git commit --allow-empty -q -m "release workflow fix" git push -q origin main-v2 recovery_workflow_sha="$(git rev-parse HEAD)" + if RELEASE_TAG=v1.3.0-preview.42 \ + "$repo_root/scripts/resolve-preview-release.sh" >"$test_root/stale-preview.log" 2>&1; then + echo "stale Preview tag unexpectedly passed normal release resolution" >&2 + exit 1 + fi + grep -Eq 'must point to current .*main-v2' "$test_root/stale-preview.log" + GITHUB_OUTPUT="$test_root/preview-recovery.out" ALLOW_PREVIEW_RECOVERY=true \ + RELEASE_TAG=v1.3.0-preview.42 "$repo_root/scripts/resolve-preview-release.sh" + grep -Eq '^sha='"$approved_sha"'$' "$test_root/preview-recovery.out" + if ALLOW_PREVIEW_RECOVERY=invalid RELEASE_TAG=v1.3.0-preview.42 \ + "$repo_root/scripts/resolve-preview-release.sh" >"$test_root/invalid-preview-recovery.log" 2>&1; then + echo "invalid Preview recovery mode unexpectedly passed" >&2 + exit 1 + fi + grep -Eq 'ALLOW_PREVIEW_RECOVERY must be true or false' \ + "$test_root/invalid-preview-recovery.log" if GITHUB_OUTPUT="$test_root/desktop-stale-preview-candidate.out" \ RELEASE_CHANNEL=preview RELEASE_TAG=desktop-v1.3.0-preview.42 \ IN_ORCHESTRATED=false CALLER_EVENT_NAME=workflow_dispatch \ diff --git a/scripts/resolve-preview-release.sh b/scripts/resolve-preview-release.sh index fdc1efa646..554ee2e686 100755 --- a/scripts/resolve-preview-release.sh +++ b/scripts/resolve-preview-release.sh @@ -5,6 +5,15 @@ set -euo pipefail release_tag="${RELEASE_TAG:?RELEASE_TAG is required}" release_remote="${RELEASE_REMOTE:-origin}" +allow_recovery="${ALLOW_PREVIEW_RECOVERY:-false}" + +case "$allow_recovery" in +true | false) ;; +*) + echo "::error::ALLOW_PREVIEW_RECOVERY must be true or false, got: $allow_recovery" >&2 + exit 2 + ;; +esac if [[ ! "$release_tag" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-preview\.([1-9][0-9]*)$ ]]; then echo "::error::Preview release tag must be vMAJOR.MINOR.PATCH-preview.N, got: $release_tag" >&2 @@ -24,10 +33,21 @@ if [ -z "$main_sha" ] || [ "$checkout_sha" != "$main_sha" ]; then echo "::error::Preview control checkout must equal current $release_remote/main-v2 ($main_sha), got $checkout_sha" >&2 exit 1 fi -if [ -z "$tag_sha" ] || [ "$tag_sha" != "$main_sha" ]; then - echo "::error::$release_tag must point to current $release_remote/main-v2 ($main_sha), got ${tag_sha:-missing}" >&2 +if [ -z "$tag_sha" ]; then + echo "::error::$release_tag is missing from $release_remote" >&2 exit 1 fi +if [ "$tag_sha" != "$main_sha" ]; then + if [ "$allow_recovery" != "true" ]; then + echo "::error::$release_tag must point to current $release_remote/main-v2 ($main_sha), got $tag_sha" >&2 + exit 1 + fi + if ! git cat-file -e "$tag_sha^{commit}" 2>/dev/null || + ! git merge-base --is-ancestor "$tag_sha" "$main_sha"; then + echo "::error::Preview recovery tag $release_tag must remain on $release_remote/main-v2 history" >&2 + exit 1 + fi +fi { echo "version=${release_tag#v}" @@ -36,7 +56,7 @@ fi echo "cli_tag=$release_tag" echo "desktop_tag=desktop-v${base_version}-preview.${preview_number}" echo "npm_version=${base_version}-canary.${preview_number}" - echo "sha=$main_sha" + echo "sha=$tag_sha" } >>"${GITHUB_OUTPUT:-/dev/stdout}" -echo "Preview release resolved: ${release_tag#v} at $main_sha" +echo "Preview release resolved: ${release_tag#v} at $tag_sha (recovery=$allow_recovery)"