返回 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-${{ inputs.tag || github.ref_name }}
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
59 jobs:
60 preflight:
61 name: validate stable release set
62 runs-on: ubuntu-latest
63 permissions:
64 actions: read
65 contents: read
66 outputs:
67 version: ${{ steps.release.outputs.version }}
68 cli_tag: ${{ steps.release.outputs.cli_tag }}
69 npm_tag: ${{ steps.release.outputs.npm_tag }}
70 desktop_tag: ${{ steps.release.outputs.desktop_tag }}
71 sha: ${{ steps.release.outputs.sha }}
72 steps:
73 - uses: actions/checkout@v7
74 with:
75 fetch-depth: 0
76 # A recovery dispatch uses the fixed workflow/scripts from protected
77 # main-v2. Publishers still check out the immutable approved tag SHA.
78 ref: ${{ github.sha }}
79 - uses: actions/setup-go@v7
80 with:
81 go-version-file: go.mod
82 cache: true
83 - uses: actions/setup-node@v7
84 with:
85 node-version: "22"
86 - name: Resolve stable release
87 id: release
88 env:
89 RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
90 ALLOW_STABLE_RECOVERY: ${{ inputs.allow_recovery }}
91 run: bash scripts/resolve-stable-release.sh
92 # scripts/manual-desktop-exception.sh owns which tags may ship without
93 # Windows Authenticode. A tag approved before its candidate exists keeps
94 # the normal candidate and push-CI validation below.
95 - name: Restrict manual Desktop distribution
96 if: ${{ inputs.desktop_manual_only }}
97 run: |
98 bash scripts/manual-desktop-exception.sh validate \
99 "${{ steps.release.outputs.desktop_tag }}" \
100 "${{ steps.release.outputs.sha }}" \
101 "${{ inputs.allow_recovery }}"
102 - name: Revalidate normal release candidate and exact push CI
103 if: ${{ !inputs.allow_recovery }}
104 env:
105 GH_TOKEN: ${{ github.token }}
106 RELEASE_REPOSITORY: ${{ github.repository }}
107 RELEASE_CI_WAIT_SECONDS: 60
108 RELEASE_VERSION: ${{ steps.release.outputs.version }}
109 RELEASE_SHA: ${{ steps.release.outputs.sha }}
110 run: |
111 bash scripts/validate-stable-candidate.sh "$RELEASE_VERSION" "$RELEASE_SHA"
112 bash scripts/verify-release-push-ci.sh "$RELEASE_SHA"
113 - name: Validate reviewed release notes
114 run: node scripts/release-notes.mjs render --version "${{ steps.release.outputs.cli_tag }}" --output /tmp/release-notes.md
115 # Recovery builds deliberately check out the immutable tagged candidate,
116 # which can predate its reviewed release-note entry. Carry the exact file
117 # validated by this protected control-plane job into both publishers.
118 - name: Upload reviewed release notes
119 uses: actions/upload-artifact@v7
120 with:
121 name: orchestrator-reviewed-release-notes
122 path: /tmp/release-notes.md
123 if-no-files-found: error
124 retention-days: 1
125 - name: Cache hit guard
126 run: ./scripts/cache-guard.sh
127
128 authorize:
129 name: approve stable release
130 needs: preflight
131 runs-on: ubuntu-latest
132 environment: release
133 permissions:
134 contents: read
135 outputs:
136 version: ${{ steps.approved.outputs.version }}
137 cli_tag: ${{ steps.approved.outputs.cli_tag }}
138 npm_tag: ${{ steps.approved.outputs.npm_tag }}
139 desktop_tag: ${{ steps.approved.outputs.desktop_tag }}
140 sha: ${{ steps.approved.outputs.sha }}
141 steps:
142 - name: Record approved release
143 id: approved
144 env:
145 VERSION: ${{ needs.preflight.outputs.version }}
146 CLI_TAG: ${{ needs.preflight.outputs.cli_tag }}
147 NPM_TAG: ${{ needs.preflight.outputs.npm_tag }}
148 DESKTOP_TAG: ${{ needs.preflight.outputs.desktop_tag }}
149 RELEASE_SHA: ${{ needs.preflight.outputs.sha }}
150 run: |
151 {
152 echo "version=$VERSION"
153 echo "cli_tag=$CLI_TAG"
154 echo "npm_tag=$NPM_TAG"
155 echo "desktop_tag=$DESKTOP_TAG"
156 echo "sha=$RELEASE_SHA"
157 } >> "$GITHUB_OUTPUT"
158 echo "Approved stable release $VERSION at $RELEASE_SHA"
159
160 cli:
161 name: publish CLI and Homebrew
162 needs: [authorize, signpath-preflight]
163 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) }}
164 uses: ./.github/workflows/release.yml
165 with:
166 tag: ${{ needs.authorize.outputs.cli_tag }}
167 approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
168 approved_sha: ${{ needs.authorize.outputs.sha }}
169 orchestrated: true
170 secrets: inherit
171
172 npm:
173 name: publish npm
174 needs: [authorize, signpath-preflight]
175 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) }}
176 # A reusable workflow only receives permissions the caller grants here.
177 permissions:
178 contents: read
179 id-token: write
180 uses: ./.github/workflows/release-npm.yml
181 with:
182 channel: stable
183 base_version: ${{ needs.authorize.outputs.version }}
184 tag: ${{ needs.authorize.outputs.npm_tag }}
185 approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
186 approved_sha: ${{ needs.authorize.outputs.sha }}
187 orchestrated: true
188 secrets: inherit
189
190 signpath-preflight:
191 name: verify stable SignPath control plane
192 needs: authorize
193 if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_desktop }}
194 uses: ./.github/workflows/release-desktop.yml
195 with:
196 channel: stable
197 tag: ${{ needs.authorize.outputs.desktop_tag }}
198 approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
199 approved_sha: ${{ needs.authorize.outputs.sha }}
200 orchestrated: true
201 signing_preflight: true
202 desktop_manual_only: ${{ inputs.desktop_manual_only || false }}
203 reuse_manual_artifacts: ${{ inputs.reuse_manual_artifacts || false }}
204 secrets: inherit
205
206 desktop:
207 name: publish desktop
208 needs: [authorize, signpath-preflight]
209 if: ${{ always() && !cancelled() && needs.authorize.result == 'success' && needs.signpath-preflight.result == 'success' && (github.event_name != 'workflow_dispatch' || inputs.publish_desktop) }}
210 uses: ./.github/workflows/release-desktop.yml
211 with:
212 channel: stable
213 tag: ${{ needs.authorize.outputs.desktop_tag }}
214 approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
215 approved_sha: ${{ needs.authorize.outputs.sha }}
216 orchestrated: true
217 signing_preflight_verified: true
218 desktop_manual_only: ${{ inputs.desktop_manual_only || false }}
219 reuse_manual_artifacts: ${{ inputs.reuse_manual_artifacts || false }}
220 preflight_artifact_prefix: ${{ needs.signpath-preflight.outputs.artifact_prefix }}
221 secrets: inherit
222
223 postflight:
224 name: verify stable release artifacts
225 needs: [authorize, cli, npm, desktop]
226 if: ${{ always() && !cancelled() }}
227 runs-on: ubuntu-latest
228 permissions:
229 actions: write
230 contents: write
231 steps:
232 - name: Require every publisher to succeed
233 env:
234 PUBLISH_CLI: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_cli }}
235 PUBLISH_NPM: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_npm }}
236 PUBLISH_DESKTOP: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_desktop }}
237 CLI_RESULT: ${{ needs.cli.result }}
238 NPM_RESULT: ${{ needs.npm.result }}
239 DESKTOP_RESULT: ${{ needs.desktop.result }}
240 run: |
241 set -euo pipefail
242 for channel in cli npm desktop; do
243 selected_var="PUBLISH_${channel^^}"
244 result_var="${channel^^}_RESULT"
245 selected="${!selected_var}"
246 result="${!result_var}"
247 if [ "$selected" != "true" ]; then
248 echo "$channel recovery skipped; public postflight will still verify it"
249 continue
250 fi
251 if [ "$result" != "success" ]; then
252 echo "::error::$channel stable publisher result is $result, expected success"
253 exit 1
254 fi
255 done
256 - uses: actions/checkout@v7
257 with:
258 # Postflight belongs to the trusted control plane, not the old build
259 # candidate, which may predate this verifier.
260 ref: ${{ github.sha }}
261 - uses: actions/setup-node@v7
262 with:
263 node-version: "22"
264 - name: Verify public artifacts and npm latest
265 env:
266 GH_TOKEN: ${{ github.token }}
267 RELEASE_REPOSITORY: ${{ github.repository }}
268 RELEASE_VERSION: ${{ needs.authorize.outputs.version }}
269 CLI_TAG: ${{ needs.authorize.outputs.cli_tag }}
270 DESKTOP_TAG: ${{ needs.authorize.outputs.desktop_tag }}
271 DESKTOP_MANUAL_ONLY: ${{ inputs.desktop_manual_only || false }}
272 run: bash scripts/verify-stable-release-artifacts.sh
273 - name: Publish exact Stable release record
274 env:
275 GH_TOKEN: ${{ github.token }}
276 VERSION: ${{ needs.authorize.outputs.version }}
277 CLI_TAG: ${{ needs.authorize.outputs.cli_tag }}
278 RELEASE_SHA: ${{ needs.authorize.outputs.sha }}
279 run: |
280 set -euo pipefail
281 if ! node -e '
282 const catalog = require("./release-notes/releases.json");
283 const release = catalog.releases.find((item) => item.version === process.env.VERSION);
284 process.exit(release?.status === "reviewed" ? 0 : 1);
285 '; then
286 echo "Legacy Stable notes do not require a publication marker"
287 exit 0
288 fi
289 node scripts/release-event.mjs generate \
290 --version "$VERSION" --sha "$RELEASE_SHA" \
291 --published-at "$(gh api "repos/${{ github.repository }}/releases/tags/$CLI_TAG" --jq .published_at)" \
292 --output /tmp/release-event.json
293 existing="$(mktemp -d)"
294 if gh release download "$CLI_TAG" --pattern release-event.json --dir "$existing" 2>/dev/null; then
295 cmp -s /tmp/release-event.json "$existing/release-event.json" || {
296 echo "::error::published release-event.json differs from the approved Stable event"
297 exit 1
298 }
299 else
300 gh release upload "$CLI_TAG" /tmp/release-event.json
301 fi
302 - name: Refresh public changelog
303 env:
304 GH_TOKEN: ${{ github.token }}
305 run: gh workflow run pages.yml --ref main-v2
306
306 lines YAML