返回 DeepSeek-Reasonix
resolve-stable-release.sh
根目录 / scripts / resolve-stable-release.sh
1 #!/usr/bin/env bash
2 # Validate one stable release tag set and emit the values shared by all release
3 # workflows. Stable releases are deliberately all-or-nothing: the CLI, npm, and
4 # desktop tags must resolve to one commit. Normal publication accepts the Notes
5 # candidate after main-v2 advances beyond it; the protected workflow separately
6 # revalidates that candidate's Notes change and exact push CI. Recovery may also
7 # target an older commit while the control plane stays on current main-v2.
8 set -euo pipefail
9
10 release_tag="${RELEASE_TAG:?RELEASE_TAG is required}"
11 release_remote="${RELEASE_REMOTE:-origin}"
12 allow_recovery="${ALLOW_STABLE_RECOVERY:-false}"
13
14 case "$allow_recovery" in
15 true | false) ;;
16 *)
17 echo "::error::ALLOW_STABLE_RECOVERY must be true or false, got: $allow_recovery" >&2
18 exit 1
19 ;;
20 esac
21
22 if [[ ! "$release_tag" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
23 echo "::error::stable release tag must be vMAJOR.MINOR.PATCH, got: $release_tag" >&2
24 exit 1
25 fi
26
27 version="${release_tag#v}"
28 cli_tag="$release_tag"
29 npm_tag="npm-v${version}"
30 desktop_tag="desktop-v${version}"
31
32 checkout_sha="$(git rev-parse HEAD^{commit})"
33 main_sha="$(git ls-remote "$release_remote" refs/heads/main-v2 | awk 'NR == 1 { print $1 }')"
34 if [ -z "$main_sha" ]; then
35 echo "::error::cannot resolve $release_remote/main-v2" >&2
36 exit 1
37 fi
38
39 git fetch --quiet --no-tags "$release_remote" refs/heads/main-v2
40 if ! git merge-base --is-ancestor "$checkout_sha" "$main_sha"; then
41 echo "::error::release control checkout $checkout_sha is not on $release_remote/main-v2 history ($main_sha)" >&2
42 exit 1
43 fi
44
45 release_sha="$(
46 git ls-remote --tags "$release_remote" "refs/tags/$cli_tag" "refs/tags/$cli_tag^{}" |
47 awk '/\^\{\}$/ { print $1; found = 1; exit } NR == 1 { first = $1 } END { if (!found) print first }'
48 )"
49 if [ -z "$release_sha" ]; then
50 echo "::error::required stable release tag is missing: $cli_tag" >&2
51 exit 1
52 fi
53 git fetch --quiet --no-tags "$release_remote" "refs/tags/$cli_tag"
54 if ! git merge-base --is-ancestor "$release_sha" "$main_sha"; then
55 echo "::error::stable tag $cli_tag points to $release_sha, which is not an ancestor of $release_remote/main-v2 ($main_sha)" >&2
56 exit 1
57 fi
58 if [ "$release_sha" != "$main_sha" ]; then
59 mode="candidate"
60 [ "$allow_recovery" = "true" ] && mode="recovery"
61 echo "stable $mode: $cli_tag remains on main-v2 history at $release_sha; current main-v2 is $main_sha"
62 fi
63
64 for tag in "$cli_tag" "$npm_tag" "$desktop_tag"; do
65 # Prefer the peeled commit for annotated tags; lightweight tags only return
66 # the first line. Both forms are valid release refs.
67 tag_sha="$(
68 git ls-remote --tags "$release_remote" "refs/tags/$tag" "refs/tags/$tag^{}" |
69 awk '/\^\{\}$/ { print $1; found = 1; exit } NR == 1 { first = $1 } END { if (!found) print first }'
70 )"
71 if [ -z "$tag_sha" ]; then
72 echo "::error::required stable release tag is missing: $tag" >&2
73 exit 1
74 fi
75 if [ "$tag_sha" != "$release_sha" ]; then
76 echo "::error::$tag points to $tag_sha, expected $release_sha" >&2
77 exit 1
78 fi
79 done
80
81 output_file="${GITHUB_OUTPUT:-/dev/stdout}"
82 {
83 echo "version=$version"
84 echo "cli_tag=$cli_tag"
85 echo "npm_tag=$npm_tag"
86 echo "desktop_tag=$desktop_tag"
87 echo "sha=$release_sha"
88 } >>"$output_file"
89
90 echo "stable release resolved: version=$version sha=$release_sha"
91
91 lines BASH