Files
DeepSeek-Reasonix/scripts/resolve-stable-release.sh
SivanCola ab24371d98 fix(release): bind stable tags to reviewed candidate
Problem:
The stable helper could consume immutable version tags if main-v2 advanced while exact-SHA CI or the protected relay was starting. Manual CLI and Desktop recovery also still accepted RC tags despite the official-only release contract.

Root cause:
The helper sampled main-v2 once, pushed only tag refs, and the orchestrator required the tags to equal the later branch tip. The candidate was not proven to be the commit that changed the target reviewed Notes record.

Fix:
Bind normal candidates to their reviewed Notes change, revalidate exact push CI from the protected control plane, include main-v2 as a guarded ref in the atomic tag transaction, and accept the immutable candidate after later protected branch advances. Restrict manual CLI and Desktop recovery to official versions while retaining historical reusable-workflow parsing.

Verification:
- bash scripts/release-stable.test.sh
- bash scripts/release-workflows.test.sh
- node --test scripts/release-notes.test.mjs
- actionlint on all active release workflows
- site contract tests, SignPath contract tests, Desktop control-plane test, cache guard, and git diff --check
2026-08-03 01:05:09 +08:00

91 lines
3.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Validate one stable release tag set and emit the values shared by all release
# workflows. Stable releases are deliberately all-or-nothing: the CLI, npm, and
# desktop tags must resolve to one commit. Normal publication accepts the Notes
# candidate after main-v2 advances beyond it; the protected workflow separately
# revalidates that candidate's Notes change and exact push CI. Recovery may also
# target an older commit while the control plane stays on current main-v2.
set -euo pipefail
release_tag="${RELEASE_TAG:?RELEASE_TAG is required}"
release_remote="${RELEASE_REMOTE:-origin}"
allow_recovery="${ALLOW_STABLE_RECOVERY:-false}"
case "$allow_recovery" in
true | false) ;;
*)
echo "::error::ALLOW_STABLE_RECOVERY must be true or false, got: $allow_recovery" >&2
exit 1
;;
esac
if [[ ! "$release_tag" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "::error::stable release tag must be vMAJOR.MINOR.PATCH, got: $release_tag" >&2
exit 1
fi
version="${release_tag#v}"
cli_tag="$release_tag"
npm_tag="npm-v${version}"
desktop_tag="desktop-v${version}"
checkout_sha="$(git rev-parse HEAD^{commit})"
main_sha="$(git ls-remote "$release_remote" refs/heads/main-v2 | awk 'NR == 1 { print $1 }')"
if [ -z "$main_sha" ]; then
echo "::error::cannot resolve $release_remote/main-v2" >&2
exit 1
fi
git fetch --quiet --no-tags "$release_remote" refs/heads/main-v2
if ! git merge-base --is-ancestor "$checkout_sha" "$main_sha"; then
echo "::error::release control checkout $checkout_sha is not on $release_remote/main-v2 history ($main_sha)" >&2
exit 1
fi
release_sha="$(
git ls-remote --tags "$release_remote" "refs/tags/$cli_tag" "refs/tags/$cli_tag^{}" |
awk '/\^\{\}$/ { print $1; found = 1; exit } NR == 1 { first = $1 } END { if (!found) print first }'
)"
if [ -z "$release_sha" ]; then
echo "::error::required stable release tag is missing: $cli_tag" >&2
exit 1
fi
git fetch --quiet --no-tags "$release_remote" "refs/tags/$cli_tag"
if ! git merge-base --is-ancestor "$release_sha" "$main_sha"; then
echo "::error::stable tag $cli_tag points to $release_sha, which is not an ancestor of $release_remote/main-v2 ($main_sha)" >&2
exit 1
fi
if [ "$release_sha" != "$main_sha" ]; then
mode="candidate"
[ "$allow_recovery" = "true" ] && mode="recovery"
echo "stable $mode: $cli_tag remains on main-v2 history at $release_sha; current main-v2 is $main_sha"
fi
for tag in "$cli_tag" "$npm_tag" "$desktop_tag"; do
# Prefer the peeled commit for annotated tags; lightweight tags only return
# the first line. Both forms are valid release refs.
tag_sha="$(
git ls-remote --tags "$release_remote" "refs/tags/$tag" "refs/tags/$tag^{}" |
awk '/\^\{\}$/ { print $1; found = 1; exit } NR == 1 { first = $1 } END { if (!found) print first }'
)"
if [ -z "$tag_sha" ]; then
echo "::error::required stable release tag is missing: $tag" >&2
exit 1
fi
if [ "$tag_sha" != "$release_sha" ]; then
echo "::error::$tag points to $tag_sha, expected $release_sha" >&2
exit 1
fi
done
output_file="${GITHUB_OUTPUT:-/dev/stdout}"
{
echo "version=$version"
echo "cli_tag=$cli_tag"
echo "npm_tag=$npm_tag"
echo "desktop_tag=$desktop_tag"
echo "sha=$release_sha"
} >>"$output_file"
echo "stable release resolved: version=$version sha=$release_sha"