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