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
This commit is contained in:
SivanCola committed 2026-08-01 11:47:55 +08:00
1 parent 1ec831e37c
commit 0125c6c4f0
4 files changed
+68 -7

No files matched your search

+7
View File
@@ -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:
+16 -3
View File
@@ -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
+21
View File
@@ -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 \
+24 -4
View File
@@ -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)"