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