返回 DeepSeek-Reasonix
release-workflows.test.sh
根目录 / scripts / release-workflows.test.sh
1 #!/usr/bin/env bash
2 set -euo pipefail
3
4 repo_root="$(git rev-parse --show-toplevel)"
5 node --test "$repo_root/scripts/verify-manual-desktop-producer.test.mjs"
6 node --test "$repo_root/scripts/release-publication-ledger.test.mjs"
7 node --test "$repo_root/scripts/release-cli-freeze.test.mjs"
8 bash "$repo_root/scripts/manual-desktop-exception.test.sh"
9 test_root="$(mktemp -d "${TMPDIR:-/tmp}/reasonix-release-workflow-test.XXXXXX")"
10 cleanup() {
11 case "$test_root" in
12 */reasonix-release-workflow-test.*) rm -rf -- "$test_root" ;;
13 *) echo "refusing to clean unexpected test directory: $test_root" >&2 ;;
14 esac
15 }
16 trap cleanup EXIT
17
18 # Stable tags have one entrypoint and one protected environment. Reusable
19 # publishers must verify that only that entrypoint can claim prior approval.
20 # The manual exception is immutable-candidate scoped and cannot advance any
21 # Desktop update entry point. Keep normal signing as the default.
22 python3 - "$repo_root" <<'PY'
23 import pathlib, sys
24 root = pathlib.Path(sys.argv[1])
25 stable = (root / '.github/workflows/release-stable.yml').read_text()
26 desktop = (root / '.github/workflows/release-desktop.yml').read_text()
27 publisher = (root / 'scripts/publish-desktop-github-release.sh').read_text()
28 exception = (root / 'scripts/manual-desktop-exception.sh').read_text()
29 for workflow in (stable, desktop):
30 block = workflow.split(' desktop_manual_only:', 1)[1].split('\n\n', 1)[0]
31 assert 'default: false' in block
32 # One owner decides which tags may skip Authenticode, so an exception
33 # cannot drift between the orchestrator, the publisher, and the release.
34 assert 'scripts/manual-desktop-exception.sh validate' in workflow
35 assert '7278072720a2dc7a31cce0eec18c1eacc149c0e0' in exception
36 verifier = (root / 'scripts/verify-stable-release-artifacts.sh').read_text()
37 # Postflight reads the same allowlist and asserts the update pointers never
38 # serve the manual release, instead of naming one release's prior version.
39 assert 'manual-desktop-exception.sh" validate "$desktop_tag"' in verifier
40 assert '1.38.8' not in verifier and 'v1.38.7' not in verifier
41 assert 'inputs.allow_recovery' in stable.split('name: Restrict manual Desktop distribution', 1)[1].split('- name:', 1)[0]
42 assert stable.count('desktop_manual_only: ${{ inputs.desktop_manual_only || false }}') == 2
43 assert "HAS_CERTUM: ${{ secrets.CERTUM_USERNAME != '' && secrets.CERTUM_OTP_URI != '' && secrets.CERTUM_KEY_ID != '' && !inputs.desktop_manual_only }}" in desktop
44 assert desktop.index('name: Validate signing mode') < desktop.index('name: Build and package')
45 assert "inputs.orchestrated }}\" != \"true\"" in desktop
46 assert 'manual-download only' in desktop
47 # The public disclosure names the release it belongs to and normalizes the
48 # v prefix, so it can never print a doubled version or a stale one.
49 disclosure = desktop.split('name: Disclose manual Desktop distribution', 1)[1].split('- name:', 1)[0]
50 assert 'version="v${MANUAL_VERSION#v}"' in disclosure
51 assert 'This ${version} desktop release' in disclosure
52 assert 'v${MANUAL_VERSION}' not in disclosure
53 manual_exit = desktop.index('if [ "$DESKTOP_MANUAL_ONLY" = "true" ]; then', desktop.index('name: Mirror immutable assets'))
54 assert manual_exit < desktop.index('validate_current_pointer()', manual_exit)
55 assert 'pointer_moved=false' in desktop[manual_exit:manual_exit + 350]
56 attach = desktop.split('name: Attach desktop manifest to matching CLI release', 1)[1].split('env:', 1)[0]
57 assert '!inputs.desktop_manual_only' in attach
58 manual_publish = publisher.split('if [ "${DESKTOP_MANUAL_ONLY:-false}" = "true" ]; then', 1)[1].split('elif', 1)[0]
59 assert 'manual-desktop-exception.sh" validate "$tag"' in manual_publish
60 assert 'args+=(--latest=false)' in manual_publish
61 stable_publish = publisher.split('elif [ "$prerelease" = "true" ]; then', 1)[1].split('fi', 1)[0]
62 assert 'args+=(--latest=false)' in stable_publish and 'args+=(--latest)' not in publisher
63 release_yml = (root / '.github/workflows/release.yml').read_text()
64 assert 'releases/latest/download' not in release_yml
65 assert 'Smoke public compatibility manifest' not in release_yml
66 verify = release_yml.split('name: Verify published CLI release assets', 1)[1].split('\n - name:', 1)[0].split('\n # ', 1)[0]
67 assert 'release-control/scripts/decide-cli-release-publication.sh' in verify and 'test "$decision" = "reuse"' in verify
68 assert 'sha256sum latest.json' in verify and 'test "$want" = "$got"' in verify
69 assert 'name: Sign artifacts (minisign)' in desktop
70 PY
71 [ "$(grep -Ec '^ environment: release$' "$repo_root/.github/workflows/release-stable.yml")" = "1" ]
72 test ! -e "$repo_root/.github/workflows/release-stable-trigger.yml"
73 candidate="$repo_root/.github/workflows/release-candidate.yml"
74 python3 - "$repo_root" <<'PY'
75 import pathlib, re, sys
76 root = pathlib.Path(sys.argv[1]) / '.github/workflows'
77 levels = {'none': 0, 'read': 1, 'write': 2}
78
79 def permissions(text, indent):
80 match = re.search(r'(?m)^' + ' ' * indent + r'permissions:\n((?:' + ' ' * (indent + 2) + r'[^\n]*\n)+)', text)
81 if not match:
82 return None
83 return {key: levels[value] for key, value in re.findall(r'([\w-]+): (none|read|write)', match[1])}
84
85 desktop = (root / 'release-desktop.yml').read_text()
86 required = permissions(desktop, 0)
87 for block in re.split(r'(?m)^ [\w-]+:\n', desktop.split('\njobs:\n', 1)[1])[1:]:
88 for key, value in (permissions(block, 4) or {}).items():
89 required[key] = max(required.get(key, 0), value)
90 # Conditions cannot hide a nested permission escalation at workflow parsing.
91 # Cover both preparation and the current/legacy publishing entrypoints.
92 for name in ('release-candidate.yml', 'release-promote.yml', 'release-stable.yml'):
93 workflow = (root / name).read_text()
94 default = permissions(workflow, 0)
95 calls = 0
96 for block in re.split(r'(?m)^ [\w-]+:\n', workflow.split('\njobs:\n', 1)[1])[1:]:
97 if 'uses: ./.github/workflows/release-desktop.yml' not in block:
98 continue
99 calls += 1
100 explicit = permissions(block, 4)
101 effective = default if explicit is None else explicit
102 for key, value in required.items():
103 assert effective.get(key, 0) >= value, f'{name}: Desktop call cannot grant {key} at required level {value}'
104 assert calls, f'{name}: expected a Desktop call'
105 PY
106 promote="$repo_root/.github/workflows/release-promote.yml"
107 verify="$repo_root/.github/workflows/release-verify.yml"
108 rehearsal_verify="$repo_root/.github/workflows/release-candidate-verify.yml"
109 for workflow in "$candidate" "$promote" "$verify" "$rehearsal_verify"; do test -s "$workflow"; done
110 candidate_dispatch="$(sed -n '/workflow_dispatch:/,/^ push:/p' "$candidate")"
111 [ "$(grep -Ec '^ [a-z_]+:$' <<<"$candidate_dispatch")" = "2" ]
112 grep -Fq 'required: true' <<<"$(sed -n '/^ version:/,/^ rehearsal:/p' <<<"$candidate_dispatch")"
113 grep -Fq 'required: false' <<<"$(sed -n '/^ rehearsal:/,$p' <<<"$candidate_dispatch")"
114 ! grep -Fq 'inputs.source_sha' "$candidate"
115 ! grep -Fq 'pull_request_target' "$candidate"
116 grep -Fq 'branches: [main-v2]' "$candidate"
117 grep -Fq -- '- release-notes/releases.json' "$candidate"
118 grep -Eq 'actions/attest-build-provenance@[0-9a-f]{40} # v3$' "$candidate"
119 grep -Fq 'bash scripts/validate-release-control-plane.sh' "$candidate"
120 preflight_line="$(grep -n -m1 'bash scripts/validate-release-control-plane.sh' "$candidate" | cut -d: -f1)"
121 source_ci_line="$(grep -n -m1 'run: bash scripts/verify-release-push-ci.sh' "$candidate" | cut -d: -f1)"
122 [ "$preflight_line" -lt "$source_ci_line" ]
123 ! grep -Fq 'source_sha="$(git rev-parse origin/main-v2)"' "$candidate"
124 grep -Fq 'retention-days: 30' "$candidate"
125 grep -Fq 'retention-days: 90' "$candidate"
126 grep -Fq 'name: ${{ needs.resolve.outputs.artifact_namespace }}-evidence-${{ needs.resolve.outputs.candidate_id }}' "$candidate"
127 grep -Fq 'RELEASE_EVIDENCE_ARTIFACT_ID: ${{ steps.evidence.outputs.artifact-id }}' "$candidate"
128 grep -Fq 'node scripts/resolve-release-candidate.mjs resolve-rehearsal' "$rehearsal_verify"
129 grep -Fq 'node scripts/release-candidate.mjs verify-rehearsal' "$rehearsal_verify"
130 grep -Fq 'node scripts/verify-release-artifact-archive.mjs' "$rehearsal_verify"
131 ! grep -Eq 'environment: release|release-candidate-tags\.sh activate|publish-(desktop|homebrew)|npm publish' "$rehearsal_verify"
132 grep -Fq 'gh attestation verify' "$promote"
133 grep -Fq 'bash scripts/release-candidate-tags.sh check' "$promote"
134 grep -Fq 'bash scripts/release-candidate-tags.sh activate' "$promote"
135 grep -Fq 'git push --atomic "$remote"' "$repo_root/scripts/release-candidate-tags.sh"
136 [ "$(grep -Ec '^ environment: release$' "$promote")" = "1" ]
137 grep -Fq 'group: stable-release-publication' "$promote"
138 grep -Fq 'run: bash scripts/reconcile-release-publication.sh' "$promote"
139 grep -Fq 'VERIFY_PUBLIC_POINTERS=true' "$repo_root/scripts/reconcile-release-publication.sh"
140 ! grep -Eq 'pages\.yml|--ref website' "$promote" "$repo_root/.github/workflows/release-stable.yml" "$repo_root/.github/workflows/release-desktop.yml"
141 grep -Fq 'release-publication-ledger-' "$promote"
142 grep -Fq 'release-publication-timing-' "$promote"
143 grep -Fq 'release-candidate-timing-' "$candidate"
144 grep -Fq 'immutable-complete-newer-pointer-preserved' "$repo_root/scripts/reconcile-release-publication.sh"
145 grep -Fq 'RELEASE_REVOKED_CANDIDATES' "$candidate"
146 grep -Fq 'PUSH_SHA: ${{ github.sha }}' "$candidate"
147 grep -Fq 'node scripts/resolve-release-candidate.mjs active "$candidate_id"' "$candidate"
148 grep -Fq 'The sealed record exists but its payload expired; preparing fresh files.' "$candidate"
149 grep -Fq 'RELEASE_REVOKED_CANDIDATES' "$promote"
150 grep -Fq 'run: bash scripts/verify-stable-release-artifacts.sh' "$verify"
151 grep -Fq 'RELEASE_OPERATION: recover' "$verify"
152 grep -Fq 'VERIFY_PUBLIC_POINTERS: "true"' "$verify"
153 ! grep -Fq -- '--dump-dom' "$repo_root/scripts/verify-stable-release-artifacts.sh"
154 grep -Eq 'ALLOW_STABLE_RECOVERY:.*inputs\.allow_recovery' \
155 "$repo_root/.github/workflows/release-stable.yml"
156 grep -Fq 'bash scripts/validate-stable-candidate.sh "$RELEASE_VERSION" "$RELEASE_SHA"' \
157 "$repo_root/.github/workflows/release-stable.yml"
158 grep -Fq 'bash scripts/verify-release-push-ci.sh "$RELEASE_SHA"' \
159 "$repo_root/.github/workflows/release-stable.yml"
160 grep -Fq 'if: ${{ !inputs.allow_recovery }}' \
161 "$repo_root/.github/workflows/release-stable.yml"
162 test -x "$repo_root/scripts/validate-stable-candidate.sh"
163 test -x "$repo_root/scripts/verify-release-push-ci.sh"
164 test -x "$repo_root/scripts/validate-release-control-plane.sh"
165 test -x "$repo_root/scripts/finalize-windows-signed-candidate.sh"
166 for retired in release-preview.yml release-cli-trigger.yml release-desktop-trigger.yml; do
167 test ! -e "$repo_root/.github/workflows/$retired"
168 done
169 npm_events="$(sed -n '/^on:/,/^permissions:/p' "$repo_root/.github/workflows/release-npm.yml")"
170 ! grep -Eq 'push:|npm-v\*-\*' <<<"$npm_events"
171 if grep -Eq '^ push:$' "$repo_root/.github/workflows/release-stable.yml" ||
172 grep -Eq '^ push:$' "$repo_root/.github/workflows/release.yml" ||
173 grep -Eq '^ push:$' "$repo_root/.github/workflows/release-desktop.yml"; then
174 echo "production workflow must be dispatched on protected main-v2, not run on a tag origin" >&2
175 exit 1
176 fi
177 for workflow in release.yml release-npm.yml release-desktop.yml; do
178 grep -Eq 'github\.workflow_ref' "$repo_root/.github/workflows/$workflow"
179 grep -Eq 'github\.ref_protected' "$repo_root/.github/workflows/$workflow"
180 grep -Eq 'inputs\.approved_sha' "$repo_root/.github/workflows/$workflow"
181 grep -Eq 'verify-release-tag\.sh' "$repo_root/.github/workflows/$workflow"
182 grep -Eq 'release-\{1\}\.yml' "$repo_root/.github/workflows/$workflow"
183 done
184 for workflow in release.yml release-npm.yml; do
185 grep -Eq "needs\.cache-guard\.result == 'success'" "$repo_root/.github/workflows/$workflow"
186 done
187 if grep -Eq '^ cache-guard:|needs\.cache-guard' "$repo_root/.github/workflows/release-desktop.yml"; then
188 echo "Desktop must reuse the orchestrator cache/docs evidence instead of executing candidate guard scripts twice" >&2
189 exit 1
190 fi
191 grep -Eq 'options: \[stable\]' "$repo_root/.github/workflows/release.yml"
192 cli_channel="$(grep -A6 -E '^ channel:' "$repo_root/.github/workflows/release.yml")"
193 grep -Eq 'default: stable' <<<"$cli_channel"
194 grep -Eq '^ environment: release$' "$repo_root/.github/workflows/release.yml"
195 grep -Eq 'GORELEASER_CURRENT_TAG:.*needs\.resolve\.outputs\.tag' \
196 "$repo_root/.github/workflows/release.yml"
197 grep -Eq 'bash scripts/resolve-cli-release\.sh' "$repo_root/.github/workflows/release.yml"
198 grep -Eq 'git merge-base --is-ancestor.*origin/main-v2' "$repo_root/.github/workflows/release.yml"
199 grep -Eq 'CLI Preview must tag current main-v2' "$repo_root/.github/workflows/release.yml"
200 grep -Fq 'ALLOW_PREVIEW_RECOVERY: ${{ inputs.allow_preview_recovery }}' "$repo_root/.github/workflows/release.yml"
201 grep -Eq 'Preview recovery requires the approved Preview orchestrator' "$repo_root/.github/workflows/release.yml"
202 grep -Eq "channel == 'stable'.*HOMEBREW_TAP_TOKEN" "$repo_root/.github/workflows/release.yml"
203 grep -Fq 'name: Isolate release-control checkout from product git state' \
204 "$repo_root/.github/workflows/release.yml"
205 grep -Fq "grep -qxF '/release-control/'" "$repo_root/.github/workflows/release.yml"
206 grep -Fq 'git check-ignore -q release-control/' "$repo_root/.github/workflows/release.yml"
207 release_control_isolation_line="$(
208 grep -n -m1 'name: Isolate release-control checkout from product git state' \
209 "$repo_root/.github/workflows/release.yml" | cut -d: -f1
210 )"
211 goreleaser_action_line="$(
212 grep -n -m1 'uses: goreleaser/goreleaser-action@' \
213 "$repo_root/.github/workflows/release.yml" | cut -d: -f1
214 )"
215 [ "$release_control_isolation_line" -lt "$goreleaser_action_line" ]
216
217 # A protected control-plane checkout must remain available to the workflow
218 # without making the immutable product checkout dirty for GoReleaser.
219 mkdir -p "$test_root/product-checkout/release-control"
220 git init -q "$test_root/product-checkout"
221 printf 'control plane\n' >"$test_root/product-checkout/release-control/marker"
222 [ "$(git -C "$test_root/product-checkout" status --porcelain --untracked-files=all)" = \
223 '?? release-control/marker' ]
224 product_git_common_dir="$(
225 git -C "$test_root/product-checkout" rev-parse --path-format=absolute --git-common-dir
226 )"
227 product_exclude="$product_git_common_dir/info/exclude"
228 printf '%s\n' '/release-control/' >>"$product_exclude"
229 git -C "$test_root/product-checkout" check-ignore -q release-control/
230 [ -z "$(git -C "$test_root/product-checkout" status --porcelain --untracked-files=all)" ]
231 grep -Eq "needs\.build\.result == 'success'" "$repo_root/.github/workflows/release-desktop.yml"
232 grep -Eq "needs\.publish\.result == 'success'" "$repo_root/.github/workflows/release-desktop.yml"
233 grep -Eq 'options: \[stable\]' "$repo_root/.github/workflows/release-desktop.yml"
234 desktop_dispatch="$(sed -n '/^ workflow_dispatch:/,/^ workflow_call:/p' "$repo_root/.github/workflows/release-desktop.yml")"
235 if grep -Eqi 'preview|canary' <<<"$desktop_dispatch"; then
236 echo "Standalone Desktop dispatch must not expose a Preview or Canary choice" >&2
237 exit 1
238 fi
239 grep -Eq '^ resolve:$' "$repo_root/.github/workflows/release-desktop.yml"
240 grep -Eq 'sha:.*steps\.candidate\.outputs\.sha' "$repo_root/.github/workflows/release-desktop.yml"
241 grep -Fq 'bash scripts/resolve-desktop-candidate.sh' "$repo_root/.github/workflows/release-desktop.yml"
242 grep -Fq 'name: Smoke-test packaged Electron startup' "$repo_root/.github/workflows/release-desktop.yml"
243 grep -Eq '^ windows-build:$' "$repo_root/.github/workflows/release-desktop.yml"
244 grep -Eq '^ windows-sign:$' "$repo_root/.github/workflows/release-desktop.yml"
245 windows_sign="$(sed -n '/^ windows-sign:/,/^ windows-runtime-acceptance:/p' "$repo_root/.github/workflows/release-desktop.yml")"
246 grep -Fq 'needs: [resolve, windows-build, signing-contract]' <<<"$windows_sign"
247 grep -Fq 'desktop/build/electron/${{ matrix.name }}/app' "$repo_root/.github/workflows/release-desktop.yml"
248 grep -Fq 'node desktop/packaging/smoke.mjs' "$repo_root/.github/workflows/release-desktop.yml"
249 grep -Fq -- '--service desktop/build/bin/reasonix-desktop.exe' "$repo_root/.github/workflows/release-desktop.yml"
250 grep -Fq 'node desktop/packaging/smoke.mjs "$RUNNER_TEMP/desktop-startup/Reasonix.app"' "$repo_root/.github/workflows/release-desktop.yml"
251 grep -Fq 'xvfb-run -a node desktop/packaging/smoke.mjs' "$repo_root/.github/workflows/release-desktop.yml"
252 test ! -e "$repo_root/scripts/test-webview2-approval-smoke.ps1"
253 # The Wails-era WebView2/WebKitGTK native smoke harnesses are retired with the
254 # old shell; the packaged Electron startup smoke replaces them.
255 test ! -e "$repo_root/scripts/test-webview2-native-smoke.ps1"
256 test ! -e "$repo_root/scripts/test-transcript-selection-webview2.ps1"
257 test ! -e "$repo_root/.github/workflows/transcript-native-smoke.yml"
258 grep -Fq 'name: Package Electron shell for native startup smoke' "$repo_root/.github/workflows/ci.yml"
259 grep -Fq 'name: Smoke-test Electron native startup' "$repo_root/.github/workflows/ci.yml"
260 grep -Fq 'node packaging/package.mjs windows/amd64 v0.0.0-ci canary' \
261 "$repo_root/.github/workflows/ci.yml"
262 grep -Fq -- '-X main.version=v0.0.0-ci -X main.channel=canary' "$repo_root/.github/workflows/ci.yml"
263 grep -Fq 'node packaging/smoke.mjs build/electron/windows-amd64/app --service build/bin/reasonix-desktop.exe' \
264 "$repo_root/.github/workflows/ci.yml"
265 grep -Fq 'node packaging/verify.mjs ../dist/Reasonix-darwin-arm64.zip' \
266 "$repo_root/.github/workflows/ci.yml"
267 grep -Fq 'node packaging/verify.mjs ../dist/Reasonix-windows-amd64.zip' \
268 "$repo_root/.github/workflows/ci.yml"
269 grep -Fq 'node packaging/signing-files.mjs build/windows/signing-payload --check' \
270 "$repo_root/.github/workflows/ci.yml"
271 if grep -Fq 'wails build' "$repo_root/.github/workflows/ci.yml"; then
272 echo "CI must package the Electron shell, not wails build" >&2
273 exit 1
274 fi
275 for retired_review_gate in \
276 "$repo_root/.github/workflows/cross-boundary-review.yml" \
277 "$repo_root/.github/workflows/cross-boundary-review-signal.yml" \
278 "$repo_root/.github/scripts/cross-boundary-review.cjs" \
279 "$repo_root/.github/scripts/cross-boundary-review.test.cjs"; do
280 if [ -e "$retired_review_gate" ]; then
281 echo "Retired cross-boundary review gate still exists: $retired_review_gate" >&2
282 exit 1
283 fi
284 done
285 ! grep -Fq 'cross-boundary-review' "$repo_root/.github/workflows/ci.yml"
286 ! grep -Fq 'independent cross-boundary review' "$repo_root/.github/pull_request_template.md"
287 desktop_build_line="$(grep -n -m1 'name: Build and package' "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1)"
288 electron_smoke_line="$(grep -n -m1 'name: Smoke-test packaged Electron startup' "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1)"
289 signing_upload_line="$(grep -n -m1 'name: Upload Windows signing inputs' "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1)"
290 [ "$desktop_build_line" -lt "$electron_smoke_line" ]
291 [ "$electron_smoke_line" -lt "$signing_upload_line" ]
292 [ "$(grep -Fc 'IN_ORCHESTRATOR: ${{ inputs.orchestrator }}' "$repo_root/.github/workflows/release-desktop.yml")" = "3" ]
293 [ "$(grep -Fc 'name: Revalidate immutable Desktop candidate' "$repo_root/.github/workflows/release-desktop.yml")" = "2" ]
294 [ "$(grep -Fc 'ref: ${{ needs.resolve.outputs.sha }}' "$repo_root/.github/workflows/release-desktop.yml")" -ge 4 ]
295 [ "$(grep -Ec '^ path: release-control$' "$repo_root/.github/workflows/release-desktop.yml")" -ge "5" ]
296 grep -Fq 'name: Checkout protected release verifier' "$repo_root/.github/workflows/release-desktop.yml"
297 grep -Fq 'scripts/desktop-release-artifacts.mjs' "$repo_root/.github/workflows/release-desktop.yml"
298 if grep -Fq 'test-webview2-native-smoke.ps1' "$repo_root/.github/workflows/release-desktop.yml"; then
299 echo "Desktop release must smoke the packaged Electron shell, not the Wails/WebView2 harness" >&2
300 exit 1
301 fi
302 grep -Fq 'finalize-windows-signed-candidate.sh' "$repo_root/.github/workflows/release-desktop.yml"
303 grep -Fq 'verify-windows-authenticode.ps1' "$repo_root/scripts/finalize-windows-signed-candidate.sh"
304 [ "$(grep -Fc 'ref: ${{ github.workflow_sha }}' "$repo_root/.github/workflows/release-desktop.yml")" -ge 2 ]
305 [ "$(grep -Fc 'bash release-control/scripts/resolve-desktop-candidate.sh' "$repo_root/.github/workflows/release-desktop.yml")" = "2" ]
306 [ "$(grep -Fc 'RELEASE_TAG: ${{ inputs.approved_cli_tag }}' "$repo_root/.github/workflows/release-desktop.yml")" = "3" ]
307 desktop_mirror="$(sed -n '/^ mirror:/,$p' "$repo_root/.github/workflows/release-desktop.yml")"
308 if grep -Fq 'RELEASE_TAG: ${{ inputs.tag }}' <<<"$desktop_mirror"; then
309 echo "Desktop mirror must revalidate the orchestrator-approved CLI tag" >&2
310 exit 1
311 fi
312 desktop_publish="$(sed -n '/name: publish release/,/name: mirror to R2/p' "$repo_root/.github/workflows/release-desktop.yml")"
313 if grep -Eq 'bash scripts/(resolve-desktop-candidate|validate-desktop-release-manifest|publish-desktop-github-release)\.sh' <<<"$desktop_publish"; then
314 echo "Desktop publish job uses candidate-controlled release scripts" >&2
315 exit 1
316 fi
317 if grep -Eq 'bash scripts/(resolve-desktop-candidate|validate-desktop-release-manifest|verify-desktop-release-directory|decide-desktop-pointer-update)\.sh' <<<"$desktop_mirror"; then
318 echo "Desktop mirror job uses candidate-controlled release scripts" >&2
319 exit 1
320 fi
321 if grep -Fq 'ref: ${{ inputs.approved_sha || inputs.tag || github.ref }}' \
322 "$repo_root/.github/workflows/release-desktop.yml"; then
323 echo "Desktop jobs must checkout the resolved immutable candidate SHA" >&2
324 exit 1
325 fi
326 grep -Fq "group: release-desktop-\${{ (inputs.channel == 'preview' || inputs.channel == 'canary') && 'preview' || 'stable' }}" \
327 "$repo_root/.github/workflows/release-desktop.yml"
328 grep -Eq 'IN_PRODUCTION_SIGNING_SMOKE:.*inputs\.production_signing_smoke' \
329 "$repo_root/.github/workflows/release-desktop.yml"
330 grep -Eq 'IN_SIGNING_PREFLIGHT:.*inputs\.signing_preflight' \
331 "$repo_root/.github/workflows/release-desktop.yml"
332 grep -Fq "group: release-npm-\${{ inputs.channel || 'next' }}" \
333 "$repo_root/.github/workflows/release-npm.yml"
334 npm_dispatch="$(sed -n '/^ workflow_dispatch:/,/^ workflow_call:/p' "$repo_root/.github/workflows/release-npm.yml")"
335 grep -Eq 'default: stable' <<<"$npm_dispatch"
336 if grep -Eq '^ - canary$' <<<"$npm_dispatch"; then
337 echo "Standalone npm dispatch must not expose public Canary publication" >&2
338 exit 1
339 fi
340 grep -Fq 'if: ${{ !inputs.orchestrated }}' "$repo_root/.github/workflows/release-npm.yml"
341 grep -Fq 'Publish or recover immutable npm packages' "$repo_root/.github/workflows/release-npm.yml"
342 npm_cache_guard="$(sed -n '/^ cache-guard:/,/^ npm:/p' "$repo_root/.github/workflows/release-npm.yml")"
343 if grep -Fq 'RECOVERY_CONTROL_SHA' <<<"$npm_cache_guard"; then
344 echo "npm recovery control plane must load in the publisher job after candidate checkout" >&2
345 exit 1
346 fi
347 npm_job="$(sed -n '/^ npm:/,$p' "$repo_root/.github/workflows/release-npm.yml")"
348 grep -Fq 'RECOVERY_CONTROL_SHA: ${{ github.workflow_sha }}' <<<"$npm_job"
349 grep -Fq 'git restore --source="$RECOVERY_CONTROL_SHA"' <<<"$npm_job"
350 for recovery_script in npm/publish.mjs scripts/finalize-npm-official-release.mjs; do
351 grep -Fq "$recovery_script" <<<"$npm_job"
352 done
353 # Orchestrated Stable recovery needs the same protected publisher repair as a
354 # standalone run; the immutable product checkout must not select the old helper.
355 npm_control_step="$(sed -n '/ - name: Load approved npm publication control plane/,/ - uses: actions\/setup-go@/p' "$repo_root/.github/workflows/release-npm.yml")"
356 [ -n "$npm_control_step" ]
357 if grep -q 'if:' <<<"$npm_control_step"; then
358 echo "npm publication control plane must load for orchestrated recovery too" >&2
359 exit 1
360 fi
361 grep -Fq 'publishPackages' "$repo_root/npm/build.mjs"
362 grep -Fq 'thumbprint: ${{ secrets.CERTUM_KEY_ID }}' "$repo_root/.github/workflows/release-desktop.yml"
363 grep -Eq 'SIGNPATH_RELEASE_SIGNING_ATTESTATION does not match' "$repo_root/.github/workflows/release-desktop.yml"
364 grep -Eq '^ signing_preflight:$' "$repo_root/.github/workflows/release-desktop.yml"
365 grep -Eq '^ signing_preflight_verified:$' "$repo_root/.github/workflows/release-desktop.yml"
366 grep -Eq '^ signing-contract:$' "$repo_root/.github/workflows/release-desktop.yml"
367 grep -Eq '^ attest-signing-contract:$' "$repo_root/.github/workflows/release-desktop.yml"
368 grep -Eq '^ production_signing_smoke:$' "$repo_root/.github/workflows/release-desktop.yml"
369 grep -Eq "needs\.build\.result == 'success'.*!inputs\.production_signing_smoke.*!inputs\.signing_preflight" \
370 "$repo_root/.github/workflows/release-desktop.yml"
371 ! grep -Eq 'signpath/github-action-submit-signing-request|secrets.SIGNPATH_API_TOKEN|complete-signpath-request\.ps1' "$repo_root/.github/workflows/release-desktop.yml"
372 grep -Fq 'uses: ./release-control/.github/actions/setup-certum' "$repo_root/.github/workflows/release-desktop.yml"
373 grep -Fq 'sign-certum.ps1" -PayloadDirectory' "$repo_root/scripts/finalize-windows-signed-candidate.sh"
374 grep -Fq 'sign-certum.ps1" -FilePath' "$repo_root/scripts/finalize-windows-signed-candidate.sh"
375 grep -Fq -- '-ExpectedThumbprint "$CERTUM_KEY_ID"' "$repo_root/scripts/finalize-windows-signed-candidate.sh"
376 grep -Eq '^ signpath-preflight:$' "$repo_root/.github/workflows/release-stable.yml"
377 grep -Eq 'signing_preflight: true' "$repo_root/.github/workflows/release-stable.yml"
378 grep -Eq 'signing_preflight_verified: true' "$repo_root/.github/workflows/release-stable.yml"
379 grep -Eq 'needs: \[authorize, signpath-preflight\]' "$repo_root/.github/workflows/release-stable.yml"
380 grep -Eq 'name: Require R2 for Preview' "$repo_root/.github/workflows/release-desktop.yml"
381 grep -Eq "channel == 'preview'.*HAS_R2 != 'true'" "$repo_root/.github/workflows/release-desktop.yml"
382 grep -Eq 'name: Validate generated manifest before publication' "$repo_root/.github/workflows/release-desktop.yml"
383 grep -Eq 'name: Validate R2 manifest before upload' "$repo_root/.github/workflows/release-desktop.yml"
384 grep -Fq 'scripts/publish-desktop-github-release.sh' "$repo_root/.github/workflows/release-desktop.yml"
385 if grep -Eq 'gh release create .*dist/' "$repo_root/.github/workflows/release-desktop.yml"; then
386 echo "Desktop release publication must recover an existing GitHub release" >&2
387 exit 1
388 fi
389 grep -Fq 'scripts/decide-desktop-pointer-update.sh' "$repo_root/.github/workflows/release-desktop.yml"
390 grep -Fq 'scripts/verify-desktop-release-directory.sh' "$repo_root/.github/workflows/release-desktop.yml"
391 grep -Fq 'scripts/compare-desktop-release-manifests.sh' "$repo_root/.github/workflows/release-desktop.yml"
392 grep -Fq 'go -C release-control/desktop build -o "$signature_verifier" ./cmd/sign' \
393 "$repo_root/.github/workflows/release-desktop.yml"
394 grep -Fq 'verify_signature_directory assets' "$repo_root/.github/workflows/release-desktop.yml"
395 grep -Fq 'verify_signature_directory "$existing_directory"' \
396 "$repo_root/.github/workflows/release-desktop.yml"
397 grep -Fq -- '--allow-authenticated-payload-differences' \
398 "$repo_root/.github/workflows/release-desktop.yml"
399 grep -Fq 'cp "$payload" "assets/$payload_relative"' "$repo_root/.github/workflows/release-desktop.yml"
400 grep -Fq 'cp "$signature" "assets/$relative"' "$repo_root/.github/workflows/release-desktop.yml"
401 grep -Fq 'verify-desktop-release-manifest-assets.sh' "$repo_root/.github/workflows/release-desktop.yml"
402 grep -Fq 'verify_signature_directory "$published_directory"' \
403 "$repo_root/.github/workflows/release-desktop.yml"
404 manifest_adoption_line="$(grep -nF 'cp "$existing_directory/latest.json" assets/latest.json' \
405 "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1)"
406 authenticated_compare_line="$(grep -nF -- '--allow-authenticated-payload-differences assets "$existing_directory"' \
407 "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1)"
408 if [ -z "$manifest_adoption_line" ] || [ -z "$authenticated_compare_line" ] ||
409 [ "$manifest_adoption_line" -ge "$authenticated_compare_line" ]; then
410 echo "Desktop recovery must adopt a validated existing manifest before directory comparison" >&2
411 exit 1
412 fi
413 grep -Fq 'download_optional "preview/latest.json"' "$repo_root/.github/workflows/release-desktop.yml"
414 grep -Fq 'download_optional "canary/latest.json"' "$repo_root/.github/workflows/release-desktop.yml"
415 grep -Fq '"legacy-${current_channel}" "$current_version" "$current_base"' \
416 "$repo_root/.github/workflows/release-desktop.yml"
417 grep -Fq 'legacy-preview "$current_version" "$legacy_preview_base"' \
418 "$repo_root/.github/workflows/release-desktop.yml"
419 grep -Fq 'publish_pointer canary' "$repo_root/.github/workflows/release-desktop.yml"
420 grep -Fq 'publish_pointer preview' "$repo_root/.github/workflows/release-desktop.yml"
421 grep -Fq 'cmp -s /tmp/reasonix-desktop-canary-latest.json /tmp/reasonix-desktop-preview-latest.json' \
422 "$repo_root/.github/workflows/release-desktop.yml"
423 grep -Fq 'steps.mirror_r2.outputs.pointer_moved' "$repo_root/.github/workflows/release-desktop.yml"
424 if grep -Fq 'aws s3 cp assets/ "s3://${R2_BUCKET}/preview/"' "$repo_root/.github/workflows/release-desktop.yml" ||
425 grep -Fq 'aws s3 cp assets/ "s3://${R2_BUCKET}/canary/"' "$repo_root/.github/workflows/release-desktop.yml"; then
426 echo "Desktop public pointers must contain only latest.json, not mutable asset copies" >&2
427 exit 1
428 fi
429
430 desktop_generated_validation_line="$(
431 grep -n -m1 'name: Validate generated manifest before publication' \
432 "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1
433 )"
434 desktop_github_publish_line="$(
435 grep -n -m1 'name: Publish GitHub release' \
436 "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1
437 )"
438 desktop_r2_validation_line="$(
439 grep -n -m1 'name: Validate R2 manifest before upload' \
440 "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1
441 )"
442 desktop_r2_upload_line="$(
443 grep -n -m1 'name: Mirror immutable assets and advance R2 pointer' \
444 "$repo_root/.github/workflows/release-desktop.yml" | cut -d: -f1
445 )"
446 [ "$desktop_generated_validation_line" -lt "$desktop_github_publish_line" ]
447 [ "$desktop_r2_validation_line" -lt "$desktop_r2_upload_line" ]
448 grep -Eq '^ postflight:$' "$repo_root/.github/workflows/release-stable.yml"
449 grep -Eq 'verify-stable-release-artifacts\.sh' "$repo_root/.github/workflows/release-stable.yml"
450 grep -Eq 'name: Upload reviewed release notes' "$repo_root/.github/workflows/release-stable.yml"
451 grep -Eq 'name: orchestrator-reviewed-release-notes' "$repo_root/.github/workflows/release-stable.yml"
452 for channel in cli npm desktop; do
453 grep -Eq '^ publish_'"$channel"':' "$repo_root/.github/workflows/release-stable.yml"
454 grep -Eq "inputs\.publish_$channel" "$repo_root/.github/workflows/release-stable.yml"
455 done
456
457 # The checked-in SignPath policy contract is the single source of truth for the
458 # provider allowlist and every top-level workflow that can reach signing.
459 go run ./cmd/signpath-contract validate
460 contract_fingerprint="$(go run ./cmd/signpath-contract fingerprint)"
461 grep -Eq '^v1:[0-9a-f]{64}$' <<<"$contract_fingerprint"
462 grep -Eq 'public postflight will still verify it' "$repo_root/.github/workflows/release-stable.yml"
463 for workflow in release.yml release-desktop.yml; do
464 grep -Eq 'name: Download orchestrator-reviewed release notes' "$repo_root/.github/workflows/$workflow"
465 grep -Eq 'name: orchestrator-reviewed-release-notes' "$repo_root/.github/workflows/$workflow"
466 grep -Eq 'if: \$\{\{ !inputs\.orchestrated' "$repo_root/.github/workflows/$workflow"
467 done
468
469 # CLI clients and the website consume separate Stable and Preview pointers. The
470 # publisher must validate every public archive before moving a pointer, retain an
471 # immutable per-tag record, and leave both public pointers untouched for RCs.
472 cli_release_workflow="$repo_root/.github/workflows/release.yml"
473 grep -Eq 'name: Publish CLI release metadata to R2' "$cli_release_workflow"
474 grep -Fq 'cli/releases/${TAG}/latest.json' "$cli_release_workflow"
475 grep -Fq 'cli/${channel}/latest.json' "$cli_release_workflow"
476 grep -Fq "group: release-cli-\${{ inputs.channel || 'stable' }}" "$cli_release_workflow"
477 grep -Fq 'scripts/decide-cli-pointer-update.sh' "$cli_release_workflow"
478 grep -Fq 'scripts/validate-cli-release-manifest.sh' "$cli_release_workflow"
479 grep -Fq 'scripts/compare-cli-release-manifests.sh' "$cli_release_workflow"
480 grep -Eq 'name: Decide whether CLI artifacts need publication' "$cli_release_workflow"
481 grep -Fq 'path: release-control' "$cli_release_workflow"
482 grep -Fq 'ref: ${{ github.workflow_sha }}' "$cli_release_workflow"
483 grep -Fq 'release-control/scripts/decide-cli-release-publication.sh' "$cli_release_workflow"
484 cli_goreleaser="$(sed -n '/^ goreleaser:/,$p' "$cli_release_workflow")"
485 if grep -Fq 'bash scripts/decide-cli-release-publication.sh' <<<"$cli_goreleaser"; then
486 echo "CLI recovery uses candidate-controlled publication policy" >&2
487 exit 1
488 fi
489 grep -Fq "steps.publication.outputs.decision == 'publish' && inputs.candidate_artifact_name == ''" "$cli_release_workflow"
490 grep -Fq "steps.publication.outputs.decision == 'publish' && inputs.candidate_artifact_name != ''" "$cli_release_workflow"
491 grep -Fq 'immutable CLI release metadata for $TAG already exists with different content' "$cli_release_workflow"
492 grep -Fq 'cmp -s /tmp/cli-release.json /tmp/cli-release.pointer.json' "$cli_release_workflow"
493 grep -Eq 'internal CLI release .*Stable and Preview pointers remain unchanged' "$cli_release_workflow"
494 cli_public_validation_line="$(
495 grep -n -m1 'if \[ -n "\$channel" \]' "$cli_release_workflow" | cut -d: -f1
496 )"
497 cli_immutable_upload_line="$(
498 grep -n -m1 'cli/releases/${TAG}/latest.json' "$cli_release_workflow" | cut -d: -f1
499 )"
500 [ "$cli_public_validation_line" -lt "$cli_immutable_upload_line" ]
501 pointer_compare="$repo_root/scripts/compare-cli-release-tags.sh"
502 test -x "$pointer_compare"
503 [ "$(bash "$pointer_compare" stable v1.2.4 v1.2.3)" = "update" ]
504 [ "$(bash "$pointer_compare" stable v1.2.3 v1.2.3)" = "skip" ]
505 [ "$(bash "$pointer_compare" stable v1.2.2 v1.2.3)" = "skip" ]
506 [ "$(bash "$pointer_compare" stable v100000000000000000000.0.0 v99999999999999999999.999.999)" = "update" ]
507 [ "$(bash "$pointer_compare" preview v1.2.3-preview.11 v1.2.3-preview.9)" = "update" ]
508 [ "$(bash "$pointer_compare" preview v1.2.3-preview.9 v1.2.3-preview.9)" = "skip" ]
509 [ "$(bash "$pointer_compare" preview v1.2.3-preview.8 v1.2.3-preview.9)" = "skip" ]
510 [ "$(bash "$pointer_compare" stable v1.2.3 "")" = "update" ]
511 if bash "$pointer_compare" stable v1.2.3 v1.2.3-preview.1 >/dev/null 2>&1; then
512 echo "stable comparator accepted a preview pointer" >&2
513 exit 1
514 fi
515 if bash "$pointer_compare" preview v1.2.3-preview.1 v1.2.3 >/dev/null 2>&1; then
516 echo "preview comparator accepted a stable pointer" >&2
517 exit 1
518 fi
519 if bash "$pointer_compare" stable v01.2.3 v1.2.2 >/dev/null 2>&1; then
520 echo "stable comparator accepted a non-canonical tag" >&2
521 exit 1
522 fi
523 cli_pointer_decider="$repo_root/scripts/decide-cli-pointer-update.sh"
524 test -x "$cli_pointer_decider"
525 cli_pointer_candidate="$test_root/cli-pointer-candidate.json"
526 cli_pointer_older="$test_root/cli-pointer-older.json"
527 cli_pointer_equal="$test_root/cli-pointer-equal.json"
528 cli_pointer_equal_different="$test_root/cli-pointer-equal-different.json"
529 cli_pointer_newer="$test_root/cli-pointer-newer.json"
530 printf '{"tag_name":"v1.3.0","marker":"candidate"}\n' >"$cli_pointer_candidate"
531 printf '{"tag_name":"v1.2.9","marker":"older"}\n' >"$cli_pointer_older"
532 cp "$cli_pointer_candidate" "$cli_pointer_equal"
533 printf '{"tag_name":"v1.3.0","marker":"different"}\n' >"$cli_pointer_equal_different"
534 printf '{"tag_name":"v1.3.1","marker":"newer"}\n' >"$cli_pointer_newer"
535 [ "$(bash "$cli_pointer_decider" stable "$cli_pointer_candidate" -)" = "update" ]
536 [ "$(bash "$cli_pointer_decider" stable "$cli_pointer_candidate" "$cli_pointer_older")" = "update" ]
537 [ "$(bash "$cli_pointer_decider" stable "$cli_pointer_candidate" "$cli_pointer_equal")" = "skip" ]
538 [ "$(bash "$cli_pointer_decider" stable "$cli_pointer_candidate" "$cli_pointer_equal_different")" = "update" ]
539 [ "$(bash "$cli_pointer_decider" stable "$cli_pointer_candidate" "$cli_pointer_newer")" = "skip" ]
540 if bash "$cli_pointer_decider" stable "$cli_pointer_candidate" "$test_root/missing-cli-pointer.json" >/dev/null 2>&1; then
541 echo "CLI pointer decider accepted an unreadable manifest path" >&2
542 exit 1
543 fi
544 for asset in \
545 reasonix-darwin-amd64.tar.gz \
546 reasonix-darwin-arm64.tar.gz \
547 reasonix-linux-amd64.tar.gz \
548 reasonix-linux-arm64.tar.gz \
549 reasonix-windows-amd64.zip \
550 reasonix-windows-arm64.zip \
551 SHA256SUMS; do
552 grep -Fq "\"$asset\"" "$cli_release_workflow"
553 done
554 publication_decider="$repo_root/scripts/decide-cli-release-publication.sh"
555 test -x "$publication_decider"
556 [ "$(bash "$publication_decider" stable v1.2.3 esengine/DeepSeek-Reasonix - -)" = "publish" ]
557 publication_checksums="$test_root/cli-publication-SHA256SUMS"
558 publication_release="$test_root/cli-publication-release.json"
559 publication_hash="0000000000000000000000000000000000000000000000000000000000000000"
560 publication_assets='[
561 "reasonix-darwin-amd64.tar.gz",
562 "reasonix-darwin-arm64.tar.gz",
563 "reasonix-linux-amd64.tar.gz",
564 "reasonix-linux-arm64.tar.gz",
565 "reasonix-windows-amd64.zip",
566 "reasonix-windows-arm64.zip",
567 "SHA256SUMS"
568 ]'
569 for asset in \
570 reasonix-darwin-amd64.tar.gz \
571 reasonix-darwin-arm64.tar.gz \
572 reasonix-linux-amd64.tar.gz \
573 reasonix-linux-arm64.tar.gz \
574 reasonix-windows-amd64.zip \
575 reasonix-windows-arm64.zip; do
576 printf '%s %s\n' "$publication_hash" "$asset"
577 done >"$publication_checksums"
578 publication_checksum_hash="$(shasum -a 256 "$publication_checksums" | awk '{print $1}')"
579 jq -n \
580 --arg repo "esengine/DeepSeek-Reasonix" \
581 --arg tag "v1.2.3" \
582 --arg archive_hash "$publication_hash" \
583 --arg checksum_hash "$publication_checksum_hash" \
584 --argjson names "$publication_assets" '
585 {
586 tag_name: $tag,
587 draft: false,
588 prerelease: false,
589 html_url: ("https://github.com/" + $repo + "/releases/tag/" + $tag),
590 assets: [
591 $names[] as $name |
592 {
593 name: $name,
594 state: "uploaded",
595 size: 1,
596 browser_download_url:
597 ("https://github.com/" + $repo + "/releases/download/" + $tag + "/" + $name),
598 digest: ("sha256:" + (if $name == "SHA256SUMS" then $checksum_hash else $archive_hash end))
599 }
600 ]
601 }
602 ' >"$publication_release"
603 [ "$(bash "$publication_decider" stable v1.2.3 esengine/DeepSeek-Reasonix \
604 "$publication_release" "$publication_checksums")" = "reuse" ]
605 publication_with_compat="$test_root/cli-publication-with-compat-release.json"
606 jq '.assets += [{name: "latest.json", state: "uploaded", size: 1,
607 browser_download_url: "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.2.3/latest.json",
608 digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"}]' \
609 "$publication_release" >"$publication_with_compat"
610 [ "$(bash "$publication_decider" stable v1.2.3 esengine/DeepSeek-Reasonix \
611 "$publication_with_compat" "$publication_checksums")" = "reuse" ]
612 publication_unexpected_asset="$test_root/cli-publication-unexpected-asset-release.json"
613 jq '.assets += [{name: "unexpected.zip", state: "uploaded", size: 1,
614 browser_download_url: "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.2.3/unexpected.zip",
615 digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"}]' \
616 "$publication_release" >"$publication_unexpected_asset"
617 if bash "$publication_decider" stable v1.2.3 esengine/DeepSeek-Reasonix \
618 "$publication_unexpected_asset" "$publication_checksums" >/dev/null 2>&1; then
619 echo "CLI publication decider accepted an unexpected asset" >&2
620 exit 1
621 fi
622 publication_preview="$test_root/cli-publication-preview-release.json"
623 jq '.tag_name = "v1.2.3-preview.4" | .prerelease = true |
624 .html_url = "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.2.3-preview.4" |
625 .assets |= map(.browser_download_url |= sub("/v1.2.3/"; "/v1.2.3-preview.4/"))' \
626 "$publication_release" >"$publication_preview"
627 [ "$(bash "$publication_decider" preview v1.2.3-preview.4 esengine/DeepSeek-Reasonix \
628 "$publication_preview" "$publication_checksums")" = "reuse" ]
629 publication_rc="$test_root/cli-publication-rc-release.json"
630 jq '.tag_name = "v1.2.3-rc.1" | .prerelease = true |
631 .html_url = "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.2.3-rc.1" |
632 .assets |= map(.browser_download_url |= sub("/v1.2.3/"; "/v1.2.3-rc.1/"))' \
633 "$publication_release" >"$publication_rc"
634 [ "$(bash "$publication_decider" any v1.2.3-rc.1 esengine/DeepSeek-Reasonix \
635 "$publication_rc" "$publication_checksums")" = "reuse" ]
636 publication_partial="$test_root/cli-publication-partial-release.json"
637 jq '.assets |= map(select(.name != "reasonix-linux-arm64.tar.gz"))' \
638 "$publication_release" >"$publication_partial"
639 if bash "$publication_decider" stable v1.2.3 esengine/DeepSeek-Reasonix \
640 "$publication_partial" "$publication_checksums" >/dev/null 2>&1; then
641 echo "CLI publication decider accepted a partial existing release" >&2
642 exit 1
643 fi
644 publication_bad_checksums="$test_root/cli-publication-bad-SHA256SUMS"
645 sed '1s/^0/1/' "$publication_checksums" >"$publication_bad_checksums"
646 if bash "$publication_decider" stable v1.2.3 esengine/DeepSeek-Reasonix \
647 "$publication_release" "$publication_bad_checksums" >/dev/null 2>&1; then
648 echo "CLI publication decider accepted mismatched checksums" >&2
649 exit 1
650 fi
651 manifest_validator="$repo_root/scripts/validate-cli-release-manifest.sh"
652 manifest_comparator="$repo_root/scripts/compare-cli-release-manifests.sh"
653 test -x "$manifest_validator"
654 test -x "$manifest_comparator"
655 manifest_assets='[
656 "reasonix-darwin-amd64.tar.gz",
657 "reasonix-darwin-arm64.tar.gz",
658 "reasonix-linux-amd64.tar.gz",
659 "reasonix-linux-arm64.tar.gz",
660 "reasonix-windows-amd64.zip",
661 "reasonix-windows-arm64.zip",
662 "SHA256SUMS"
663 ]'
664 manifest_repo="esengine/DeepSeek-Reasonix"
665 manifest_tag="v1.2.3"
666 manifest_file="$test_root/cli-release-manifest.json"
667 jq -n \
668 --arg repo "$manifest_repo" \
669 --arg tag "$manifest_tag" \
670 --argjson names "$manifest_assets" '
671 {
672 tag_name: $tag,
673 prerelease: false,
674 html_url: ("https://github.com/" + $repo + "/releases/tag/" + $tag),
675 release_notes_url: ("https://reasonix.io/changelog/" + $tag + "/"),
676 assets: [
677 $names[] as $name |
678 {
679 name: $name,
680 browser_download_url:
681 ("https://github.com/" + $repo + "/releases/download/" + $tag + "/" + $name),
682 size: 1
683 }
684 ]
685 }
686 ' >"$manifest_file"
687 bash "$manifest_validator" stable "$manifest_tag" "$manifest_repo" "$manifest_file"
688 bash "$manifest_validator" any "$manifest_tag" "$manifest_repo" "$manifest_file"
689
690 legacy_manifest="$test_root/cli-release-manifest-legacy.json"
691 jq 'del(.release_notes_url)' "$manifest_file" >"$legacy_manifest"
692 bash "$manifest_validator" legacy-stable "$manifest_tag" "$manifest_repo" "$legacy_manifest"
693 bash "$manifest_comparator" "$manifest_file" "$legacy_manifest"
694 if bash "$manifest_validator" stable "$manifest_tag" "$manifest_repo" "$legacy_manifest" >/dev/null 2>&1; then
695 echo "strict CLI manifest validator accepted a legacy manifest" >&2
696 exit 1
697 fi
698 wrong_legacy_notes="$test_root/cli-release-manifest-wrong-legacy-notes.json"
699 jq '.release_notes_url = "https://reasonix.io/changelog/v9.9.9/"' \
700 "$manifest_file" >"$wrong_legacy_notes"
701 if bash "$manifest_validator" legacy-stable "$manifest_tag" "$manifest_repo" \
702 "$wrong_legacy_notes" >/dev/null 2>&1; then
703 echo "legacy CLI manifest validator accepted a mismatched release-notes URL" >&2
704 exit 1
705 fi
706 if bash "$manifest_comparator" "$manifest_file" "$wrong_legacy_notes" >/dev/null 2>&1; then
707 echo "CLI manifest comparator ignored a non-legacy difference" >&2
708 exit 1
709 fi
710
711 rc_manifest_tag="v1.2.4-rc.1"
712 rc_notes_tag="v1.2.4"
713 rc_manifest="$test_root/cli-release-manifest-rc.json"
714 jq \
715 --arg repo "$manifest_repo" \
716 --arg tag "$rc_manifest_tag" \
717 --arg notes_tag "$rc_notes_tag" '
718 .tag_name = $tag |
719 .prerelease = true |
720 .html_url = ("https://github.com/" + $repo + "/releases/tag/" + $tag) |
721 .release_notes_url = ("https://reasonix.io/changelog/" + $notes_tag + "/") |
722 .assets |= map(
723 .browser_download_url =
724 ("https://github.com/" + $repo + "/releases/download/" + $tag + "/" + .name)
725 )
726 ' "$manifest_file" >"$rc_manifest"
727 bash "$manifest_validator" any "$rc_manifest_tag" "$manifest_repo" "$rc_manifest" "$rc_notes_tag"
728
729 expect_invalid_cli_manifest() {
730 local description="$1"
731 local file="$2"
732 if bash "$manifest_validator" stable "$manifest_tag" "$manifest_repo" "$file" >/dev/null 2>&1; then
733 echo "CLI manifest validator accepted $description" >&2
734 exit 1
735 fi
736 }
737
738 jq '.assets[0].browser_download_url = "https://github.com/attacker/project/releases/download/v1.2.3/reasonix-darwin-amd64.tar.gz"' \
739 "$manifest_file" >"$test_root/wrong-repository.json"
740 expect_invalid_cli_manifest "an asset from another repository" "$test_root/wrong-repository.json"
741 jq '.assets[0].browser_download_url = "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v9.9.9/reasonix-darwin-amd64.tar.gz"' \
742 "$manifest_file" >"$test_root/wrong-tag.json"
743 expect_invalid_cli_manifest "an asset from another tag" "$test_root/wrong-tag.json"
744 jq '.assets = .assets[:-1]' "$manifest_file" >"$test_root/missing-asset.json"
745 expect_invalid_cli_manifest "a missing required asset" "$test_root/missing-asset.json"
746 jq '.assets[0].size = 0' "$manifest_file" >"$test_root/empty-asset.json"
747 expect_invalid_cli_manifest "a zero-byte asset" "$test_root/empty-asset.json"
748 jq '.assets[0].size = 1073741825' "$manifest_file" >"$test_root/oversized-asset.json"
749 expect_invalid_cli_manifest "an asset above the release size limit" "$test_root/oversized-asset.json"
750 jq '.assets[0].size = 9007199254740992' "$manifest_file" >"$test_root/unsafe-integer-asset.json"
751 expect_invalid_cli_manifest "an asset size above the JavaScript safe-integer range" \
752 "$test_root/unsafe-integer-asset.json"
753 if bash "$manifest_validator" preview "$manifest_tag" "$manifest_repo" "$manifest_file" >/dev/null 2>&1; then
754 echo "CLI manifest validator accepted a Stable manifest as Preview" >&2
755 exit 1
756 fi
757
758 # Desktop public pointers use the same strict channel grammar, but their assets
759 # must also share one exact immutable directory and complete integrity metadata.
760 desktop_compare="$repo_root/scripts/compare-desktop-release-versions.sh"
761 test -x "$desktop_compare"
762 [ "$(bash "$desktop_compare" stable v1.2.4 v1.2.3)" = "update" ]
763 [ "$(bash "$desktop_compare" stable v1.2.3 v1.2.3)" = "skip" ]
764 [ "$(bash "$desktop_compare" stable v1.2.2 v1.2.3)" = "skip" ]
765 [ "$(bash "$desktop_compare" stable v100000000000000000000.0.0 v99999999999999999999.999.999)" = "update" ]
766 [ "$(bash "$desktop_compare" preview v1.2.3-preview.11 v1.2.3-preview.9)" = "update" ]
767 [ "$(bash "$desktop_compare" preview v1.2.3-preview.9 v1.2.3-preview.9)" = "skip" ]
768 [ "$(bash "$desktop_compare" preview v1.2.3-preview.8 v1.2.3-preview.9)" = "skip" ]
769 [ "$(bash "$desktop_compare" preview v1.2.3-preview.1 "")" = "update" ]
770 if bash "$desktop_compare" stable v1.2.3 v1.2.3-preview.1 >/dev/null 2>&1; then
771 echo "Desktop Stable comparator accepted a Preview pointer" >&2
772 exit 1
773 fi
774 if bash "$desktop_compare" preview v1.2.3-preview.1 v1.2.3 >/dev/null 2>&1; then
775 echo "Desktop Preview comparator accepted a Stable pointer" >&2
776 exit 1
777 fi
778 if bash "$desktop_compare" preview v1.2.3-preview.01 v1.2.3-preview.1 >/dev/null 2>&1; then
779 echo "Desktop comparator accepted a non-canonical Preview version" >&2
780 exit 1
781 fi
782
783 desktop_pointer_decider="$repo_root/scripts/decide-desktop-pointer-update.sh"
784 test -x "$desktop_pointer_decider"
785 preview_candidate="$test_root/preview-candidate.json"
786 preview_older="$test_root/preview-older.json"
787 preview_equal="$test_root/preview-equal.json"
788 preview_equal_different="$test_root/preview-equal-different.json"
789 preview_newer="$test_root/preview-newer.json"
790 printf '{"version":"v1.3.0-preview.42","marker":"candidate"}\n' >"$preview_candidate"
791 printf '{"version":"v1.3.0-preview.41","marker":"older"}\n' >"$preview_older"
792 cp "$preview_candidate" "$preview_equal"
793 printf '{"version":"v1.3.0-preview.42","marker":"different"}\n' >"$preview_equal_different"
794 printf '{"version":"v1.3.0-preview.43","marker":"newer"}\n' >"$preview_newer"
795 preview_newer_equal="$test_root/preview-newer-equal.json"
796 preview_newer_conflict="$test_root/preview-newer-conflict.json"
797 cp "$preview_newer" "$preview_newer_equal"
798 printf '{"version":"v1.3.0-preview.43","marker":"conflict"}\n' >"$preview_newer_conflict"
799 expect_pointer_source() {
800 local expected="$1"
801 shift
802 local decision
803 local action
804 local source
805 decision="$(bash "$desktop_pointer_decider" "$@")"
806 IFS=$'\t' read -r action source <<<"$decision"
807 [ "$action" = "update" ]
808 [ "$source" = "$expected" ]
809 }
810 expect_pointer_source "$preview_candidate" preview "$preview_candidate" - -
811 expect_pointer_source "$preview_candidate" preview "$preview_candidate" "$preview_older" "$preview_older"
812 expect_pointer_source "$preview_candidate" preview "$preview_candidate" "$preview_equal" "$preview_older"
813 expect_pointer_source "$preview_candidate" preview "$preview_candidate" "$preview_older" "$preview_equal"
814 [ "$(bash "$desktop_pointer_decider" preview "$preview_candidate" "$preview_equal" "$preview_equal")" = "skip" ]
815 expect_pointer_source "$preview_candidate" preview "$preview_candidate" "$preview_equal" "$preview_equal_different"
816 expect_pointer_source "$preview_newer" preview "$preview_candidate" "$preview_newer" "$preview_older"
817 [ "$(bash "$desktop_pointer_decider" preview "$preview_candidate" "$preview_newer" "$preview_newer_equal")" = "skip" ]
818 if bash "$desktop_pointer_decider" preview "$preview_candidate" "$preview_newer" \
819 "$preview_newer_conflict" >/dev/null 2>&1; then
820 echo "Desktop Preview pointer decider accepted conflicting equal newer aliases" >&2
821 exit 1
822 fi
823 if bash "$desktop_pointer_decider" preview "$preview_candidate" "$test_root/missing.json" - >/dev/null 2>&1; then
824 echo "Desktop Preview pointer decider accepted an unreadable manifest path" >&2
825 exit 1
826 fi
827
828 stable_candidate="$test_root/stable-candidate.json"
829 stable_older="$test_root/stable-older.json"
830 stable_equal="$test_root/stable-equal.json"
831 stable_equal_different="$test_root/stable-equal-different.json"
832 stable_newer="$test_root/stable-newer.json"
833 printf '{"version":"v1.3.0","marker":"candidate"}\n' >"$stable_candidate"
834 printf '{"version":"v1.2.9","marker":"older"}\n' >"$stable_older"
835 cp "$stable_candidate" "$stable_equal"
836 printf '{"version":"v1.3.0","marker":"different"}\n' >"$stable_equal_different"
837 printf '{"version":"v1.3.1","marker":"newer"}\n' >"$stable_newer"
838 expect_pointer_source "$stable_candidate" stable "$stable_candidate" -
839 expect_pointer_source "$stable_candidate" stable "$stable_candidate" "$stable_older"
840 [ "$(bash "$desktop_pointer_decider" stable "$stable_candidate" "$stable_equal")" = "skip" ]
841 expect_pointer_source "$stable_candidate" stable "$stable_candidate" "$stable_equal_different"
842 [ "$(bash "$desktop_pointer_decider" stable "$stable_candidate" "$stable_newer")" = "skip" ]
843
844 desktop_directory_verifier="$repo_root/scripts/verify-desktop-release-directory.sh"
845 test -x "$desktop_directory_verifier"
846 candidate_directory="$test_root/desktop-directory-candidate"
847 existing_directory="$test_root/desktop-directory-existing"
848 mkdir -p "$candidate_directory" "$existing_directory"
849 printf 'asset-a\n' >"$candidate_directory/a"
850 printf 'asset-b\n' >"$candidate_directory/b"
851 cp "$candidate_directory/a" "$existing_directory/a"
852 bash "$desktop_directory_verifier" --allow-missing "$candidate_directory" "$existing_directory"
853 if bash "$desktop_directory_verifier" "$candidate_directory" "$existing_directory" >/dev/null 2>&1; then
854 echo "Desktop release directory verifier accepted a missing object in exact mode" >&2
855 exit 1
856 fi
857 cp "$candidate_directory/b" "$existing_directory/b"
858 bash "$desktop_directory_verifier" "$candidate_directory" "$existing_directory"
859 printf 'candidate-signature\n' >"$candidate_directory/a.minisig"
860 printf 'existing-signature\n' >"$existing_directory/a.minisig"
861 if bash "$desktop_directory_verifier" "$candidate_directory" "$existing_directory" >/dev/null 2>&1; then
862 echo "Desktop release directory verifier accepted a byte-distinct signature without opt-in" >&2
863 exit 1
864 fi
865 bash "$desktop_directory_verifier" --allow-signature-differences \
866 "$candidate_directory" "$existing_directory"
867 printf 'candidate-payload\n' >"$candidate_directory/signed"
868 printf 'existing-payload\n' >"$existing_directory/signed"
869 printf 'candidate-signature\n' >"$candidate_directory/signed.minisig"
870 printf 'existing-signature\n' >"$existing_directory/signed.minisig"
871 if bash "$desktop_directory_verifier" --allow-signature-differences \
872 "$candidate_directory" "$existing_directory" >/dev/null 2>&1; then
873 echo "Desktop release directory verifier accepted a byte-distinct payload without authenticated-payload opt-in" >&2
874 exit 1
875 fi
876 bash "$desktop_directory_verifier" --allow-authenticated-payload-differences \
877 "$candidate_directory" "$existing_directory"
878 printf '' >"$existing_directory/signed.minisig"
879 if bash "$desktop_directory_verifier" --allow-authenticated-payload-differences \
880 "$candidate_directory" "$existing_directory" >/dev/null 2>&1; then
881 echo "Desktop release directory verifier accepted a byte-distinct payload with an empty signature" >&2
882 exit 1
883 fi
884 cp "$candidate_directory/signed" "$existing_directory/signed"
885 cp "$candidate_directory/signed.minisig" "$existing_directory/signed.minisig"
886 printf '' >"$existing_directory/a.minisig"
887 if bash "$desktop_directory_verifier" --allow-signature-differences \
888 "$candidate_directory" "$existing_directory" >/dev/null 2>&1; then
889 echo "Desktop release directory verifier accepted an empty recovered signature" >&2
890 exit 1
891 fi
892 cp "$candidate_directory/a.minisig" "$existing_directory/a.minisig"
893 printf 'conflict\n' >"$existing_directory/a"
894 if bash "$desktop_directory_verifier" --allow-missing "$candidate_directory" "$existing_directory" >/dev/null 2>&1; then
895 echo "Desktop release directory verifier accepted conflicting immutable content" >&2
896 exit 1
897 fi
898 cp "$candidate_directory/a" "$existing_directory/a"
899 printf 'unexpected\n' >"$existing_directory/unexpected"
900 if bash "$desktop_directory_verifier" --allow-missing "$candidate_directory" "$existing_directory" >/dev/null 2>&1; then
901 echo "Desktop release directory verifier accepted an unexpected immutable object" >&2
902 exit 1
903 fi
904
905 recovery_candidate_directory="$test_root/desktop-directory-recovery-candidate"
906 recovery_existing_directory="$test_root/desktop-directory-recovery-existing"
907 mkdir -p "$recovery_candidate_directory" "$recovery_existing_directory"
908 printf 'candidate-payload\n' >"$recovery_candidate_directory/artifact"
909 printf 'candidate-signature\n' >"$recovery_candidate_directory/artifact.minisig"
910 printf 'existing-payload\n' >"$recovery_existing_directory/artifact"
911 printf 'existing-signature\n' >"$recovery_existing_directory/artifact.minisig"
912 printf '{"version":"v1.2.3","release_notes_url":"https://reasonix.io/changelog/v1.2.3/","marker":"candidate"}\n' \
913 >"$recovery_candidate_directory/latest.json"
914 printf '{"version":"v1.2.3","release_notes_url":"https://reasonix.io/changelog/v1.2.3/","marker":"existing"}\n' \
915 >"$recovery_existing_directory/latest.json"
916 if bash "$desktop_directory_verifier" --allow-missing --allow-legacy-manifest \
917 --allow-authenticated-payload-differences \
918 "$recovery_candidate_directory" "$recovery_existing_directory" >/dev/null 2>&1; then
919 echo "Desktop recovery accepted a conflicting manifest before validated adoption" >&2
920 exit 1
921 fi
922 cp "$recovery_existing_directory/latest.json" "$recovery_candidate_directory/latest.json"
923 bash "$desktop_directory_verifier" --allow-missing --allow-legacy-manifest \
924 --allow-authenticated-payload-differences \
925 "$recovery_candidate_directory" "$recovery_existing_directory"
926
927 legacy_candidate_directory="$test_root/desktop-directory-legacy-candidate"
928 legacy_existing_directory="$test_root/desktop-directory-legacy-existing"
929 mkdir -p "$legacy_candidate_directory" "$legacy_existing_directory"
930 printf 'asset\n' >"$legacy_candidate_directory/artifact"
931 cp "$legacy_candidate_directory/artifact" "$legacy_existing_directory/artifact"
932 jq -n '{
933 version: "v1.2.3",
934 release_notes_url: "https://reasonix.io/changelog/v1.2.3/",
935 platforms: {"darwin-arm64": {size: 42}}
936 }' >"$legacy_candidate_directory/latest.json"
937 jq 'del(.release_notes_url)' "$legacy_candidate_directory/latest.json" \
938 >"$legacy_existing_directory/latest.json"
939 bash "$desktop_directory_verifier" --allow-legacy-manifest \
940 "$legacy_candidate_directory" "$legacy_existing_directory"
941 if bash "$desktop_directory_verifier" \
942 "$legacy_candidate_directory" "$legacy_existing_directory" >/dev/null 2>&1; then
943 echo "Desktop release directory verifier accepted a legacy manifest without opt-in" >&2
944 exit 1
945 fi
946 jq '.platforms["darwin-arm64"].size = 99' "$legacy_existing_directory/latest.json" \
947 >"$legacy_existing_directory/latest-conflict.json"
948 mv "$legacy_existing_directory/latest-conflict.json" "$legacy_existing_directory/latest.json"
949 if bash "$desktop_directory_verifier" --allow-legacy-manifest \
950 "$legacy_candidate_directory" "$legacy_existing_directory" >/dev/null 2>&1; then
951 echo "Desktop release directory verifier ignored a non-legacy manifest difference" >&2
952 exit 1
953 fi
954
955 desktop_manifest_asset_verifier="$repo_root/scripts/verify-desktop-release-manifest-assets.sh"
956 test -x "$desktop_manifest_asset_verifier"
957 manifest_asset_directory="$test_root/desktop-manifest-assets"
958 mkdir -p "$manifest_asset_directory"
959 printf 'manifest-bound-payload\n' >"$manifest_asset_directory/payload.zip"
960 printf 'manifest-bound-native\n' >"$manifest_asset_directory/payload.deb"
961 manifest_asset_sha="$(shasum -a 256 "$manifest_asset_directory/payload.zip" | awk '{print $1}')"
962 manifest_asset_size="$(wc -c <"$manifest_asset_directory/payload.zip" | tr -d '[:space:]')"
963 manifest_native_sha="$(shasum -a 256 "$manifest_asset_directory/payload.deb" | awk '{print $1}')"
964 manifest_native_size="$(wc -c <"$manifest_asset_directory/payload.deb" | tr -d '[:space:]')"
965 jq -n \
966 --arg url "https://dl.reasonix.io/desktop-v1.2.3/payload.zip" \
967 --arg sha "$manifest_asset_sha" \
968 --argjson size "$manifest_asset_size" \
969 --arg native_url "https://dl.reasonix.io/desktop-v1.2.3/payload.deb" \
970 --arg native_sha "$manifest_native_sha" \
971 --argjson native_size "$manifest_native_size" \
972 '{
973 platforms: {test: {url: $url, sha256: $sha, size: $size}},
974 native_packages: {test: {url: $native_url, sha256: $native_sha, size: $native_size}},
975 downloads: {}
976 }' \
977 >"$manifest_asset_directory/latest.json"
978 bash "$desktop_manifest_asset_verifier" \
979 "$manifest_asset_directory/latest.json" "$manifest_asset_directory"
980 printf 'corrupted-payload\n' >"$manifest_asset_directory/payload.zip"
981 if bash "$desktop_manifest_asset_verifier" \
982 "$manifest_asset_directory/latest.json" "$manifest_asset_directory" >/dev/null 2>&1; then
983 echo "Desktop release manifest asset verifier accepted a mismatched payload" >&2
984 exit 1
985 fi
986
987 # GitHub release recovery uses the same immutable-subset rule: an interrupted
988 # release may fill missing assets, while conflicting or extra assets fail closed.
989 desktop_github_publisher="$repo_root/scripts/publish-desktop-github-release.sh"
990 test -x "$desktop_github_publisher"
991 fake_gh_bin="$test_root/fake-gh-bin"
992 fake_gh_state="$test_root/fake-gh-state"
993 mkdir -p "$fake_gh_bin" "$fake_gh_state"
994 cat >"$fake_gh_bin/gh" <<'FAKE_GH'
995 #!/usr/bin/env bash
996 set -euo pipefail
997 state="${FAKE_GH_STATE:?FAKE_GH_STATE is required}"
998 command="${1:-}"
999 shift || true
1000
1001 release_json() {
1002 if [ ! -f "$state/release.json" ]; then
1003 echo "HTTP 404: Not Found" >&2
1004 return 1
1005 fi
1006 assets='[]'
1007 if compgen -G "$state/assets/*" >/dev/null; then
1008 for asset in "$state/assets"/*; do
1009 assets="$(jq -cn --argjson current "$assets" --arg name "$(basename "$asset")" \
1010 '$current + [{name: $name}]')"
1011 done
1012 fi
1013 jq --argjson assets "$assets" '. + {assets: $assets}' "$state/release.json"
1014 }
1015
1016 case "$command" in
1017 api)
1018 release_json
1019 ;;
1020 release)
1021 subcommand="${1:-}"
1022 shift || true
1023 case "$subcommand" in
1024 create)
1025 tag="${1:?tag is required}"
1026 shift
1027 title=""
1028 notes_file=""
1029 prerelease=false
1030 while [ "$#" -gt 0 ]; do
1031 case "$1" in
1032 -R | --repo | --title | --notes-file)
1033 option="$1"
1034 value="${2:?$option requires a value}"
1035 shift 2
1036 case "$option" in
1037 --title) title="$value" ;;
1038 --notes-file) notes_file="$value" ;;
1039 esac
1040 ;;
1041 --prerelease)
1042 prerelease=true
1043 shift
1044 ;;
1045 --latest | --latest=false)
1046 shift
1047 ;;
1048 *)
1049 echo "unexpected fake gh release create argument: $1" >&2
1050 exit 2
1051 ;;
1052 esac
1053 done
1054 mkdir -p "$state/assets"
1055 jq -n --arg tag "$tag" --arg name "$title" --rawfile body "$notes_file" \
1056 --argjson prerelease "$prerelease" \
1057 '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: $prerelease}' \
1058 >"$state/release.json"
1059 ;;
1060 download)
1061 tag="${1:?tag is required}"
1062 shift
1063 pattern=""
1064 destination=""
1065 while [ "$#" -gt 0 ]; do
1066 case "$1" in
1067 -R | --repo)
1068 shift 2
1069 ;;
1070 -p | --pattern)
1071 pattern="${2:?pattern is required}"
1072 shift 2
1073 ;;
1074 -D | --dir)
1075 destination="${2:?destination is required}"
1076 shift 2
1077 ;;
1078 *)
1079 echo "unexpected fake gh release download argument: $1" >&2
1080 exit 2
1081 ;;
1082 esac
1083 done
1084 test -n "$tag"
1085 mkdir -p "$destination"
1086 if [ -n "$pattern" ]; then
1087 cp "$state/assets/$pattern" "$destination/$pattern"
1088 elif compgen -G "$state/assets/*" >/dev/null; then
1089 cp "$state/assets"/* "$destination/"
1090 fi
1091 ;;
1092 upload)
1093 tag="${1:?tag is required}"
1094 shift
1095 asset=""
1096 while [ "$#" -gt 0 ]; do
1097 case "$1" in
1098 -R | --repo)
1099 shift 2
1100 ;;
1101 *)
1102 asset="$1"
1103 shift
1104 ;;
1105 esac
1106 done
1107 test -n "$tag"
1108 mkdir -p "$state/assets"
1109 cp "$asset" "$state/assets/$(basename "$asset")"
1110 ;;
1111 *)
1112 echo "unsupported fake gh command: $command $subcommand" >&2
1113 exit 2
1114 ;;
1115 esac
1116 ;;
1117 *)
1118 echo "unsupported fake gh command: $command" >&2
1119 exit 2
1120 ;;
1121 esac
1122 FAKE_GH
1123 chmod +x "$fake_gh_bin/gh"
1124
1125 github_candidate="$test_root/desktop-github-candidate"
1126 github_notes="$test_root/desktop-github-notes.md"
1127 mkdir -p "$github_candidate"
1128 printf 'asset-a\n' >"$github_candidate/a"
1129 printf 'asset-b\n' >"$github_candidate/b"
1130 printf 'Release notes.\n' >"$github_notes"
1131 PATH="$fake_gh_bin:$PATH" FAKE_GH_STATE="$fake_gh_state" \
1132 GITHUB_REPOSITORY=esengine/DeepSeek-Reasonix \
1133 bash "$desktop_github_publisher" desktop-v1.2.3 v1.2.3 false \
1134 "$github_notes" "$github_candidate"
1135 bash "$desktop_directory_verifier" "$github_candidate" "$fake_gh_state/assets"
1136
1137 rm -f "$fake_gh_state/assets/b"
1138 PATH="$fake_gh_bin:$PATH" FAKE_GH_STATE="$fake_gh_state" \
1139 GITHUB_REPOSITORY=esengine/DeepSeek-Reasonix \
1140 bash "$desktop_github_publisher" desktop-v1.2.3 v1.2.3 false \
1141 "$github_notes" "$github_candidate"
1142 bash "$desktop_directory_verifier" "$github_candidate" "$fake_gh_state/assets"
1143
1144 printf 'conflict\n' >"$fake_gh_state/assets/a"
1145 if PATH="$fake_gh_bin:$PATH" FAKE_GH_STATE="$fake_gh_state" \
1146 GITHUB_REPOSITORY=esengine/DeepSeek-Reasonix \
1147 bash "$desktop_github_publisher" desktop-v1.2.3 v1.2.3 false \
1148 "$github_notes" "$github_candidate" >/dev/null 2>&1; then
1149 echo "Desktop GitHub recovery accepted conflicting immutable content" >&2
1150 exit 1
1151 fi
1152 cp "$github_candidate/a" "$fake_gh_state/assets/a"
1153 printf 'unexpected\n' >"$fake_gh_state/assets/unexpected"
1154 if PATH="$fake_gh_bin:$PATH" FAKE_GH_STATE="$fake_gh_state" \
1155 GITHUB_REPOSITORY=esengine/DeepSeek-Reasonix \
1156 bash "$desktop_github_publisher" desktop-v1.2.3 v1.2.3 false \
1157 "$github_notes" "$github_candidate" >/dev/null 2>&1; then
1158 echo "Desktop GitHub recovery accepted an unexpected immutable asset" >&2
1159 exit 1
1160 fi
1161 rm -f "$fake_gh_state/assets/unexpected"
1162 jq '.name = "Wrong title"' "$fake_gh_state/release.json" >"$fake_gh_state/release.json.new"
1163 mv "$fake_gh_state/release.json.new" "$fake_gh_state/release.json"
1164 if PATH="$fake_gh_bin:$PATH" FAKE_GH_STATE="$fake_gh_state" \
1165 GITHUB_REPOSITORY=esengine/DeepSeek-Reasonix \
1166 bash "$desktop_github_publisher" desktop-v1.2.3 v1.2.3 false \
1167 "$github_notes" "$github_candidate" >/dev/null 2>&1; then
1168 echo "Desktop GitHub recovery accepted conflicting release metadata" >&2
1169 exit 1
1170 fi
1171
1172 desktop_validator="$repo_root/scripts/validate-desktop-release-manifest.sh"
1173 desktop_manifest_comparator="$repo_root/scripts/compare-desktop-release-manifests.sh"
1174 test -x "$desktop_validator"
1175 test -x "$desktop_manifest_comparator"
1176 write_desktop_manifest() {
1177 local version="$1"
1178 local base="$2"
1179 local output="$3"
1180 local notes_version="${4:-$version}"
1181 jq -n --arg version "$version" --arg notes_version "$notes_version" \
1182 --arg base "$base" --arg sha "$(printf 'a%.0s' {1..64})" '
1183 def asset($name): {
1184 url: ($base + $name),
1185 sig: ($base + $name + ".minisig"),
1186 size: 42,
1187 sha256: $sha
1188 };
1189 {
1190 version: $version,
1191 download_page: "https://reasonix.io/?download=desktop#start",
1192 release_notes_url: ("https://reasonix.io/changelog/" + $notes_version + "/"),
1193 platforms: {
1194 "darwin-arm64": asset("Reasonix-darwin-arm64.zip"),
1195 "darwin-amd64": asset("Reasonix-darwin-amd64.zip"),
1196 "windows-amd64": asset("Reasonix-windows-amd64-installer.exe"),
1197 "windows-arm64": asset("Reasonix-windows-arm64-installer.exe"),
1198 "linux-amd64": asset("Reasonix-linux-amd64.tar.gz")
1199 },
1200 native_packages: {
1201 "linux-amd64": asset("Reasonix-linux-amd64.deb")
1202 },
1203 downloads: {
1204 "Reasonix-darwin-arm64.dmg": asset("Reasonix-darwin-arm64.dmg"),
1205 "Reasonix-darwin-amd64.dmg": asset("Reasonix-darwin-amd64.dmg"),
1206 "Reasonix-darwin-universal.dmg": asset("Reasonix-darwin-universal.dmg"),
1207 "Reasonix-windows-amd64.zip": asset("Reasonix-windows-amd64.zip")
1208 }
1209 }
1210 ' >"$output"
1211 }
1212
1213 desktop_stable_version="v1.2.3"
1214 desktop_stable_base="https://dl.reasonix.io/desktop-${desktop_stable_version}/"
1215 desktop_stable_manifest="$test_root/desktop-stable.json"
1216 write_desktop_manifest "$desktop_stable_version" "$desktop_stable_base" "$desktop_stable_manifest"
1217 bash "$desktop_validator" stable "$desktop_stable_version" "$desktop_stable_base" "$desktop_stable_manifest"
1218
1219 desktop_github_base="https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-${desktop_stable_version}/"
1220 desktop_github_manifest="$test_root/desktop-stable-github.json"
1221 write_desktop_manifest "$desktop_stable_version" "$desktop_github_base" "$desktop_github_manifest"
1222 bash "$desktop_validator" stable "$desktop_stable_version" "$desktop_github_base" "$desktop_github_manifest"
1223
1224 desktop_preview_version="v1.3.0-preview.42"
1225 desktop_preview_base="https://dl.reasonix.io/desktop-${desktop_preview_version}/"
1226 desktop_preview_manifest="$test_root/desktop-preview.json"
1227 write_desktop_manifest "$desktop_preview_version" "$desktop_preview_base" "$desktop_preview_manifest"
1228 bash "$desktop_validator" preview "$desktop_preview_version" "$desktop_preview_base" "$desktop_preview_manifest"
1229
1230 desktop_rc_version="v1.3.0-rc.1"
1231 desktop_rc_notes_version="v1.3.0"
1232 desktop_rc_base="https://dl.reasonix.io/desktop-${desktop_rc_version}/"
1233 desktop_rc_manifest="$test_root/desktop-rc.json"
1234 write_desktop_manifest "$desktop_rc_version" "$desktop_rc_base" "$desktop_rc_manifest" \
1235 "$desktop_rc_notes_version"
1236 bash "$desktop_validator" any "$desktop_rc_version" "$desktop_rc_base" \
1237 "$desktop_rc_manifest" "$desktop_rc_notes_version"
1238
1239 desktop_legacy_notes_manifest="$test_root/desktop-stable-legacy-notes.json"
1240 jq 'del(.release_notes_url)' "$desktop_stable_manifest" >"$desktop_legacy_notes_manifest"
1241 bash "$desktop_validator" legacy-stable "$desktop_stable_version" \
1242 "$desktop_stable_base" "$desktop_legacy_notes_manifest"
1243 bash "$desktop_manifest_comparator" "$desktop_stable_manifest" "$desktop_legacy_notes_manifest"
1244 if bash "$desktop_validator" stable "$desktop_stable_version" "$desktop_stable_base" \
1245 "$desktop_legacy_notes_manifest" >/dev/null 2>&1; then
1246 echo "strict Desktop manifest validator accepted a legacy release-notes manifest" >&2
1247 exit 1
1248 fi
1249 jq '.release_notes_url = "https://reasonix.io/changelog/v9.9.9/"' \
1250 "$desktop_stable_manifest" >"$test_root/desktop-wrong-legacy-notes.json"
1251 if bash "$desktop_manifest_comparator" "$desktop_stable_manifest" \
1252 "$test_root/desktop-wrong-legacy-notes.json" >/dev/null 2>&1; then
1253 echo "Desktop manifest comparator ignored a non-legacy difference" >&2
1254 exit 1
1255 fi
1256
1257 expect_invalid_desktop_manifest() {
1258 local description="$1"
1259 local channel="$2"
1260 local version="$3"
1261 local base="$4"
1262 local file="$5"
1263 if bash "$desktop_validator" "$channel" "$version" "$base" "$file" >/dev/null 2>&1; then
1264 echo "Desktop manifest validator accepted $description" >&2
1265 exit 1
1266 fi
1267 }
1268
1269 jq 'del(.platforms["windows-arm64"])' "$desktop_preview_manifest" >"$test_root/desktop-missing-platform.json"
1270 expect_invalid_desktop_manifest "a missing platform" preview "$desktop_preview_version" \
1271 "$desktop_preview_base" "$test_root/desktop-missing-platform.json"
1272 jq '.platforms["darwin-arm64"].url = "https://evil.invalid/file"' \
1273 "$desktop_preview_manifest" >"$test_root/desktop-hostile-url.json"
1274 expect_invalid_desktop_manifest "a hostile URL" preview "$desktop_preview_version" \
1275 "$desktop_preview_base" "$test_root/desktop-hostile-url.json"
1276 jq '.platforms["darwin-arm64"].sig += "?mirror=1"' \
1277 "$desktop_preview_manifest" >"$test_root/desktop-bad-signature.json"
1278 expect_invalid_desktop_manifest "a mismatched signature URL" preview "$desktop_preview_version" \
1279 "$desktop_preview_base" "$test_root/desktop-bad-signature.json"
1280 jq '.native_packages["linux-amd64"].size = 0' \
1281 "$desktop_preview_manifest" >"$test_root/desktop-zero-size.json"
1282 expect_invalid_desktop_manifest "a zero-byte native package" preview "$desktop_preview_version" \
1283 "$desktop_preview_base" "$test_root/desktop-zero-size.json"
1284 jq '.native_packages["linux-amd64"].size = 1073741825' \
1285 "$desktop_preview_manifest" >"$test_root/desktop-oversized-asset.json"
1286 expect_invalid_desktop_manifest "an asset above the release size limit" preview \
1287 "$desktop_preview_version" "$desktop_preview_base" \
1288 "$test_root/desktop-oversized-asset.json"
1289 jq '.native_packages["linux-amd64"].size = 9007199254740992' \
1290 "$desktop_preview_manifest" >"$test_root/desktop-unsafe-integer-size.json"
1291 expect_invalid_desktop_manifest "an asset size above the JavaScript safe-integer range" preview \
1292 "$desktop_preview_version" "$desktop_preview_base" \
1293 "$test_root/desktop-unsafe-integer-size.json"
1294 jq '.downloads["Reasonix-darwin-universal.dmg"].sha256 = ("A" * 64)' \
1295 "$desktop_preview_manifest" >"$test_root/desktop-bad-sha.json"
1296 expect_invalid_desktop_manifest "an uppercase SHA-256" preview "$desktop_preview_version" \
1297 "$desktop_preview_base" "$test_root/desktop-bad-sha.json"
1298 jq 'del(.downloads["Reasonix-windows-amd64.zip"])' \
1299 "$desktop_preview_manifest" >"$test_root/desktop-missing-download.json"
1300 expect_invalid_desktop_manifest "a missing website download" preview "$desktop_preview_version" \
1301 "$desktop_preview_base" "$test_root/desktop-missing-download.json"
1302 jq '.platforms.extra = .platforms["darwin-arm64"]' \
1303 "$desktop_preview_manifest" >"$test_root/desktop-extra-platform.json"
1304 expect_invalid_desktop_manifest "an unexpected platform" preview "$desktop_preview_version" \
1305 "$desktop_preview_base" "$test_root/desktop-extra-platform.json"
1306 write_desktop_manifest "$desktop_preview_version" "https://dl.reasonix.io/desktop-preview/" \
1307 "$test_root/desktop-rolling-preview.json"
1308 expect_invalid_desktop_manifest "the legacy mutable Preview directory" preview "$desktop_preview_version" \
1309 "$desktop_preview_base" "$test_root/desktop-rolling-preview.json"
1310 jq 'del(.downloads)' "$test_root/desktop-rolling-preview.json" \
1311 >"$test_root/desktop-legacy-preview.json"
1312 bash "$desktop_validator" legacy-preview "$desktop_preview_version" \
1313 "https://dl.reasonix.io/desktop-preview/" "$test_root/desktop-legacy-preview.json"
1314 jq 'del(.release_notes_url, .downloads)' "$desktop_preview_manifest" \
1315 >"$test_root/desktop-legacy-preview-immutable.json"
1316 bash "$desktop_validator" legacy-preview "$desktop_preview_version" \
1317 "$desktop_preview_base" "$test_root/desktop-legacy-preview-immutable.json"
1318 jq '.release_notes_url = null' "$desktop_preview_manifest" \
1319 >"$test_root/desktop-null-legacy-preview-immutable.json"
1320 bash "$desktop_validator" legacy-preview "$desktop_preview_version" \
1321 "$desktop_preview_base" "$test_root/desktop-null-legacy-preview-immutable.json"
1322 jq 'del(.downloads)' "$desktop_stable_manifest" >"$test_root/desktop-legacy-stable.json"
1323 bash "$desktop_validator" legacy-stable "$desktop_stable_version" \
1324 "$desktop_stable_base" "$test_root/desktop-legacy-stable.json"
1325 jq '.downloads = null' "$desktop_stable_manifest" >"$test_root/desktop-null-legacy-stable.json"
1326 bash "$desktop_validator" legacy-stable "$desktop_stable_version" \
1327 "$desktop_stable_base" "$test_root/desktop-null-legacy-stable.json"
1328 jq '.downloads = {}' "$desktop_stable_manifest" >"$test_root/desktop-empty-downloads.json"
1329 expect_invalid_desktop_manifest "empty downloads in a legacy Stable manifest" legacy-stable \
1330 "$desktop_stable_version" "$desktop_stable_base" "$test_root/desktop-empty-downloads.json"
1331 expect_invalid_desktop_manifest "a legacy Stable manifest as a new publication" stable \
1332 "$desktop_stable_version" "$desktop_stable_base" "$test_root/desktop-legacy-stable.json"
1333 jq 'del(.downloads["Reasonix-windows-amd64.zip"])' \
1334 "$test_root/desktop-rolling-preview.json" >"$test_root/desktop-partial-legacy-downloads.json"
1335 expect_invalid_desktop_manifest "partial downloads in a legacy Preview manifest" legacy-preview \
1336 "$desktop_preview_version" "https://dl.reasonix.io/desktop-preview/" \
1337 "$test_root/desktop-partial-legacy-downloads.json"
1338 expect_invalid_desktop_manifest "a Preview manifest as Stable" stable "$desktop_preview_version" \
1339 "$desktop_preview_base" "$desktop_preview_manifest"
1340 expect_invalid_desktop_manifest "a non-official asset base" preview "$desktop_preview_version" \
1341 "https://cdn.invalid/desktop-${desktop_preview_version}/" "$desktop_preview_manifest"
1342
1343 desktop_candidate_resolver="$repo_root/scripts/resolve-desktop-candidate.sh"
1344 test -x "$desktop_candidate_resolver"
1345
1346 git init --bare -q "$test_root/remote.git"
1347 git clone -q "$test_root/remote.git" "$test_root/repo"
1348 (
1349 cd "$test_root/repo"
1350 git config user.name "Release Workflow Test"
1351 git config user.email "release-workflow-test@example.invalid"
1352 git commit --allow-empty -q -m "candidate"
1353 git branch -M main-v2
1354 git push -q -u origin main-v2
1355
1356 git tag v1.2.3
1357 git tag npm-v1.2.3
1358 git tag -a desktop-v1.2.3 -m "desktop release"
1359 git tag v1.3.0-preview.42
1360 git push -q origin v1.2.3 npm-v1.2.3 desktop-v1.2.3 v1.3.0-preview.42
1361 GITHUB_OUTPUT="$test_root/stable.out" RELEASE_TAG=v1.2.3 "$repo_root/scripts/resolve-stable-release.sh"
1362 grep -Eq '^version=1\.2\.3$' "$test_root/stable.out"
1363 grep -Eq '^desktop_tag=desktop-v1\.2\.3$' "$test_root/stable.out"
1364 GITHUB_OUTPUT="$test_root/preview.out" RELEASE_TAG=v1.3.0-preview.42 \
1365 "$repo_root/scripts/resolve-preview-release.sh"
1366 grep -Eq '^version=1\.3\.0-preview\.42$' "$test_root/preview.out"
1367 grep -Eq '^desktop_tag=desktop-v1\.3\.0-preview\.42$' "$test_root/preview.out"
1368 grep -Eq '^npm_version=1\.3\.0-canary\.42$' "$test_root/preview.out"
1369 approved_sha="$(git rev-parse HEAD)"
1370 GITHUB_OUTPUT="$test_root/desktop-rehearsal.out" \
1371 RELEASE_CHANNEL=stable RELEASE_TAG=desktop-v1.2.3 \
1372 IN_ORCHESTRATED=true IN_ORCHESTRATOR=candidate APPROVED_SHA="$approved_sha" \
1373 CANDIDATE_PREPARATION=true CANDIDATE_REHEARSAL=true \
1374 "$desktop_candidate_resolver"
1375 grep -Eq '^sha='"$approved_sha"'$' "$test_root/desktop-rehearsal.out"
1376 if GITHUB_OUTPUT="$test_root/desktop-unsafe-rehearsal.out" \
1377 RELEASE_CHANNEL=stable RELEASE_TAG=desktop-v1.2.3 \
1378 IN_ORCHESTRATED=true IN_ORCHESTRATOR=promote APPROVED_SHA="$approved_sha" \
1379 CANDIDATE_REHEARSAL=true \
1380 "$desktop_candidate_resolver" >"$test_root/desktop-unsafe-rehearsal.log" 2>&1; then
1381 echo "rehearsal without candidate preparation unexpectedly passed" >&2
1382 exit 1
1383 fi
1384 grep -Fq 'rehearsal requires non-publishing candidate preparation' "$test_root/desktop-unsafe-rehearsal.log"
1385 GITHUB_OUTPUT="$test_root/desktop-stable-candidate.out" \
1386 RELEASE_CHANNEL=stable RELEASE_TAG=desktop-v1.2.3 \
1387 IN_ORCHESTRATED=false CALLER_EVENT_NAME=workflow_dispatch \
1388 CALLER_REF=refs/heads/main-v2 CALLER_REF_PROTECTED=true \
1389 CALLER_SHA="$approved_sha" CALLER_WORKFLOW_SHA="$approved_sha" \
1390 "$desktop_candidate_resolver"
1391 grep -Eq '^sha='"$approved_sha"'$' "$test_root/desktop-stable-candidate.out"
1392 GITHUB_OUTPUT="$test_root/desktop-preview-candidate.out" \
1393 RELEASE_CHANNEL=preview RELEASE_TAG=desktop-v1.3.0-preview.42 \
1394 IN_ORCHESTRATED=false CALLER_EVENT_NAME=workflow_dispatch \
1395 CALLER_REF=refs/heads/main-v2 CALLER_REF_PROTECTED=true \
1396 CALLER_SHA="$approved_sha" CALLER_WORKFLOW_SHA="$approved_sha" \
1397 "$desktop_candidate_resolver"
1398 grep -Eq '^sha='"$approved_sha"'$' "$test_root/desktop-preview-candidate.out"
1399 GITHUB_OUTPUT="$test_root/desktop-orchestrated-preview-candidate.out" \
1400 RELEASE_CHANNEL=preview RELEASE_TAG=desktop-v1.3.0-preview.42 \
1401 IN_ORCHESTRATED=true IN_ORCHESTRATOR=preview APPROVED_SHA="$approved_sha" \
1402 "$desktop_candidate_resolver"
1403 grep -Eq '^sha='"$approved_sha"'$' "$test_root/desktop-orchestrated-preview-candidate.out"
1404 if GITHUB_OUTPUT="$test_root/desktop-wrong-orchestrator-candidate.out" \
1405 RELEASE_CHANNEL=preview RELEASE_TAG=desktop-v1.3.0-preview.42 \
1406 IN_ORCHESTRATED=true IN_ORCHESTRATOR=stable APPROVED_SHA="$approved_sha" \
1407 "$desktop_candidate_resolver" >"$test_root/desktop-wrong-orchestrator-candidate.log" 2>&1; then
1408 echo "stable orchestrator unexpectedly authorized a Desktop Preview candidate" >&2
1409 exit 1
1410 fi
1411 grep -Eq 'stable orchestrator cannot authorize a Desktop preview candidate' \
1412 "$test_root/desktop-wrong-orchestrator-candidate.log"
1413 if GITHUB_OUTPUT="$test_root/desktop-unprotected-candidate.out" \
1414 RELEASE_CHANNEL=preview RELEASE_TAG=desktop-v1.3.0-preview.42 \
1415 IN_ORCHESTRATED=false CALLER_EVENT_NAME=workflow_dispatch \
1416 CALLER_REF=refs/heads/topic CALLER_REF_PROTECTED=false \
1417 CALLER_SHA="$approved_sha" CALLER_WORKFLOW_SHA="$approved_sha" \
1418 "$desktop_candidate_resolver" >"$test_root/desktop-unprotected-candidate.log" 2>&1; then
1419 echo "unprotected Desktop candidate unexpectedly passed" >&2
1420 exit 1
1421 fi
1422 grep -Eq 'standalone Desktop releases must run from protected main-v2' \
1423 "$test_root/desktop-unprotected-candidate.log"
1424 git tag desktop-v1.3.0-preview.42
1425 if GITHUB_OUTPUT="$test_root/desktop-tagged-preview-candidate.out" \
1426 RELEASE_CHANNEL=preview RELEASE_TAG=desktop-v1.3.0-preview.42 \
1427 IN_ORCHESTRATED=false CALLER_EVENT_NAME=workflow_dispatch \
1428 CALLER_REF=refs/heads/main-v2 CALLER_REF_PROTECTED=true \
1429 CALLER_SHA="$approved_sha" CALLER_WORKFLOW_SHA="$approved_sha" \
1430 "$desktop_candidate_resolver" >"$test_root/desktop-tagged-preview-candidate.log" 2>&1; then
1431 echo "Git-tagged Desktop Preview candidate unexpectedly passed" >&2
1432 exit 1
1433 fi
1434 grep -Eq 'Desktop Preview uses an immutable asset directory, not a Git tag' \
1435 "$test_root/desktop-tagged-preview-candidate.log"
1436 git tag -d desktop-v1.3.0-preview.42 >/dev/null
1437
1438 ACTUAL_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-stable.yml@refs/tags/v1.2.3' \
1439 EXPECTED_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-stable.yml@refs/tags/v1.2.3' \
1440 CALLER_EVENT_NAME=push CALLER_REF=refs/tags/v1.2.3 CALLER_REF_PROTECTED=true \
1441 CALLER_WORKFLOW_SHA="$approved_sha" CALLER_SHA="$approved_sha" \
1442 APPROVED_CLI_TAG=v1.2.3 APPROVED_SHA="$approved_sha" \
1443 "$repo_root/scripts/verify-release-authorization.sh"
1444 ACTUAL_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-preview.yml@refs/heads/main-v2' \
1445 EXPECTED_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-preview.yml@refs/heads/main-v2' \
1446 CALLER_EVENT_NAME=workflow_dispatch CALLER_REF=refs/heads/main-v2 CALLER_REF_PROTECTED=true \
1447 CALLER_WORKFLOW_SHA="$approved_sha" CALLER_SHA="$approved_sha" \
1448 APPROVED_CHANNEL=preview APPROVED_CLI_TAG=v1.3.0-preview.42 APPROVED_SHA="$approved_sha" \
1449 "$repo_root/scripts/verify-release-authorization.sh"
1450 RELEASE_TAG=desktop-v1.2.3 APPROVED_SHA="$approved_sha" \
1451 "$repo_root/scripts/verify-release-tag.sh"
1452
1453 git commit --allow-empty -q -m "release workflow fix"
1454 git push -q origin main-v2
1455 recovery_workflow_sha="$(git rev-parse HEAD)"
1456 if RELEASE_TAG=v1.3.0-preview.42 \
1457 "$repo_root/scripts/resolve-preview-release.sh" >"$test_root/stale-preview.log" 2>&1; then
1458 echo "stale Preview tag unexpectedly passed normal release resolution" >&2
1459 exit 1
1460 fi
1461 grep -Eq 'must point to current .*main-v2' "$test_root/stale-preview.log"
1462 GITHUB_OUTPUT="$test_root/preview-recovery.out" ALLOW_PREVIEW_RECOVERY=true \
1463 RELEASE_TAG=v1.3.0-preview.42 "$repo_root/scripts/resolve-preview-release.sh"
1464 grep -Eq '^sha='"$approved_sha"'$' "$test_root/preview-recovery.out"
1465 if ALLOW_PREVIEW_RECOVERY=invalid RELEASE_TAG=v1.3.0-preview.42 \
1466 "$repo_root/scripts/resolve-preview-release.sh" >"$test_root/invalid-preview-recovery.log" 2>&1; then
1467 echo "invalid Preview recovery mode unexpectedly passed" >&2
1468 exit 1
1469 fi
1470 grep -Eq 'ALLOW_PREVIEW_RECOVERY must be true or false' \
1471 "$test_root/invalid-preview-recovery.log"
1472 if GITHUB_OUTPUT="$test_root/desktop-stale-preview-candidate.out" \
1473 RELEASE_CHANNEL=preview RELEASE_TAG=desktop-v1.3.0-preview.42 \
1474 IN_ORCHESTRATED=false CALLER_EVENT_NAME=workflow_dispatch \
1475 CALLER_REF=refs/heads/main-v2 CALLER_REF_PROTECTED=true \
1476 CALLER_SHA="$approved_sha" CALLER_WORKFLOW_SHA="$approved_sha" \
1477 "$desktop_candidate_resolver" >"$test_root/desktop-stale-preview-candidate.log" 2>&1; then
1478 echo "stale Desktop Preview candidate unexpectedly passed" >&2
1479 exit 1
1480 fi
1481 grep -Eq 'Desktop Preview must use current main-v2' \
1482 "$test_root/desktop-stale-preview-candidate.log"
1483 GITHUB_OUTPUT="$test_root/desktop-orchestrated-preview-recovery-candidate.out" \
1484 RELEASE_CHANNEL=preview RELEASE_TAG=desktop-v1.3.0-preview.42 \
1485 IN_ORCHESTRATED=true IN_ORCHESTRATOR=preview APPROVED_SHA="$approved_sha" \
1486 "$desktop_candidate_resolver"
1487 grep -Eq '^sha='"$approved_sha"'$' \
1488 "$test_root/desktop-orchestrated-preview-recovery-candidate.out"
1489 GITHUB_OUTPUT="$test_root/desktop-stable-recovery-candidate.out" \
1490 RELEASE_CHANNEL=stable RELEASE_TAG=desktop-v1.2.3 \
1491 IN_ORCHESTRATED=false CALLER_EVENT_NAME=workflow_dispatch \
1492 CALLER_REF=refs/heads/main-v2 CALLER_REF_PROTECTED=true \
1493 CALLER_SHA="$recovery_workflow_sha" CALLER_WORKFLOW_SHA="$recovery_workflow_sha" \
1494 "$desktop_candidate_resolver"
1495 grep -Eq '^sha='"$approved_sha"'$' "$test_root/desktop-stable-recovery-candidate.out"
1496 GITHUB_OUTPUT="$test_root/stale-main.out" RELEASE_TAG=v1.2.3 \
1497 "$repo_root/scripts/resolve-stable-release.sh"
1498 grep -Eq '^sha='"$approved_sha"'$' "$test_root/stale-main.out"
1499 GITHUB_OUTPUT="$test_root/recovery.out" ALLOW_STABLE_RECOVERY=true RELEASE_TAG=v1.2.3 \
1500 "$repo_root/scripts/resolve-stable-release.sh"
1501 grep -Eq '^sha='"$approved_sha"'$' "$test_root/recovery.out"
1502 ACTUAL_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-stable.yml@refs/heads/main-v2' \
1503 EXPECTED_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-stable.yml@refs/heads/main-v2' \
1504 CALLER_EVENT_NAME=workflow_dispatch CALLER_REF=refs/heads/main-v2 CALLER_REF_PROTECTED=true \
1505 CALLER_WORKFLOW_SHA="$recovery_workflow_sha" CALLER_SHA="$recovery_workflow_sha" \
1506 APPROVED_CLI_TAG=v1.2.3 APPROVED_SHA="$approved_sha" \
1507 "$repo_root/scripts/verify-release-authorization.sh"
1508 RELEASE_TAG=v1.2.3 APPROVED_SHA="$approved_sha" VERIFY_RELEASE_CHECKOUT=false \
1509 "$repo_root/scripts/verify-release-tag.sh"
1510 if ACTUAL_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-stable.yml@refs/heads/main-v2' \
1511 EXPECTED_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-stable.yml@refs/heads/main-v2' \
1512 CALLER_EVENT_NAME=workflow_dispatch CALLER_REF=refs/heads/main-v2 CALLER_REF_PROTECTED=true \
1513 CALLER_WORKFLOW_SHA="$approved_sha" CALLER_SHA="$recovery_workflow_sha" \
1514 APPROVED_CLI_TAG=v1.2.3 APPROVED_SHA="$approved_sha" \
1515 "$repo_root/scripts/verify-release-authorization.sh" >"$test_root/stale-workflow.log" 2>&1; then
1516 echo "stale recovery workflow unexpectedly passed release authorization" >&2
1517 exit 1
1518 fi
1519 grep -Eq 'protected caller workflow SHA is' "$test_root/stale-workflow.log"
1520
1521 if ACTUAL_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-stable.yml@refs/heads/topic' \
1522 EXPECTED_CALLER_WORKFLOW_REF='example/reasonix/.github/workflows/release-stable.yml@refs/heads/topic' \
1523 CALLER_EVENT_NAME=workflow_dispatch CALLER_REF=refs/heads/topic CALLER_REF_PROTECTED=false \
1524 CALLER_WORKFLOW_SHA="$recovery_workflow_sha" CALLER_SHA="$recovery_workflow_sha" \
1525 APPROVED_CLI_TAG=v1.2.3 APPROVED_SHA="$approved_sha" \
1526 "$repo_root/scripts/verify-release-authorization.sh" >"$test_root/unprotected.log" 2>&1; then
1527 echo "unprotected caller unexpectedly passed release authorization" >&2
1528 exit 1
1529 fi
1530 grep -Eq 'caller ref is not protected' "$test_root/unprotected.log"
1531
1532 git tag v1.2.4
1533 git tag npm-v1.2.4
1534 git push -q origin v1.2.4 npm-v1.2.4
1535 if RELEASE_TAG=v1.2.4 "$repo_root/scripts/resolve-stable-release.sh" >"$test_root/missing.log" 2>&1; then
1536 echo "missing sibling tag unexpectedly passed" >&2
1537 exit 1
1538 fi
1539 grep -Eq 'required stable release tag is missing: desktop-v1\.2\.4' "$test_root/missing.log"
1540
1541 other_sha="$(git commit-tree HEAD^{tree} -p HEAD -m "other")"
1542 git tag v1.2.6 "$other_sha"
1543 git tag npm-v1.2.6 "$other_sha"
1544 git tag desktop-v1.2.6 "$other_sha"
1545 git push -q origin v1.2.6 npm-v1.2.6 desktop-v1.2.6
1546 if ALLOW_STABLE_RECOVERY=true RELEASE_TAG=v1.2.6 \
1547 "$repo_root/scripts/resolve-stable-release.sh" >"$test_root/non-ancestor.log" 2>&1; then
1548 echo "non-ancestor recovery tags unexpectedly passed release resolution" >&2
1549 exit 1
1550 fi
1551 grep -Eq 'is not an ancestor of' "$test_root/non-ancestor.log"
1552 git tag -f desktop-v1.2.3 "$other_sha" >/dev/null
1553 git push -q -f origin desktop-v1.2.3
1554 git checkout -q --detach "$approved_sha"
1555 if RELEASE_TAG=desktop-v1.2.3 APPROVED_SHA="$approved_sha" \
1556 "$repo_root/scripts/verify-release-tag.sh" >"$test_root/moved-tag.log" 2>&1; then
1557 echo "moved release tag unexpectedly passed approved SHA validation" >&2
1558 exit 1
1559 fi
1560 grep -Eq 'moved to .* after approval' "$test_root/moved-tag.log"
1561 git tag -f desktop-v1.2.3 "$approved_sha" >/dev/null
1562 git push -q -f origin desktop-v1.2.3
1563 git switch -q main-v2
1564
1565 git tag v1.2.5
1566 git tag npm-v1.2.5 "$other_sha"
1567 git tag desktop-v1.2.5
1568 git push -q origin v1.2.5 npm-v1.2.5 desktop-v1.2.5
1569 if RELEASE_TAG=v1.2.5 "$repo_root/scripts/resolve-stable-release.sh" >"$test_root/mismatch.log" 2>&1; then
1570 echo "mismatched sibling tag unexpectedly passed" >&2
1571 exit 1
1572 fi
1573 grep -Eq 'npm-v1\.2\.5 points to .* expected' "$test_root/mismatch.log"
1574
1575 if RELEASE_TAG=v1.2.3-rc.1 "$repo_root/scripts/resolve-stable-release.sh" >"$test_root/prerelease.log" 2>&1; then
1576 echo "prerelease tag unexpectedly passed stable validation" >&2
1577 exit 1
1578 fi
1579 grep -Eq 'stable release tag must be vMAJOR.MINOR.PATCH' "$test_root/prerelease.log"
1580 )
1581
1582 EVENT_NAME=push IN_CHANNEL=stable IN_TAG=desktop-v1.2.3 REF_NAME=v1.2.3 RUN_NUMBER=10 \
1583 GITHUB_OUTPUT="$test_root/desktop-stable.out" bash "$repo_root/scripts/resolve-desktop-release.sh"
1584 grep -Eq '^tag=desktop-v1\.2\.3$' "$test_root/desktop-stable.out"
1585 grep -Eq '^version=v1\.2\.3$' "$test_root/desktop-stable.out"
1586 grep -Eq '^notes_version=v1\.2\.3$' "$test_root/desktop-stable.out"
1587
1588 EVENT_NAME=workflow_call IN_ORCHESTRATED=true IN_CHANNEL=preview IN_BASE_VERSION=1.3.0 \
1589 IN_TAG='' REF_NAME=main-v2 RUN_NUMBER=999 IN_PREVIEW_NUMBER=42 \
1590 GITHUB_OUTPUT="$test_root/desktop-preview.out" bash "$repo_root/scripts/resolve-desktop-release.sh"
1591 grep -Eq '^version=v1\.3\.0-preview\.42$' "$test_root/desktop-preview.out"
1592 grep -Eq '^tag=desktop-v1\.3\.0-preview\.42$' "$test_root/desktop-preview.out"
1593 grep -Eq '^channel=preview$' "$test_root/desktop-preview.out"
1594 grep -Eq '^notes_version=v1\.3\.0-preview\.42$' "$test_root/desktop-preview.out"
1595
1596 if EVENT_NAME=workflow_call IN_ORCHESTRATED=true IN_CHANNEL=preview IN_BASE_VERSION=1.3.0 \
1597 IN_TAG=desktop-v1.3.0-preview.42 REF_NAME=main-v2 RUN_NUMBER=42 \
1598 GITHUB_OUTPUT="$test_root/desktop-preview-tagged.out" \
1599 bash "$repo_root/scripts/resolve-desktop-release.sh" \
1600 >"$test_root/desktop-preview-tagged.log" 2>&1; then
1601 echo "tagged Desktop Preview dispatch unexpectedly passed" >&2
1602 exit 1
1603 fi
1604 grep -Eq 'Desktop Preview versions are synthesized from protected main-v2' \
1605 "$test_root/desktop-preview-tagged.log"
1606
1607 # Legacy signing automation may still dispatch `canary`; normalize it to Preview
1608 # without exposing a third public release path.
1609 EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_SIGNING_PREFLIGHT=true \
1610 IN_CHANNEL=canary IN_BASE_VERSION=1.3.0 IN_TAG='' REF_NAME=main-v2 RUN_NUMBER=43 \
1611 GITHUB_OUTPUT="$test_root/desktop-canary-compat.out" bash "$repo_root/scripts/resolve-desktop-release.sh"
1612 grep -Eq '^version=v1\.3\.0-preview\.43$' "$test_root/desktop-canary-compat.out"
1613 grep -Eq '^tag=desktop-v1\.3\.0-preview\.43$' "$test_root/desktop-canary-compat.out"
1614 grep -Eq '^channel=preview$' "$test_root/desktop-canary-compat.out"
1615
1616 EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_PRODUCTION_SIGNING_SMOKE=true \
1617 IN_CHANNEL=preview IN_BASE_VERSION=1.3.0 IN_TAG='' REF_NAME=main-v2 RUN_NUMBER=44 \
1618 GITHUB_OUTPUT="$test_root/desktop-preview-smoke.out" bash "$repo_root/scripts/resolve-desktop-release.sh"
1619 grep -Eq '^version=v1\.3\.0-preview\.44$' "$test_root/desktop-preview-smoke.out"
1620
1621 if EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_CHANNEL=preview \
1622 IN_BASE_VERSION=1.3.0 IN_TAG='' REF_NAME=main-v2 RUN_NUMBER=45 \
1623 GITHUB_OUTPUT="$test_root/desktop-preview-direct.out" \
1624 bash "$repo_root/scripts/resolve-desktop-release.sh" \
1625 >"$test_root/desktop-preview-direct.log" 2>&1; then
1626 echo "standalone public Desktop Preview unexpectedly passed" >&2
1627 exit 1
1628 fi
1629 grep -Eq 'public Desktop Preview releases must be dispatched by release-preview.yml' \
1630 "$test_root/desktop-preview-direct.log"
1631
1632 EVENT_NAME=push IN_CHANNEL='' IN_TAG='' REF_NAME=desktop-v1.4.0-rc.1 RUN_NUMBER=50 \
1633 GITHUB_OUTPUT="$test_root/desktop-rc.out" bash "$repo_root/scripts/resolve-desktop-release.sh"
1634 grep -Eq '^prerelease=true$' "$test_root/desktop-rc.out"
1635 grep -Eq '^notes_version=v1\.4\.0$' "$test_root/desktop-rc.out"
1636
1637 if EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_CHANNEL=stable \
1638 IN_TAG=desktop-v1.4.0-rc.1 REF_NAME=main-v2 RUN_NUMBER=50 \
1639 GITHUB_OUTPUT="$test_root/desktop-rc-dispatch.out" \
1640 bash "$repo_root/scripts/resolve-desktop-release.sh" \
1641 >"$test_root/desktop-rc-dispatch.log" 2>&1; then
1642 echo "manual Desktop RC recovery unexpectedly passed" >&2
1643 exit 1
1644 fi
1645 grep -Eq 'manual Desktop recovery accepts only desktop-vMAJOR.MINOR.PATCH' \
1646 "$test_root/desktop-rc-dispatch.log"
1647
1648 if EVENT_NAME=workflow_dispatch IN_CHANNEL=stable IN_TAG=desktop-v1.4.0-preview.1 \
1649 REF_NAME=main-v2 RUN_NUMBER=50 GITHUB_OUTPUT="$test_root/desktop-preview-as-stable.out" \
1650 bash "$repo_root/scripts/resolve-desktop-release.sh" \
1651 >"$test_root/desktop-preview-as-stable.log" 2>&1; then
1652 echo "Desktop Preview tag unexpectedly passed as Stable" >&2
1653 exit 1
1654 fi
1655 grep -Eq 'Desktop Preview versions must use channel=preview without a Git tag' \
1656 "$test_root/desktop-preview-as-stable.log"
1657
1658 if EVENT_NAME=workflow_dispatch IN_CHANNEL=stable IN_TAG='' REF_NAME=main-v2 RUN_NUMBER=50 \
1659 GITHUB_OUTPUT="$test_root/desktop-missing-tag.out" bash "$repo_root/scripts/resolve-desktop-release.sh" \
1660 >"$test_root/desktop-missing-tag.log" 2>&1; then
1661 echo "tag-less desktop stable dispatch unexpectedly passed" >&2
1662 exit 1
1663 fi
1664 grep -Eq 'stable dispatch requires tag' "$test_root/desktop-missing-tag.log"
1665
1666 EVENT_NAME=workflow_call IN_ORCHESTRATED=true IN_CHANNEL=preview IN_TAG=v1.3.0-preview.42 \
1667 REF_NAME=main-v2 GITHUB_OUTPUT="$test_root/cli-preview-call.out" \
1668 bash "$repo_root/scripts/resolve-cli-release.sh"
1669 grep -Eq '^tag=v1\.3\.0-preview\.42$' "$test_root/cli-preview-call.out"
1670 grep -Eq '^version=1\.3\.0-preview\.42$' "$test_root/cli-preview-call.out"
1671 grep -Eq '^base_version=1\.3\.0$' "$test_root/cli-preview-call.out"
1672 grep -Eq '^notes_version=v1\.3\.0-preview\.42$' "$test_root/cli-preview-call.out"
1673 grep -Eq '^channel=preview$' "$test_root/cli-preview-call.out"
1674 grep -Eq '^prerelease=true$' "$test_root/cli-preview-call.out"
1675
1676 if EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_CHANNEL=preview \
1677 IN_TAG=v1.3.0-preview.42 REF_NAME=main-v2 CALLER_REF=refs/heads/main-v2 \
1678 CALLER_REF_PROTECTED=true GITHUB_OUTPUT="$test_root/cli-preview-dispatch.out" \
1679 bash "$repo_root/scripts/resolve-cli-release.sh" \
1680 >"$test_root/cli-preview-dispatch.log" 2>&1; then
1681 echo "standalone public CLI Preview unexpectedly passed" >&2
1682 exit 1
1683 fi
1684 grep -Eq 'manual CLI recovery accepts only vMAJOR.MINOR.PATCH' \
1685 "$test_root/cli-preview-dispatch.log"
1686
1687 EVENT_NAME=push IN_CHANNEL='' IN_TAG='' REF_NAME=v1.3.0-rc.1 \
1688 GITHUB_OUTPUT="$test_root/cli-rc.out" bash "$repo_root/scripts/resolve-cli-release.sh"
1689 grep -Eq '^channel=stable$' "$test_root/cli-rc.out"
1690 grep -Eq '^prerelease=true$' "$test_root/cli-rc.out"
1691 grep -Eq '^notes_version=v1\.3\.0$' "$test_root/cli-rc.out"
1692
1693 if EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_CHANNEL=stable \
1694 IN_TAG=v1.3.0-rc.1 REF_NAME=main-v2 CALLER_REF=refs/heads/main-v2 \
1695 CALLER_REF_PROTECTED=true GITHUB_OUTPUT="$test_root/cli-rc-dispatch.out" \
1696 bash "$repo_root/scripts/resolve-cli-release.sh" \
1697 >"$test_root/cli-rc-dispatch.log" 2>&1; then
1698 echo "manual CLI RC recovery unexpectedly passed" >&2
1699 exit 1
1700 fi
1701 grep -Eq 'manual CLI recovery accepts only vMAJOR.MINOR.PATCH' \
1702 "$test_root/cli-rc-dispatch.log"
1703
1704 if EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_CHANNEL=stable \
1705 IN_TAG=v1.3.0-preview.42 REF_NAME=main-v2 CALLER_REF=refs/heads/main-v2 \
1706 CALLER_REF_PROTECTED=true GITHUB_OUTPUT="$test_root/cli-channel-mismatch.out" \
1707 bash "$repo_root/scripts/resolve-cli-release.sh" >"$test_root/cli-channel-mismatch.log" 2>&1; then
1708 echo "CLI Preview tag unexpectedly passed as Stable" >&2
1709 exit 1
1710 fi
1711 grep -Eq 'manual CLI recovery accepts only vMAJOR.MINOR.PATCH' "$test_root/cli-channel-mismatch.log"
1712
1713 if EVENT_NAME=push IN_CHANNEL='' IN_TAG='' REF_NAME=v1.3.0-preview.latest \
1714 GITHUB_OUTPUT="$test_root/cli-invalid-preview.out" bash "$repo_root/scripts/resolve-cli-release.sh" \
1715 >"$test_root/cli-invalid-preview.log" 2>&1; then
1716 echo "malformed CLI Preview tag unexpectedly passed" >&2
1717 exit 1
1718 fi
1719 grep -Eq 'CLI Preview tag must be vMAJOR.MINOR.PATCH-preview.N' "$test_root/cli-invalid-preview.log"
1720
1721 if EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_CHANNEL=preview \
1722 IN_TAG=v1.3.0-preview.42 REF_NAME=topic CALLER_REF=refs/heads/topic \
1723 CALLER_REF_PROTECTED=false GITHUB_OUTPUT="$test_root/cli-unprotected.out" \
1724 bash "$repo_root/scripts/resolve-cli-release.sh" >"$test_root/cli-unprotected.log" 2>&1; then
1725 echo "unprotected CLI Preview dispatch unexpectedly passed" >&2
1726 exit 1
1727 fi
1728 grep -Eq 'manual CLI releases must run from protected main-v2' "$test_root/cli-unprotected.log"
1729
1730 EVENT_NAME=push IN_ORCHESTRATED=false IN_CHANNEL='' IN_BASE_VERSION='' IN_TAG='' \
1731 REF_NAME=npm-v1.4.0-rc.1 RUN_NUMBER=50 GITHUB_OUTPUT="$test_root/npm-rc.out" \
1732 bash "$repo_root/scripts/resolve-npm-release.sh"
1733 grep -Eq '^arg=npm-v1\.4\.0-rc\.1$' "$test_root/npm-rc.out"
1734
1735 EVENT_NAME=push IN_ORCHESTRATED=true IN_CHANNEL=stable IN_BASE_VERSION=1.5.0 \
1736 IN_TAG=npm-v1.5.0 REF_NAME=v1.5.0 RUN_NUMBER=51 GITHUB_OUTPUT="$test_root/npm-stable.out" \
1737 bash "$repo_root/scripts/resolve-npm-release.sh"
1738 grep -Eq '^arg=v1\.5\.0$' "$test_root/npm-stable.out"
1739
1740 EVENT_NAME=workflow_call IN_ORCHESTRATED=true IN_CHANNEL=canary IN_BASE_VERSION=1.5.0 \
1741 IN_TAG='' REF_NAME=main-v2 RUN_NUMBER=999 IN_PREVIEW_NUMBER=42 \
1742 GITHUB_OUTPUT="$test_root/npm-canary.out" bash "$repo_root/scripts/resolve-npm-release.sh"
1743 grep -Eq '^arg=v1\.5\.0-canary\.42$' "$test_root/npm-canary.out"
1744
1745 if EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_CHANNEL=canary IN_BASE_VERSION=1.5.0 \
1746 IN_TAG='' REF_NAME=main-v2 RUN_NUMBER=52 GITHUB_OUTPUT="$test_root/npm-canary-direct.out" \
1747 bash "$repo_root/scripts/resolve-npm-release.sh" \
1748 >"$test_root/npm-canary-direct.log" 2>&1; then
1749 echo "standalone public npm Canary unexpectedly passed" >&2
1750 exit 1
1751 fi
1752 grep -Eq 'public npm Canary releases must be dispatched by release-preview.yml' \
1753 "$test_root/npm-canary-direct.log"
1754
1755 if EVENT_NAME=workflow_dispatch IN_ORCHESTRATED=false IN_CHANNEL=stable IN_BASE_VERSION=1.5.0 \
1756 IN_TAG=npm-v1.5.1 REF_NAME=main-v2 RUN_NUMBER=52 GITHUB_OUTPUT="$test_root/npm-mismatch.out" \
1757 bash "$repo_root/scripts/resolve-npm-release.sh" >"$test_root/npm-mismatch.log" 2>&1; then
1758 echo "mismatched npm stable dispatch unexpectedly passed" >&2
1759 exit 1
1760 fi
1761 grep -Eq 'does not match requested version' "$test_root/npm-mismatch.log"
1762
1763 e2e_workflow="$repo_root/.github/workflows/e2e-bot.yml"
1764 grep -Fq 'REASONIX_HOME: ${{ runner.temp }}/reasonix-e2e-home' "$e2e_workflow"
1765 grep -Fq 'cp /tmp/reasonix-e2e.toml "$REASONIX_HOME/config.toml"' "$e2e_workflow"
1766 grep -Fq "printf 'DEEPSEEK_API_KEY=%s\\n' \"\$DEEPSEEK_API_KEY\" > \"\$REASONIX_HOME/.env\"" "$e2e_workflow"
1767 grep -Fq -- '-task "compaction,fix-add-bug,fizzbuzz,palindrome,subagent-delegation"' "$e2e_workflow"
1768 grep -Fq 'const unsuccessful = results.filter((result) => !result.Passed || result.Skipped);' "$e2e_workflow"
1769 grep -Fq "if: always() && hashFiles('report.md') != ''" "$e2e_workflow"
1770 e2e_missing_key="$(grep -A2 -F 'missing DEEPSEEK_API_KEY secret' "$e2e_workflow")"
1771 if grep -Fq 'exit 0' <<<"$e2e_missing_key"; then
1772 echo "e2e bot still treats a missing provider secret as success" >&2
1773 exit 1
1774 fi
1775
1776 node --test "$repo_root/npm/publish.test.mjs"
1777 node --test "$repo_root/scripts/finalize-npm-official-release.test.mjs"
1778 node --test "$repo_root/scripts/package-desktop-dmg.test.mjs"
1779 node "$repo_root/scripts/check-desktop-build-contract.mjs"
1780 bash "$repo_root/scripts/release-stable.test.sh"
1781 bash "$repo_root/scripts/check-cache-impact.test.sh"
1782 bash "$repo_root/scripts/check-docs-impact.test.sh"
1783
1784 # Each build orchestrator must gate on the compiled docs identity once. The
1785 # Desktop child consumes the candidate evidence instead of rerunning the guard.
1786 for workflow in release.yml release-npm.yml release-candidate.yml; do
1787 grep -Fq 'bash scripts/verify-embedded-docs.sh "$DOCS_BUILD_VERSION"' \
1788 "$repo_root/.github/workflows/$workflow"
1789 done
1790 grep -Fq 'reasonix/internal/productdocs.linkedVersion={{ .Tag }}' "$repo_root/.goreleaser.yaml"
1791 grep -Fq 'reasonix/internal/productdocs.linkedRevision={{ .Commit }}' "$repo_root/.goreleaser.yaml"
1792 # The Homebrew cask must keep stripping quarantine from the unsigned CLI, but
1793 # through Homebrew's current postflight_steps stanza, never the deprecated
1794 # `postflight do` that GoReleaser's hooks field renders.
1795 homebrew_config="$(sed -n '/^homebrew_casks:/,/^release:/p' "$repo_root/.goreleaser.yaml")"
1796 grep -Fq 'postflight_steps do' <<<"$homebrew_config"
1797 grep -Fq 'com.apple.quarantine' <<<"$homebrew_config"
1798 ! grep -Eq '^\s+hooks:|^\s+post:' <<<"$homebrew_config"
1799 grep -Fq 'reasonix/internal/productdocs.linkedVersion=${binaryVersion}' "$repo_root/npm/build.mjs"
1800 grep -Fq 'product_docs_ldflags="-X reasonix/internal/productdocs.linkedVersion=$VERSION' \
1801 "$repo_root/scripts/desktop-build.sh"
1802
1803 echo "release workflow contract tests: PASS"
1804
1804 lines BASH