返回 DeepSeek-Reasonix
verify-stable-release-artifacts.sh
根目录 / scripts / verify-stable-release-artifacts.sh
1 #!/usr/bin/env bash
2 # Verify that a stable orchestration produced every public release channel.
3 set -euo pipefail
4
5 repository="${RELEASE_REPOSITORY:?RELEASE_REPOSITORY is required}"
6 version="${RELEASE_VERSION:?RELEASE_VERSION is required}"
7 cli_tag="${CLI_TAG:?CLI_TAG is required}"
8 desktop_tag="${DESKTOP_TAG:?DESKTOP_TAG is required}"
9 script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
10 attempts="${VERIFY_ATTEMPTS:-6}"
11 delay="${VERIFY_DELAY_SECONDS:-10}"
12 verify_pointers="${VERIFY_PUBLIC_POINTERS:-false}"
13 operation="${RELEASE_OPERATION:-publish}"
14 ledger_output="${RELEASE_LEDGER_OUTPUT:-}"
15 # A frozen 1.x line publishes npm under the legacy-v1 dist-tag and leaves the
16 # latest tag, the Homebrew cask and the CLI pointers to the line that owns them.
17 frozen=false
18 npm_tag=latest
19 if [ "${CLI_PUBLISH_FROZEN:-}" = "true" ]; then
20 frozen=true
21 npm_tag=legacy-v1
22 fi
23
24 if [[ ! "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
25 echo "::error::RELEASE_VERSION must be stable semver, got: $version" >&2
26 exit 1
27 fi
28 if [ "$cli_tag" != "v$version" ] || [ "$desktop_tag" != "desktop-v$version" ]; then
29 echo "::error::release tags do not match version $version: cli=$cli_tag desktop=$desktop_tag" >&2
30 exit 1
31 fi
32 case "$operation" in publish | recover) ;; *) echo "::error::RELEASE_OPERATION must be publish or recover" >&2; exit 1 ;; esac
33
34 release_git_url="https://github.com/${repository}.git"
35 cli_sha="$(git ls-remote --tags --refs "$release_git_url" "refs/tags/$cli_tag" | awk 'NR == 1 {print $1}')"
36 npm_sha="$(git ls-remote --tags --refs "$release_git_url" "refs/tags/npm-v$version" | awk 'NR == 1 {print $1}')"
37 desktop_sha="$(git ls-remote --tags --refs "$release_git_url" "refs/tags/$desktop_tag" | awk 'NR == 1 {print $1}')"
38 if [ -z "$cli_sha" ] || [ "$cli_sha" != "$npm_sha" ] || [ "$cli_sha" != "$desktop_sha" ]; then
39 echo "::error::release tags are missing or do not identify one immutable commit" >&2
40 exit 1
41 fi
42 if [ -n "${RELEASE_EXPECTED_SHA:-}" ] && [ "$cli_sha" != "$RELEASE_EXPECTED_SHA" ]; then
43 echo "::error::public release identity differs from the verified source SHA" >&2
44 exit 1
45 fi
46
47 tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/reasonix-release-postflight.XXXXXX")"
48 cleanup() {
49 case "$tmp_dir" in
50 */reasonix-release-postflight.*) rm -rf -- "$tmp_dir" ;;
51 *) echo "refusing to clean unexpected postflight directory: $tmp_dir" >&2 ;;
52 esac
53 }
54 trap cleanup EXIT
55
56 # The Desktop updater pointer and the Homebrew cask are product channels. The
57 # contents API reads the tap's branch head; raw.githubusercontent.com is cached.
58 verify_pointers() {
59 local manifest="$1"
60 local cask="$tmp_dir/reasonix.rb"
61 local homebrew_version=frozen
62 jq -e --arg version "v$version" '
63 .version == $version and
64 ([.platforms[], (.native_packages // {})[], (.downloads // {})[]] |
65 all(.url | type == "string" and startswith("https://dl.reasonix.io/desktop-" + $version + "/")))
66 ' "$manifest" >/dev/null
67 if [ "$frozen" != "true" ]; then
68 gh api -H 'Accept: application/vnd.github.raw' \
69 repos/esengine/homebrew-reasonix/contents/Casks/reasonix.rb >"$cask"
70 homebrew_version="$(sed -nE "s/^[[:space:]]*version ['\"]([^'\"]+)['\"].*/\1/p" "$cask" | head -n 1)"
71 fi
72 node "$script_dir/release-publication-ledger.mjs" pointers "$version" "$cli_sha" "$operation" \
73 "$manifest" "$homebrew_version" "$tmp_dir/pointer-ledger.json"
74 }
75
76 gh release view "$cli_tag" --repo "$repository" --json isDraft,isPrerelease,assets >"$tmp_dir/cli.json"
77 jq -e '
78 .isDraft == false and .isPrerelease == false and
79 ([.assets[].name] as $names |
80 ["SHA256SUMS", "reasonix-darwin-amd64.tar.gz", "reasonix-darwin-arm64.tar.gz",
81 "reasonix-linux-amd64.tar.gz", "reasonix-linux-arm64.tar.gz",
82 "reasonix-windows-amd64.zip", "reasonix-windows-arm64.zip"] |
83 all(. as $required | $names | index($required)))
84 ' "$tmp_dir/cli.json" >/dev/null
85
86 gh release view "$desktop_tag" --repo "$repository" --json isDraft,isPrerelease,assets >"$tmp_dir/desktop.json"
87 jq -e '
88 .isDraft == false and .isPrerelease == false and
89 ([.assets[].name] as $names |
90 ($names | index("latest.json")) and
91 (["Reasonix-darwin-arm64.dmg", "Reasonix-darwin-amd64.dmg",
92 "Reasonix-darwin-universal.dmg", "Reasonix-darwin-arm64.zip",
93 "Reasonix-darwin-amd64.zip", "Reasonix-linux-amd64.deb",
94 "Reasonix-linux-amd64.tar.gz", "Reasonix-windows-amd64-installer.exe",
95 "Reasonix-windows-amd64.zip", "Reasonix-windows-arm64-installer.exe"] |
96 all(. as $required | ($names | index($required)) and ($names | index($required + ".minisig")))))
97 ' "$tmp_dir/desktop.json" >/dev/null
98
99 if [ "${DESKTOP_MANUAL_ONLY:-false}" = "true" ]; then
100 bash "$script_dir/manual-desktop-exception.sh" validate "$desktop_tag"
101 # Neither updater entry point may serve the manual release: the exception
102 # publishes downloads without advancing automatic updates. Assert that
103 # invariant rather than one release's prior version.
104 gh_latest="$(gh api "repos/$repository/releases/latest" --jq .tag_name)"
105 if [ "$gh_latest" = "$desktop_tag" ]; then
106 echo "::error::GitHub latest advanced to the manual release $desktop_tag" >&2
107 exit 1
108 fi
109 bash "$script_dir/fetch-stable-release-manifest.sh" "$version" "$tmp_dir/desktop-pointer.json"
110 jq -e --arg v "v$version" '.version != $v' "$tmp_dir/desktop-pointer.json" >/dev/null
111 gh release view "$desktop_tag" --repo "$repository" --json body --jq .body | grep -F 'manual-download only'
112 fi
113
114 npm_names=(
115 "reasonix"
116 "@reasonix/cli-darwin-arm64"
117 "@reasonix/cli-darwin-x64"
118 "@reasonix/cli-linux-arm64"
119 "@reasonix/cli-linux-x64"
120 "@reasonix/cli-win32-arm64"
121 "@reasonix/cli-win32-x64"
122 )
123 for attempt in $(seq 1 "$attempts"); do
124 rm -rf "$tmp_dir/npm"
125 mkdir -p "$tmp_dir/npm"
126 visible=true
127 for index in "${!npm_names[@]}"; do
128 package="${npm_names[$index]}"
129 raw="$tmp_dir/npm/$index.raw.json"
130 if ! npm view "$package@$version" name version reasonixCandidateSha gitHead dist.integrity "dist-tags.$npm_tag" --json >"$raw" 2>/dev/null; then
131 visible=false
132 continue
133 fi
134 jq --arg name "$package" --arg tag "$npm_tag" '
135 (."dist-tags.\($tag)" // ."dist-tags"[$tag]) as $tagged |
136 {
137 name: (.name // $name),
138 version,
139 reasonixCandidateSha,
140 gitHead,
141 integrity: (."dist.integrity" // .dist.integrity)
142 } + (if $tag == "latest" then {latest: $tagged} else {distTag: $tag, distTagVersion: $tagged} end)
143 ' "$raw" >"$tmp_dir/npm/$index.json"
144 jq -e --arg name "$package" --arg version "$version" --arg sha "$cli_sha" '
145 .name == $name and .version == $version and
146 ((.reasonixCandidateSha == null or .reasonixCandidateSha == $sha) and
147 (.gitHead == null or .gitHead == $sha) and
148 ((.reasonixCandidateSha // .gitHead) == $sha)) and
149 (.integrity | type == "string" and length > 0) and
150 ((.latest // .distTagVersion) | type == "string" and length > 0)
151 ' "$tmp_dir/npm/$index.json" >/dev/null || {
152 echo "::error::npm package identity differs from the release candidate: $package@$version" >&2
153 exit 1
154 }
155 done
156 if [ "$visible" = "true" ]; then
157 jq -s '.' "$tmp_dir"/npm/[0-9].json >"$tmp_dir/npm.json"
158 if node "$script_dir/release-publication-ledger.mjs" core "$version" "$cli_sha" "$operation" \
159 "$tmp_dir/cli.json" "$tmp_dir/desktop.json" "$tmp_dir/npm.json" "$tmp_dir/core-ledger.json"; then
160 if [ "$verify_pointers" = "true" ]; then
161 manifest="$tmp_dir/desktop-pointer.json"
162 bash "$script_dir/fetch-stable-release-manifest.sh" "$version" "$manifest"
163 owns_pointer="$(node "$script_dir/release-publication-ledger.mjs" pointer-owner "$version" "$operation" "$manifest")"
164 if [ "$owns_pointer" = true ]; then
165 verify_pointers "$manifest"
166 node "$script_dir/release-publication-ledger.mjs" merge "$tmp_dir/core-ledger.json" \
167 "$tmp_dir/pointer-ledger.json" "${ledger_output:-$tmp_dir/publication-ledger.json}"
168 else
169 echo "A verified newer Stable release owns the public pointers; recovered immutable v$version files only."
170 [ -z "$ledger_output" ] || cp "$tmp_dir/core-ledger.json" "$ledger_output"
171 fi
172 elif [ -n "$ledger_output" ]; then
173 cp "$tmp_dir/core-ledger.json" "$ledger_output"
174 fi
175 echo "stable release postflight OK: cli=$cli_tag desktop=$desktop_tag npm-packages=${#npm_names[@]}"
176 exit 0
177 fi
178 fi
179 echo "npm publication has not converged (attempt $attempt/$attempts)"
180 if [ "$attempt" -lt "$attempts" ]; then sleep "$delay"; fi
181 done
182
183 echo "::error::npm package set or public pointers did not converge for $version" >&2
184 exit 1
185
185 lines BASH