返回 DeepSeek-Reasonix
release-promote.yml
根目录 / .github / workflows / release-promote.yml
1 name: Publish release candidate
2 run-name: ${{ inputs.operation }} ${{ inputs.candidate_id }}
3
4 on:
5 workflow_dispatch:
6 inputs:
7 candidate_id:
8 description: "Sealed candidate from Prepare release candidate"
9 required: true
10 type: string
11 operation:
12 description: "Publish a new identity or recover an interrupted publication"
13 required: true
14 default: publish
15 type: choice
16 options: [publish, recover]
17
18 concurrency:
19 group: stable-release-publication
20 cancel-in-progress: false
21
22 permissions:
23 actions: write
24 attestations: read
25 contents: write
26 issues: read # granted to the nested release workflows, which can render notes
27 pull-requests: read
28
29 jobs:
30 preflight:
31 name: verify sealed candidate before approval
32 if: github.repository == 'esengine/DeepSeek-Reasonix' && github.ref == 'refs/heads/main-v2' && github.ref_protected
33 runs-on: ubuntu-latest
34 permissions:
35 actions: read
36 attestations: read
37 contents: read
38 outputs:
39 tag_actor_id: ${{ steps.identity.outputs.actor_id }}
40 candidate_id: ${{ steps.record.outputs.candidate_id }}
41 version: ${{ steps.record.outputs.version }}
42 source_sha: ${{ steps.record.outputs.source_sha }}
43 producer_run_id: ${{ steps.record.outputs.producer_run_id }}
44 producer_run_attempt: ${{ steps.record.outputs.producer_run_attempt }}
45 candidate_control_sha: ${{ steps.record.outputs.candidate_control_sha }}
46 signing_fingerprint: ${{ steps.record.outputs.signing_fingerprint }}
47 desktop_prefix: ${{ steps.record.outputs.desktop_prefix }}
48 approved_artifact: approved-release-candidate-${{ github.run_id }}-${{ github.run_attempt }}
49 steps:
50 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
51 with:
52 fetch-depth: 0
53 ref: ${{ github.sha }}
54 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
55 with:
56 node-version: "22"
57 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
58 with:
59 go-version-file: go.mod
60 cache: false
61 - name: Validate executable contracts and public access before payload or approval
62 env:
63 CANDIDATE_ID: ${{ inputs.candidate_id }}
64 run: |
65 bash scripts/validate-release-control-plane.sh
66 version="${CANDIDATE_ID#v}"
67 bash scripts/check-release-public-access.sh "${version%%-*}"
68 - name: Resolve candidate record artifact
69 id: artifact
70 env:
71 GH_TOKEN: ${{ github.token }}
72 RELEASE_REVOKED_CANDIDATES: ${{ vars.RELEASE_REVOKED_CANDIDATES }}
73 run: node scripts/resolve-release-candidate.mjs resolve "${{ inputs.candidate_id }}"
74 - name: Download exact candidate record
75 env:
76 GH_TOKEN: ${{ github.token }}
77 ARTIFACT_ID: ${{ steps.artifact.outputs.record_artifact_id }}
78 PRODUCER_RUN_ID: ${{ steps.artifact.outputs.producer_run_id }}
79 run: |
80 mkdir -p record
81 gh api "repos/${{ github.repository }}/actions/artifacts/$ARTIFACT_ID" > /tmp/record-artifact.json
82 gh api "repos/${{ github.repository }}/actions/runs/$PRODUCER_RUN_ID" > /tmp/producer-run.json
83 gh api "repos/${{ github.repository }}/actions/artifacts/$ARTIFACT_ID/zip" > /tmp/record.zip
84 node scripts/verify-release-artifact-archive.mjs /tmp/record-artifact.json /tmp/record.zip
85 unzip -q /tmp/record.zip -d record
86 test -s record/record.json
87 - name: Verify record provenance and resolve exact payload
88 id: record
89 env:
90 GH_TOKEN: ${{ github.token }}
91 run: |
92 signer_sha="$(jq -r .head_sha /tmp/producer-run.json)"
93 gh attestation verify record/record.json --repo "$GITHUB_REPOSITORY" \
94 --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release-candidate.yml" \
95 --signer-digest "$signer_sha" --source-ref refs/heads/main-v2 --deny-self-hosted-runners
96 node scripts/resolve-release-candidate.mjs inspect "${{ inputs.candidate_id }}" \
97 record/record.json /tmp/record-artifact.json /tmp/producer-run.json
98 - name: Check the configured tag publisher before approval
99 id: identity
100 env:
101 GH_TOKEN: ${{ secrets.RELEASE_TAG_TOKEN }}
102 RELEASE_TAG_ACTOR: ${{ vars.RELEASE_TAG_ACTOR }}
103 run: node scripts/verify-release-tag-identity.mjs "${{ steps.record.outputs.version }}"
104 - name: Download exact candidate payload
105 env:
106 GH_TOKEN: ${{ github.token }}
107 PAYLOAD_ID: ${{ steps.record.outputs.payload_artifact_id }}
108 run: |
109 gh api "repos/${{ github.repository }}/actions/artifacts/$PAYLOAD_ID" > /tmp/payload-artifact.json
110 test "$(jq -r .name /tmp/payload-artifact.json)" = "${{ steps.record.outputs.payload_artifact_name }}"
111 test "$(jq -r .expired /tmp/payload-artifact.json)" = false
112 test "$(jq -r .workflow_run.id /tmp/payload-artifact.json)" = "${{ steps.record.outputs.producer_run_id }}"
113 mkdir -p payload
114 gh api "repos/${{ github.repository }}/actions/artifacts/$PAYLOAD_ID/zip" > /tmp/payload.zip
115 node scripts/verify-release-artifact-archive.mjs /tmp/payload-artifact.json /tmp/payload.zip
116 unzip -q /tmp/payload.zip -d payload
117 - name: Verify payload provenance, bytes, source, and operation
118 env:
119 GH_TOKEN: ${{ github.token }}
120 OPERATION: ${{ inputs.operation }}
121 VERSION: ${{ steps.record.outputs.version }}
122 SOURCE_SHA: ${{ steps.record.outputs.source_sha }}
123 run: |
124 set -euo pipefail
125 # One provenance check per payload file; each is a network round trip,
126 # so they run eight at a time and any single failure fails the step.
127 signer_digest="${{ steps.record.outputs.candidate_control_sha }}"
128 find payload -type f -print0 | xargs -0 -P 8 -I{} \
129 gh attestation verify {} --repo "$GITHUB_REPOSITORY" \
130 --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release-candidate.yml" \
131 --signer-digest "$signer_digest" --source-ref refs/heads/main-v2 \
132 --deny-self-hosted-runners >/dev/null
133 node scripts/release-candidate.mjs verify payload record/record.json >/dev/null
134 git fetch origin main-v2 --tags
135 git merge-base --is-ancestor "$SOURCE_SHA" origin/main-v2
136 git show "$SOURCE_SHA:release-notes/releases.json" > /tmp/catalog.json
137 test "$(sha256sum /tmp/catalog.json | awk '{print $1}')" = "$(jq -r .notes.catalogSha256 record/record.json)"
138 bash scripts/release-candidate-tags.sh check "$OPERATION" "$VERSION" "$SOURCE_SHA"
139 {
140 echo "### Sealed release approval"
141 echo
142 echo "- Candidate: \`${{ inputs.candidate_id }}\`"
143 echo "- Operation: \`$OPERATION\`"
144 echo "- Product SHA: \`$SOURCE_SHA\`"
145 echo "- Candidate record SHA-256: \`$(sha256sum record/record.json | awk '{print $1}')\`"
146 echo "- Notes SHA-256: \`$(jq -r .notes.renderedSha256 record/record.json)\`"
147 echo "- Signing policy: \`$(jq -r .signing.desktopFingerprint record/record.json)\`"
148 echo "- Payload files: \`$(jq '.files | length' record/record.json)\`"
149 echo "- Native acceptance: \`$(jq -r '[.acceptance[] | select(.status == "passed") | .kind] | join(", ")' record/record.json)\`"
150 } >> "$GITHUB_STEP_SUMMARY"
151 # A candidate sealed with its notes publishes those bytes without touching
152 # the network. One sealed before that renders offline from its own source.
153 - name: Bind reviewed release notes
154 run: |
155 sealed="$(jq -r '.notes.renderedPath // empty' record/record.json)"
156 if [ -n "$sealed" ]; then
157 cp "payload/$sealed" release-notes.md
158 else
159 worktree="$(mktemp -d "$RUNNER_TEMP/reasonix-notes.XXXXXX")"
160 git worktree add --detach "$worktree" "${{ steps.record.outputs.source_sha }}"
161 node "$worktree/scripts/release-notes.mjs" render --version "v${{ steps.record.outputs.version }}" --output release-notes.md
162 git worktree remove "$worktree"
163 fi
164 test "$(sha256sum release-notes.md | awk '{print $1}')" = "$(jq -r .notes.renderedSha256 record/record.json)"
165 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
166 with:
167 name: orchestrator-reviewed-release-notes
168 path: release-notes.md
169 if-no-files-found: error
170 retention-days: 1
171 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
172 with:
173 name: approved-release-candidate-${{ github.run_id }}-${{ github.run_attempt }}
174 path: payload
175 if-no-files-found: error
176 retention-days: 7
177 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
178 with:
179 name: ${{ steps.record.outputs.desktop_prefix }}-darwin-arm64
180 path: payload/desktop/darwin-arm64
181 if-no-files-found: error
182 retention-days: 7
183 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
184 with:
185 name: ${{ steps.record.outputs.desktop_prefix }}-darwin-amd64
186 path: payload/desktop/darwin-amd64
187 if-no-files-found: error
188 retention-days: 7
189 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
190 with:
191 name: ${{ steps.record.outputs.desktop_prefix }}-darwin-universal
192 path: payload/desktop/darwin-universal
193 if-no-files-found: error
194 retention-days: 7
195 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
196 with:
197 name: ${{ steps.record.outputs.desktop_prefix }}-windows-amd64
198 path: payload/desktop/windows-amd64
199 if-no-files-found: error
200 retention-days: 7
201 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
202 with:
203 name: ${{ steps.record.outputs.desktop_prefix }}-windows-arm64
204 path: payload/desktop/windows-arm64
205 if-no-files-found: error
206 retention-days: 7
207 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
208 with:
209 name: ${{ steps.record.outputs.desktop_prefix }}-linux-amd64
210 path: payload/desktop/linux-amd64
211 if-no-files-found: error
212 retention-days: 7
213
214 authorize:
215 name: approve sealed Stable candidate
216 needs: preflight
217 runs-on: ubuntu-latest
218 environment: release
219 permissions:
220 contents: read
221 outputs:
222 version: ${{ needs.preflight.outputs.version }}
223 source_sha: ${{ needs.preflight.outputs.source_sha }}
224 steps:
225 - run: echo "Approved ${{ needs.preflight.outputs.candidate_id }} at ${{ needs.preflight.outputs.source_sha }}"
226
227 activate:
228 name: atomically activate release identity
229 needs: [preflight, authorize]
230 runs-on: ubuntu-latest
231 permissions:
232 contents: read
233 steps:
234 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
235 with:
236 fetch-depth: 0
237 ref: ${{ github.sha }}
238 persist-credentials: false
239 - name: Create or verify all implementation tags
240 env:
241 GH_TOKEN: ${{ secrets.RELEASE_TAG_TOKEN }}
242 RELEASE_TAG_ACTOR: ${{ vars.RELEASE_TAG_ACTOR }}
243 RELEASE_TAG_ACTOR_ID: ${{ needs.preflight.outputs.tag_actor_id }}
244 OPERATION: ${{ inputs.operation }}
245 VERSION: ${{ needs.preflight.outputs.version }}
246 SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
247 CANDIDATE_ID: ${{ needs.preflight.outputs.candidate_id }}
248 RELEASE_REVOKED_CANDIDATES: ${{ vars.RELEASE_REVOKED_CANDIDATES }}
249 run: |
250 set -euo pipefail
251 node scripts/verify-release-tag-identity.mjs "$VERSION"
252 gh auth setup-git --hostname github.com
253 node scripts/resolve-release-candidate.mjs active "$CANDIDATE_ID"
254 git fetch origin main-v2 --tags
255 bash scripts/release-candidate-tags.sh activate "$OPERATION" "$VERSION" "$SOURCE_SHA"
256
257 cli:
258 name: publish CLI and Homebrew from candidate
259 needs: [preflight, activate]
260 uses: ./.github/workflows/release.yml
261 with:
262 tag: v${{ needs.preflight.outputs.version }}
263 approved_cli_tag: v${{ needs.preflight.outputs.version }}
264 approved_sha: ${{ needs.preflight.outputs.source_sha }}
265 orchestrated: true
266 orchestrator: promote
267 candidate_artifact_name: ${{ needs.preflight.outputs.approved_artifact }}
268 candidate_verified: true
269 secrets: inherit
270
271 npm:
272 name: publish npm from candidate
273 needs: [preflight, activate]
274 permissions:
275 contents: read
276 id-token: write
277 uses: ./.github/workflows/release-npm.yml
278 with:
279 channel: stable
280 base_version: ${{ needs.preflight.outputs.version }}
281 tag: npm-v${{ needs.preflight.outputs.version }}
282 approved_cli_tag: v${{ needs.preflight.outputs.version }}
283 approved_sha: ${{ needs.preflight.outputs.source_sha }}
284 orchestrated: true
285 orchestrator: promote
286 candidate_artifact_name: ${{ needs.preflight.outputs.approved_artifact }}
287 candidate_verified: true
288 secrets: inherit
289
290 desktop:
291 name: publish Desktop from candidate
292 needs: [preflight, activate]
293 uses: ./.github/workflows/release-desktop.yml
294 with:
295 channel: stable
296 tag: desktop-v${{ needs.preflight.outputs.version }}
297 approved_cli_tag: v${{ needs.preflight.outputs.version }}
298 approved_sha: ${{ needs.preflight.outputs.source_sha }}
299 orchestrated: true
300 orchestrator: promote
301 signing_preflight_verified: true
302 preflight_artifact_prefix: ${{ needs.preflight.outputs.desktop_prefix }}
303 candidate_id: ${{ needs.preflight.outputs.candidate_id }}
304 candidate_source_run_id: ${{ needs.preflight.outputs.producer_run_id }}
305 candidate_source_run_attempt: ${{ needs.preflight.outputs.producer_run_attempt }}
306 candidate_control_sha: ${{ needs.preflight.outputs.candidate_control_sha }}
307 candidate_signing_fingerprint: ${{ needs.preflight.outputs.signing_fingerprint }}
308 candidate_verified: true
309 secrets: inherit
310
311 postflight:
312 name: verify every public Stable surface
313 timeout-minutes: 45
314 needs: [preflight, cli, npm, desktop]
315 if: ${{ always() && !cancelled() }}
316 runs-on: ubuntu-latest
317 permissions:
318 contents: write
319 steps:
320 - name: Require every publisher
321 env:
322 CLI_RESULT: ${{ needs.cli.result }}
323 NPM_RESULT: ${{ needs.npm.result }}
324 DESKTOP_RESULT: ${{ needs.desktop.result }}
325 run: |
326 for result in "$CLI_RESULT" "$NPM_RESULT" "$DESKTOP_RESULT"; do test "$result" = success; done
327 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
328 with:
329 ref: ${{ github.sha }}
330 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
331 with:
332 node-version: "22"
333 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
334 with:
335 go-version-file: go.mod
336 cache: false
337 - name: Reconcile public artifacts, release event, and durable ledger
338 env:
339 GH_TOKEN: ${{ github.token }}
340 RELEASE_REPOSITORY: ${{ github.repository }}
341 RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
342 RELEASE_OPERATION: ${{ inputs.operation }}
343 RELEASE_EXPECTED_SHA: ${{ needs.preflight.outputs.source_sha }}
344 RELEASE_LEDGER_OUTPUT: /tmp/publication-ledger.json
345 CLI_PUBLISH_FROZEN: ${{ vars.CLI_PUBLISH_FROZEN == 'true' }}
346 run: bash scripts/reconcile-release-publication.sh
347 - name: Upload publication ledger
348 if: always()
349 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
350 with:
351 name: release-publication-ledger-${{ needs.preflight.outputs.candidate_id }}-${{ github.run_attempt }}
352 path: /tmp/publication-ledger.json
353 if-no-files-found: error
354 overwrite: true
355 retention-days: 90
356
357 metrics:
358 name: record publication timing
359 needs: [preflight, authorize, activate, cli, npm, desktop, postflight]
360 if: ${{ always() && !cancelled() }}
361 continue-on-error: true
362 runs-on: ubuntu-latest
363 permissions:
364 actions: read
365 contents: read
366 steps:
367 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
368 with:
369 ref: ${{ github.sha }}
370 - name: Record queue, execution, and critical-path evidence
371 env:
372 GH_TOKEN: ${{ github.token }}
373 run: |
374 gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" > "$RUNNER_TEMP/release-run.json"
375 gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?filter=all&per_page=100" > "$RUNNER_TEMP/release-jobs.json"
376 node scripts/ci-timings.mjs \
377 --run "$RUNNER_TEMP/release-run.json" \
378 --jobs "$RUNNER_TEMP/release-jobs.json" \
379 --summary "$GITHUB_STEP_SUMMARY" \
380 --output "$RUNNER_TEMP/publication-timing.json" \
381 --title "Release publication timing"
382 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
383 with:
384 name: release-publication-timing-${{ needs.preflight.outputs.candidate_id || github.run_id }}-${{ github.run_attempt }}
385 path: ${{ runner.temp }}/publication-timing.json
386 if-no-files-found: ignore
387 overwrite: true
388 retention-days: 90
389
389 lines YAML