返回 DeepSeek-Reasonix
release-stable.yml
根目录 / .github / workflows / release-stable.yml
1 name: Legacy release recovery
2 run-name: Legacy recovery ${{ inputs.tag || github.ref_name }}
3
4 # Compatibility recovery for releases created before sealed candidates. New
5 # releases use release-candidate.yml followed by release-promote.yml. This path
6 # retains one GitHub environment gate and verifies all three existing tags.
7 # Keeping the control-plane ref on main-v2 lets SignPath restrict
8 # production signing to that one protected origin instead of trusting wildcard
9 # tag-like branch names. Manual recovery uses the same fixed control plane while
10 # preserving an older tagged candidate on main-v2 history.
11 on:
12 workflow_dispatch:
13 inputs:
14 tag:
15 description: "Existing stable CLI tag to recover (for example v1.18.0)"
16 required: true
17 type: string
18 publish_cli:
19 description: "Recover the CLI/Homebrew channel"
20 required: false
21 default: true
22 type: boolean
23 publish_npm:
24 description: "Recover the npm channel"
25 required: false
26 default: true
27 type: boolean
28 publish_desktop:
29 description: "Recover the Desktop/R2 channel"
30 required: false
31 default: true
32 type: boolean
33 allow_recovery:
34 description: "Allow an existing stable tag on main-v2 history (manual recovery only)"
35 required: false
36 default: true
37 type: boolean
38 desktop_manual_only:
39 description: "Approved exception: unsigned Windows, manual Desktop downloads only"
40 required: false
41 default: false
42 type: boolean
43 reuse_manual_artifacts:
44 description: "Recover the verified v1.38.8 artifacts from run 34816299501 (v1.38.8 only)"
45 required: false
46 default: false
47 type: boolean
48
49 concurrency:
50 group: stable-release-publication
51 cancel-in-progress: false
52
53 # Reusable release workflows can only reduce caller permissions, so the
54 # orchestrator grants the union needed by CLI/Desktop publication.
55 permissions:
56 actions: write
57 contents: write
58 issues: read # release-notes credits read PR and issue authors
59 pull-requests: read
60
61 jobs:
62 preflight:
63 name: validate stable release set
64 runs-on: ubuntu-latest
65 permissions:
66 actions: read
67 contents: read
68 issues: read # release-notes credits read PR and issue authors
69 pull-requests: read
70 outputs:
71 version: ${{ steps.release.outputs.version }}
72 cli_tag: ${{ steps.release.outputs.cli_tag }}
73 npm_tag: ${{ steps.release.outputs.npm_tag }}
74 desktop_tag: ${{ steps.release.outputs.desktop_tag }}
75 sha: ${{ steps.release.outputs.sha }}
76 steps:
77 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
78 with:
79 fetch-depth: 0
80 # A recovery dispatch uses the fixed workflow/scripts from protected
81 # main-v2. Publishers still check out the immutable approved tag SHA.
82 ref: ${{ github.sha }}
83 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
84 with:
85 go-version-file: go.mod
86 cache: true
87 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
88 with:
89 node-version: "22"
90 - name: Resolve stable release
91 id: release
92 env:
93 RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
94 ALLOW_STABLE_RECOVERY: ${{ inputs.allow_recovery }}
95 run: bash scripts/resolve-stable-release.sh
96 # scripts/manual-desktop-exception.sh owns which tags may ship without
97 # Windows Authenticode. A tag approved before its candidate exists keeps
98 # the normal candidate and push-CI validation below.
99 - name: Restrict manual Desktop distribution
100 if: ${{ inputs.desktop_manual_only }}
101 run: |
102 bash scripts/manual-desktop-exception.sh validate \
103 "${{ steps.release.outputs.desktop_tag }}" \
104 "${{ steps.release.outputs.sha }}" \
105 "${{ inputs.allow_recovery }}"
106 - name: Revalidate normal release candidate and exact push CI
107 if: ${{ !inputs.allow_recovery }}
108 env:
109 GH_TOKEN: ${{ github.token }}
110 RELEASE_REPOSITORY: ${{ github.repository }}
111 RELEASE_CI_WAIT_SECONDS: 60
112 RELEASE_VERSION: ${{ steps.release.outputs.version }}
113 RELEASE_SHA: ${{ steps.release.outputs.sha }}
114 run: |
115 bash scripts/validate-stable-candidate.sh "$RELEASE_VERSION" "$RELEASE_SHA"
116 bash scripts/verify-release-push-ci.sh "$RELEASE_SHA"
117 - name: Validate reviewed release notes
118 env:
119 GH_TOKEN: ${{ github.token }}
120 run: node scripts/release-notes.mjs render --version "${{ steps.release.outputs.cli_tag }}" --output /tmp/release-notes.md
121 # Recovery builds deliberately check out the immutable tagged candidate,
122 # which can predate its reviewed release-note entry. Carry the exact file
123 # validated by this protected control-plane job into both publishers.
124 - name: Upload reviewed release notes
125 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
126 with:
127 name: orchestrator-reviewed-release-notes
128 path: /tmp/release-notes.md
129 if-no-files-found: error
130 retention-days: 1
131 - name: Cache hit guard
132 run: ./scripts/cache-guard.sh
133
134 authorize:
135 name: approve stable release
136 needs: preflight
137 runs-on: ubuntu-latest
138 environment: release
139 permissions:
140 contents: read
141 outputs:
142 version: ${{ steps.approved.outputs.version }}
143 cli_tag: ${{ steps.approved.outputs.cli_tag }}
144 npm_tag: ${{ steps.approved.outputs.npm_tag }}
145 desktop_tag: ${{ steps.approved.outputs.desktop_tag }}
146 sha: ${{ steps.approved.outputs.sha }}
147 steps:
148 - name: Record approved release
149 id: approved
150 env:
151 VERSION: ${{ needs.preflight.outputs.version }}
152 CLI_TAG: ${{ needs.preflight.outputs.cli_tag }}
153 NPM_TAG: ${{ needs.preflight.outputs.npm_tag }}
154 DESKTOP_TAG: ${{ needs.preflight.outputs.desktop_tag }}
155 RELEASE_SHA: ${{ needs.preflight.outputs.sha }}
156 run: |
157 {
158 echo "version=$VERSION"
159 echo "cli_tag=$CLI_TAG"
160 echo "npm_tag=$NPM_TAG"
161 echo "desktop_tag=$DESKTOP_TAG"
162 echo "sha=$RELEASE_SHA"
163 } >> "$GITHUB_OUTPUT"
164 echo "Approved stable release $VERSION at $RELEASE_SHA"
165
166 cli:
167 name: publish CLI and Homebrew
168 needs: [authorize, signpath-preflight]
169 if: ${{ always() && !cancelled() && needs.authorize.result == 'success' && (needs.signpath-preflight.result == 'success' || needs.signpath-preflight.result == 'skipped') && (github.event_name != 'workflow_dispatch' || inputs.publish_cli) }}
170 uses: ./.github/workflows/release.yml
171 with:
172 tag: ${{ needs.authorize.outputs.cli_tag }}
173 approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
174 approved_sha: ${{ needs.authorize.outputs.sha }}
175 orchestrated: true
176 secrets: inherit
177
178 npm:
179 name: publish npm
180 needs: [authorize, signpath-preflight]
181 if: ${{ always() && !cancelled() && needs.authorize.result == 'success' && (needs.signpath-preflight.result == 'success' || needs.signpath-preflight.result == 'skipped') && (github.event_name != 'workflow_dispatch' || inputs.publish_npm) }}
182 # A reusable workflow only receives permissions the caller grants here.
183 permissions:
184 contents: read
185 id-token: write
186 uses: ./.github/workflows/release-npm.yml
187 with:
188 channel: stable
189 base_version: ${{ needs.authorize.outputs.version }}
190 tag: ${{ needs.authorize.outputs.npm_tag }}
191 approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
192 approved_sha: ${{ needs.authorize.outputs.sha }}
193 orchestrated: true
194 secrets: inherit
195
196 signpath-preflight:
197 name: verify stable SignPath control plane
198 needs: authorize
199 if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_desktop }}
200 uses: ./.github/workflows/release-desktop.yml
201 with:
202 channel: stable
203 tag: ${{ needs.authorize.outputs.desktop_tag }}
204 approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
205 approved_sha: ${{ needs.authorize.outputs.sha }}
206 orchestrated: true
207 signing_preflight: true
208 desktop_manual_only: ${{ inputs.desktop_manual_only || false }}
209 reuse_manual_artifacts: ${{ inputs.reuse_manual_artifacts || false }}
210 secrets: inherit
211
212 desktop:
213 name: publish desktop
214 needs: [authorize, signpath-preflight]
215 if: ${{ always() && !cancelled() && needs.authorize.result == 'success' && needs.signpath-preflight.result == 'success' && (github.event_name != 'workflow_dispatch' || inputs.publish_desktop) }}
216 uses: ./.github/workflows/release-desktop.yml
217 with:
218 channel: stable
219 tag: ${{ needs.authorize.outputs.desktop_tag }}
220 approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
221 approved_sha: ${{ needs.authorize.outputs.sha }}
222 orchestrated: true
223 signing_preflight_verified: true
224 desktop_manual_only: ${{ inputs.desktop_manual_only || false }}
225 reuse_manual_artifacts: ${{ inputs.reuse_manual_artifacts || false }}
226 preflight_artifact_prefix: ${{ needs.signpath-preflight.outputs.artifact_prefix }}
227 secrets: inherit
228
229 postflight:
230 name: verify stable release artifacts
231 needs: [authorize, cli, npm, desktop]
232 if: ${{ always() && !cancelled() }}
233 runs-on: ubuntu-latest
234 permissions:
235 contents: write
236 steps:
237 - name: Require every publisher to succeed
238 env:
239 PUBLISH_CLI: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_cli }}
240 PUBLISH_NPM: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_npm }}
241 PUBLISH_DESKTOP: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_desktop }}
242 CLI_RESULT: ${{ needs.cli.result }}
243 NPM_RESULT: ${{ needs.npm.result }}
244 DESKTOP_RESULT: ${{ needs.desktop.result }}
245 run: |
246 set -euo pipefail
247 for channel in cli npm desktop; do
248 selected_var="PUBLISH_${channel^^}"
249 result_var="${channel^^}_RESULT"
250 selected="${!selected_var}"
251 result="${!result_var}"
252 if [ "$selected" != "true" ]; then
253 echo "$channel recovery skipped; public postflight will still verify it"
254 continue
255 fi
256 if [ "$result" != "success" ]; then
257 echo "::error::$channel stable publisher result is $result, expected success"
258 exit 1
259 fi
260 done
261 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
262 with:
263 # Postflight belongs to the trusted control plane, not the old build
264 # candidate, which may predate this verifier.
265 ref: ${{ github.sha }}
266 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
267 with:
268 node-version: "22"
269 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
270 with:
271 go-version-file: go.mod
272 cache: false
273 - name: Verify public artifacts and npm latest
274 env:
275 GH_TOKEN: ${{ github.token }}
276 RELEASE_REPOSITORY: ${{ github.repository }}
277 RELEASE_VERSION: ${{ needs.authorize.outputs.version }}
278 CLI_TAG: ${{ needs.authorize.outputs.cli_tag }}
279 DESKTOP_TAG: ${{ needs.authorize.outputs.desktop_tag }}
280 DESKTOP_MANUAL_ONLY: ${{ inputs.desktop_manual_only || false }}
281 CLI_PUBLISH_FROZEN: ${{ vars.CLI_PUBLISH_FROZEN == 'true' }}
282 run: bash scripts/verify-stable-release-artifacts.sh
283 - name: Publish exact Stable release record
284 env:
285 GH_TOKEN: ${{ github.token }}
286 VERSION: ${{ needs.authorize.outputs.version }}
287 CLI_TAG: ${{ needs.authorize.outputs.cli_tag }}
288 RELEASE_SHA: ${{ needs.authorize.outputs.sha }}
289 run: |
290 set -euo pipefail
291 if ! node -e '
292 const catalog = require("./release-notes/releases.json");
293 const release = catalog.releases.find((item) => item.version === process.env.VERSION);
294 process.exit(release?.status === "reviewed" ? 0 : 1);
295 '; then
296 echo "Legacy Stable notes do not require a publication marker"
297 exit 0
298 fi
299 node scripts/release-event.mjs generate \
300 --version "$VERSION" --sha "$RELEASE_SHA" \
301 --published-at "$(gh api "repos/${{ github.repository }}/releases/tags/$CLI_TAG" --jq .published_at)" \
302 --output /tmp/release-event.json
303 existing="$(mktemp -d)"
304 if gh release download "$CLI_TAG" --pattern release-event.json --dir "$existing" 2>/dev/null; then
305 cmp -s /tmp/release-event.json "$existing/release-event.json" || {
306 echo "::error::published release-event.json differs from the approved Stable event"
307 exit 1
308 }
309 else
310 gh release upload "$CLI_TAG" /tmp/release-event.json
311 fi
312
312 lines YAML