返回 DeepSeek-Reasonix
release-candidate.yml
根目录 / .github / workflows / release-candidate.yml
1 name: Prepare release candidate
2 run-name: Prepare release candidate ${{ inputs.version || github.sha }}
3
4 on:
5 workflow_dispatch:
6 inputs:
7 version:
8 description: "Official version without a tag prefix"
9 required: true
10 type: string
11 rehearsal:
12 description: "Build and accept isolated files that cannot be published"
13 required: false
14 default: false
15 type: boolean
16 push:
17 branches: [main-v2]
18 paths:
19 - release-notes/releases.json
20
21 concurrency:
22 group: release-candidate-${{ inputs.rehearsal && 'rehearsal' || 'release' }}-${{ inputs.version || github.sha }}
23 cancel-in-progress: false
24
25 permissions:
26 actions: read
27 contents: read
28
29 jobs:
30 resolve:
31 name: validate candidate inputs before native builds
32 runs-on: ubuntu-latest
33 outputs:
34 version: ${{ steps.candidate.outputs.version }}
35 candidate_id: ${{ steps.candidate.outputs.candidate_id }}
36 catalog_sha256: ${{ steps.candidate.outputs.catalog_sha256 }}
37 reuse: ${{ steps.existing.outputs.reuse }}
38 artifact_namespace: ${{ steps.candidate.outputs.artifact_namespace }}
39 purpose: ${{ steps.candidate.outputs.purpose }}
40 permissions:
41 actions: read
42 attestations: read
43 contents: read
44 steps:
45 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
46 with:
47 fetch-depth: 0
48 ref: ${{ github.sha }}
49 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
50 with:
51 node-version: "22"
52 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
53 with:
54 go-version-file: go.mod
55 cache: false
56 - name: Resolve reviewed source
57 id: candidate
58 env:
59 EVENT_NAME: ${{ github.event_name }}
60 INPUT_VERSION: ${{ inputs.version }}
61 PUSH_SHA: ${{ github.sha }}
62 REHEARSAL: ${{ inputs.rehearsal || false }}
63 GH_TOKEN: ${{ github.token }}
64 RELEASE_REPOSITORY: ${{ github.repository }}
65 RELEASE_REVOKED_CANDIDATES: ${{ vars.RELEASE_REVOKED_CANDIDATES }}
66 run: |
67 set -euo pipefail
68 git fetch origin main-v2 --tags
69 if [ "$EVENT_NAME" = push ]; then
70 version="$(jq -r '.releases[0].version // empty' release-notes/releases.json)"
71 source_sha="$PUSH_SHA"
72 else
73 version="$INPUT_VERSION"
74 source_sha="$PUSH_SHA"
75 fi
76 purpose=release
77 artifact_namespace=release-candidate
78 if [ "$REHEARSAL" = true ]; then
79 purpose=rehearsal
80 artifact_namespace=release-candidate-rehearsal
81 fi
82 bash scripts/validate-release-candidate-source.sh "$version" "$source_sha" "$purpose"
83 git show "$source_sha:release-notes/releases.json" > /tmp/release-catalog.json
84 catalog_sha256="$(sha256sum /tmp/release-catalog.json | awk '{print $1}')"
85 candidate_id="$(node scripts/release-candidate.mjs id "$version" "$source_sha" "$catalog_sha256")"
86 node scripts/resolve-release-candidate.mjs active "$candidate_id"
87 {
88 echo "version=$version"
89 echo "source_sha=$source_sha"
90 echo "catalog_sha256=$catalog_sha256"
91 echo "candidate_id=$candidate_id"
92 echo "purpose=$purpose"
93 echo "artifact_namespace=$artifact_namespace"
94 } >> "$GITHUB_OUTPUT"
95 {
96 echo "### Candidate input"
97 echo
98 echo "- ID: \`$candidate_id\`"
99 echo "- Product: \`$source_sha\`"
100 echo "- Control: \`$GITHUB_SHA\`"
101 echo "- Purpose: \`$purpose\`"
102 } >> "$GITHUB_STEP_SUMMARY"
103 - name: Verify release control helpers and signing configuration
104 env:
105 OFFICIAL_REPOSITORY: ${{ github.repository == 'esengine/DeepSeek-Reasonix' }}
106 HAS_APPLE_SIGNING: ${{ secrets.APPLE_CERT_P12 != '' && secrets.APPLE_CERT_PASSWORD != '' && secrets.APPLE_API_KEY_P8 != '' && secrets.APPLE_API_KEY_ID != '' && secrets.APPLE_API_ISSUER_ID != '' }}
107 HAS_CERTUM_SIGNING: ${{ secrets.CERTUM_USERNAME != '' && secrets.CERTUM_OTP_URI != '' && secrets.CERTUM_KEY_ID != '' }}
108 HAS_MINISIGN: ${{ secrets.MINISIGN_PRIVATE_KEY != '' && secrets.MINISIGN_PASSWORD != '' }}
109 run: |
110 bash scripts/validate-release-control-plane.sh
111 bash scripts/check-release-public-access.sh "${{ steps.candidate.outputs.version }}"
112 if [ "$OFFICIAL_REPOSITORY" = true ]; then
113 for configured in "$HAS_APPLE_SIGNING" "$HAS_CERTUM_SIGNING" "$HAS_MINISIGN"; do
114 [ "$configured" = true ] || { echo "::error::release signing configuration is incomplete"; exit 1; }
115 done
116 fi
117 - name: Reuse an existing sealed candidate
118 if: inputs.rehearsal != true
119 id: existing
120 env:
121 GH_TOKEN: ${{ github.token }}
122 CANDIDATE_ID: ${{ steps.candidate.outputs.candidate_id }}
123 RELEASE_REVOKED_CANDIDATES: ${{ vars.RELEASE_REVOKED_CANDIDATES }}
124 run: |
125 set -euo pipefail
126 resolved="$RUNNER_TEMP/existing-candidate.outputs"
127 GITHUB_OUTPUT="$resolved" node scripts/resolve-release-candidate.mjs resolve-optional "$CANDIDATE_ID"
128 output() { sed -n "s/^$1=//p" "$resolved" | tail -1; }
129 if [ "$(output found)" != true ]; then
130 echo "reuse=false" >> "$GITHUB_OUTPUT"
131 exit 0
132 fi
133 record_id="$(output record_artifact_id)"
134 run_id="$(output producer_run_id)"
135 mkdir -p "$RUNNER_TEMP/existing-record"
136 gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$record_id" > "$RUNNER_TEMP/existing-record-artifact.json"
137 gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" > "$RUNNER_TEMP/existing-run.json"
138 gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$record_id/zip" > "$RUNNER_TEMP/existing-record.zip"
139 node scripts/verify-release-artifact-archive.mjs "$RUNNER_TEMP/existing-record-artifact.json" "$RUNNER_TEMP/existing-record.zip"
140 unzip -q "$RUNNER_TEMP/existing-record.zip" -d "$RUNNER_TEMP/existing-record"
141 jq -e '
142 .validity.revoked == false and
143 ((.validity.createdAt | fromdateiso8601) < (.validity.expiresAt | fromdateiso8601))
144 ' "$RUNNER_TEMP/existing-record/record.json" >/dev/null
145 if ! jq -e '(.validity.expiresAt | fromdateiso8601) > now' \
146 "$RUNNER_TEMP/existing-record/record.json" >/dev/null; then
147 echo "reuse=false" >> "$GITHUB_OUTPUT"
148 echo "The sealed record expired; preparing fresh files." >> "$GITHUB_STEP_SUMMARY"
149 exit 0
150 fi
151 signer_sha="$(jq -r .head_sha "$RUNNER_TEMP/existing-run.json")"
152 gh attestation verify "$RUNNER_TEMP/existing-record/record.json" --repo "$GITHUB_REPOSITORY" \
153 --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release-candidate.yml" \
154 --signer-digest "$signer_sha" --source-ref refs/heads/main-v2 --deny-self-hosted-runners
155 inspected="$RUNNER_TEMP/existing-inspected.outputs"
156 GITHUB_OUTPUT="$inspected" node scripts/resolve-release-candidate.mjs inspect "$CANDIDATE_ID" \
157 "$RUNNER_TEMP/existing-record/record.json" "$RUNNER_TEMP/existing-record-artifact.json" \
158 "$RUNNER_TEMP/existing-run.json"
159 payload_id="$(sed -n 's/^payload_artifact_id=//p' "$inspected" | tail -1)"
160 if ! gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$payload_id" \
161 > "$RUNNER_TEMP/existing-payload.json" 2> "$RUNNER_TEMP/existing-payload.error"; then
162 if grep -Eiq 'HTTP 404|Not Found' "$RUNNER_TEMP/existing-payload.error"; then
163 echo "reuse=false" >> "$GITHUB_OUTPUT"
164 echo "The sealed record exists but its payload is unavailable; preparing fresh files." >> "$GITHUB_STEP_SUMMARY"
165 exit 0
166 fi
167 cat "$RUNNER_TEMP/existing-payload.error" >&2
168 exit 1
169 fi
170 test "$(jq -r .expired "$RUNNER_TEMP/existing-payload.json")" = false || {
171 echo "reuse=false" >> "$GITHUB_OUTPUT"
172 echo "The sealed record exists but its payload expired; preparing fresh files." >> "$GITHUB_STEP_SUMMARY"
173 exit 0
174 }
175 test "$(jq -r .name "$RUNNER_TEMP/existing-payload.json")" = "release-candidate-payload-$CANDIDATE_ID"
176 test "$(jq -r .workflow_run.id "$RUNNER_TEMP/existing-payload.json")" = "$run_id"
177 echo "reuse=true" >> "$GITHUB_OUTPUT"
178 echo "A sealed candidate with an active exact payload already exists; native builds are skipped." >> "$GITHUB_STEP_SUMMARY"
179 - name: Verify source CI after control preflight
180 if: steps.existing.outputs.reuse != 'true'
181 env:
182 GH_TOKEN: ${{ github.token }}
183 RELEASE_REPOSITORY: ${{ github.repository }}
184 RELEASE_SOURCE_SHA: ${{ github.sha }}
185 RELEASE_CI_WAIT_SECONDS: 1800
186 run: bash scripts/verify-release-push-ci.sh "$RELEASE_SOURCE_SHA"
187
188 # Release-specific but not an input to the native builds: it runs beside
189 # them and gates only the seal, so it adds nothing to the critical path.
190 notes:
191 name: validate embedded notes and cache identity alongside native builds
192 needs: resolve
193 if: needs.resolve.outputs.reuse != 'true'
194 runs-on: ubuntu-latest
195 permissions:
196 contents: read
197 issues: read # release-notes credits read PR and issue authors
198 pull-requests: read
199 steps:
200 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
201 with:
202 ref: ${{ github.sha }}
203 persist-credentials: false
204 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
205 with:
206 node-version: "22"
207 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
208 with:
209 go-version-file: go.mod
210 cache: false
211 - name: Validate embedded notes and cache identity inputs
212 env:
213 DOCS_BUILD_VERSION: v${{ needs.resolve.outputs.version }}
214 DOCS_SOURCE_REVISION: ${{ github.sha }}
215 GH_TOKEN: ${{ github.token }}
216 run: |
217 node scripts/release-notes.mjs validate
218 node scripts/release-notes.mjs render --version "$DOCS_BUILD_VERSION" --output /tmp/release-notes.md
219 bash scripts/verify-embedded-docs.sh "$DOCS_BUILD_VERSION" "$DOCS_SOURCE_REVISION"
220 ./scripts/cache-guard.sh
221
222 cli-npm:
223 name: build shared CLI and npm candidate
224 needs: resolve
225 if: needs.resolve.outputs.reuse != 'true'
226 runs-on: ubuntu-latest
227 steps:
228 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
229 with:
230 fetch-depth: 0
231 ref: ${{ github.sha }}
232 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
233 with:
234 go-version-file: go.mod
235 cache: true
236 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
237 with:
238 node-version: "22"
239 - name: Build each CLI binary once and package both surfaces
240 env:
241 RELEASE_SOURCE_SHA: ${{ github.sha }}
242 run: |
243 RELEASE_BUILD_TIME="$(git show -s --format=%cI "$RELEASE_SOURCE_SHA")"
244 export RELEASE_BUILD_TIME
245 node scripts/build-release-cli-candidate.mjs "${{ needs.resolve.outputs.version }}" candidate-cli
246 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
247 with:
248 name: candidate-cli-${{ needs.resolve.outputs.candidate_id }}
249 path: |
250 candidate-cli/cli
251 candidate-cli/npm
252 if-no-files-found: error
253 overwrite: true
254 retention-days: 30
255
256 desktop:
257 name: build, sign, and accept Desktop candidate
258 needs: resolve
259 if: needs.resolve.outputs.reuse != 'true'
260 # GitHub validates every nested job before evaluating its condition, even
261 # though candidate preparation skips the publisher and mirror jobs.
262 permissions:
263 actions: write
264 contents: write
265 issues: read # release-notes credits read PR and issue authors
266 pull-requests: read
267 uses: ./.github/workflows/release-desktop.yml
268 with:
269 channel: stable
270 tag: desktop-v${{ needs.resolve.outputs.version }}
271 approved_cli_tag: v${{ needs.resolve.outputs.version }}
272 approved_sha: ${{ github.sha }}
273 orchestrated: true
274 orchestrator: candidate
275 signing_preflight: true
276 candidate_preparation: true
277 candidate_rehearsal: ${{ inputs.rehearsal || false }}
278 secrets: inherit
279
280 seal:
281 name: seal reusable release candidate
282 needs: [resolve, notes, cli-npm, desktop]
283 if: >-
284 always() && !cancelled() && needs.resolve.outputs.reuse != 'true' &&
285 needs.resolve.result == 'success' && needs.notes.result == 'success' &&
286 needs.cli-npm.result == 'success' && needs.desktop.result == 'success'
287 runs-on: ubuntu-latest
288 permissions:
289 actions: read
290 attestations: write
291 contents: read
292 id-token: write
293 issues: read # release-notes credits read PR and issue authors
294 pull-requests: read
295 steps:
296 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
297 with:
298 fetch-depth: 0
299 ref: ${{ github.sha }}
300 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
301 with:
302 node-version: "22"
303 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
304 with:
305 name: candidate-cli-${{ needs.resolve.outputs.candidate_id }}
306 path: payload
307 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
308 with:
309 name: ${{ needs.desktop.outputs.artifact_prefix }}-darwin-arm64
310 path: payload/desktop/darwin-arm64
311 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
312 with:
313 name: ${{ needs.desktop.outputs.artifact_prefix }}-darwin-amd64
314 path: payload/desktop/darwin-amd64
315 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
316 with:
317 name: ${{ needs.desktop.outputs.artifact_prefix }}-darwin-universal
318 path: payload/desktop/darwin-universal
319 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
320 with:
321 name: ${{ needs.desktop.outputs.artifact_prefix }}-windows-amd64
322 path: payload/desktop/windows-amd64
323 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
324 with:
325 name: ${{ needs.desktop.outputs.artifact_prefix }}-windows-arm64
326 path: payload/desktop/windows-arm64
327 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
328 with:
329 name: ${{ needs.desktop.outputs.artifact_prefix }}-linux-amd64
330 path: payload/desktop/linux-amd64
331 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
332 with:
333 name: ${{ needs.desktop.outputs.artifact_prefix }}-receipt-windows-amd64
334 path: payload/evidence
335 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
336 with:
337 name: ${{ needs.desktop.outputs.artifact_prefix }}-receipt-windows-arm64
338 path: payload/evidence
339 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
340 with:
341 name: ${{ needs.desktop.outputs.artifact_prefix }}-receipt-macos-universal-intel
342 path: payload/evidence
343 - name: Normalize acceptance receipt names
344 run: |
345 test -s payload/evidence/windows-amd64.json
346 test -s payload/evidence/windows-arm64.json
347 test -s payload/evidence/macos-universal-intel.json
348 # Rendered once and sealed: promotion publishes these bytes and never
349 # reads GitHub again, so a later rename or outage cannot change them.
350 - name: Render reviewed notes into the sealed payload
351 env:
352 GH_TOKEN: ${{ github.token }}
353 run: |
354 worktree="$(mktemp -d "$RUNNER_TEMP/reasonix-notes.XXXXXX")"
355 git worktree add --detach "$worktree" "${{ github.sha }}"
356 node "$worktree/scripts/release-notes.mjs" render --version "v${{ needs.resolve.outputs.version }}" --output payload/evidence/release-notes.md
357 git worktree remove "$worktree"
358 - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
359 with:
360 subject-path: payload/**/*
361 - name: Upload immutable candidate payload
362 id: payload
363 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
364 with:
365 name: ${{ needs.resolve.outputs.artifact_namespace }}-payload-${{ needs.resolve.outputs.candidate_id }}
366 path: payload
367 if-no-files-found: error
368 overwrite: true
369 retention-days: 30
370 - name: Retain native acceptance evidence
371 id: evidence
372 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
373 with:
374 name: ${{ needs.resolve.outputs.artifact_namespace }}-evidence-${{ needs.resolve.outputs.candidate_id }}
375 path: payload/evidence
376 if-no-files-found: error
377 overwrite: true
378 retention-days: 90
379 - name: Seal candidate record
380 env:
381 RELEASE_VERSION: ${{ needs.resolve.outputs.version }}
382 RELEASE_CANDIDATE_PURPOSE: ${{ needs.resolve.outputs.purpose }}
383 RELEASE_SOURCE_SHA: ${{ github.sha }}
384 RELEASE_BUILD_CONTROL_SHA: ${{ github.sha }}
385 RELEASE_ACCEPTANCE_CONTROL_SHA: ${{ github.sha }}
386 RELEASE_NOTES_SOURCE_SHA: ${{ github.sha }}
387 RELEASE_CATALOG_SHA256: ${{ needs.resolve.outputs.catalog_sha256 }}
388 RELEASE_DESKTOP_PREFIX: ${{ needs.desktop.outputs.artifact_prefix }}
389 RELEASE_DESKTOP_FINGERPRINT: ${{ needs.desktop.outputs.signing_fingerprint }}
390 RELEASE_PAYLOAD_ARTIFACT_ID: ${{ steps.payload.outputs.artifact-id }}
391 RELEASE_PAYLOAD_ARTIFACT_NAME: ${{ needs.resolve.outputs.artifact_namespace }}-payload-${{ needs.resolve.outputs.candidate_id }}
392 RELEASE_EVIDENCE_ARTIFACT_ID: ${{ steps.evidence.outputs.artifact-id }}
393 RELEASE_EVIDENCE_ARTIFACT_NAME: ${{ needs.resolve.outputs.artifact_namespace }}-evidence-${{ needs.resolve.outputs.candidate_id }}
394 RELEASE_WORKFLOW: .github/workflows/release-candidate.yml
395 RELEASE_ACCEPTANCE_JSON: >-
396 [{"kind":"windows-amd64","status":"passed","evidencePath":"evidence/windows-amd64.json"},{"kind":"windows-arm64","status":"passed","evidencePath":"evidence/windows-arm64.json"},{"kind":"macos-universal-intel","status":"passed","evidencePath":"evidence/macos-universal-intel.json"}]
397 run: |
398 RELEASE_RENDERED_NOTES_SHA256="$(sha256sum payload/evidence/release-notes.md | awk '{print $1}')"
399 RELEASE_CREATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
400 RELEASE_EXPIRES_AT="$(date -u -d '+30 days' +%Y-%m-%dT%H:%M:%SZ)"
401 export RELEASE_RENDERED_NOTES_SHA256 RELEASE_CREATED_AT RELEASE_EXPIRES_AT
402 node scripts/release-candidate.mjs seal payload record/record.json
403 if [ "$RELEASE_CANDIDATE_PURPOSE" = rehearsal ]; then
404 node scripts/release-candidate.mjs verify-rehearsal payload record/record.json
405 else
406 node scripts/release-candidate.mjs verify payload record/record.json
407 fi
408 - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
409 with:
410 subject-path: record/record.json
411 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
412 with:
413 name: ${{ needs.resolve.outputs.artifact_namespace }}-record-${{ needs.resolve.outputs.candidate_id }}
414 path: record/record.json
415 if-no-files-found: error
416 overwrite: true
417 retention-days: 90
418 - name: Report publish command
419 env:
420 PURPOSE: ${{ needs.resolve.outputs.purpose }}
421 run: |
422 {
423 echo "### Candidate ready"
424 echo
425 echo "\`${{ needs.resolve.outputs.candidate_id }}\`"
426 echo
427 echo '```sh'
428 if [ "$PURPOSE" = rehearsal ]; then
429 echo "gh workflow run release-candidate-verify.yml --ref main-v2 -f candidate_id='${{ needs.resolve.outputs.candidate_id }}'"
430 else
431 echo "./scripts/release-stable.sh '${{ needs.resolve.outputs.candidate_id }}'"
432 fi
433 echo '```'
434 } >> "$GITHUB_STEP_SUMMARY"
435
436 metrics:
437 name: record candidate timing
438 needs: [resolve, notes, cli-npm, desktop, seal]
439 if: ${{ always() && !cancelled() }}
440 continue-on-error: true
441 runs-on: ubuntu-latest
442 permissions:
443 actions: read
444 contents: read
445 steps:
446 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
447 with:
448 ref: ${{ github.sha }}
449 - name: Record queue, execution, and critical-path evidence
450 env:
451 GH_TOKEN: ${{ github.token }}
452 run: |
453 gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" > "$RUNNER_TEMP/release-run.json"
454 gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?filter=all&per_page=100" > "$RUNNER_TEMP/release-jobs.json"
455 node scripts/ci-timings.mjs \
456 --run "$RUNNER_TEMP/release-run.json" \
457 --jobs "$RUNNER_TEMP/release-jobs.json" \
458 --summary "$GITHUB_STEP_SUMMARY" \
459 --output "$RUNNER_TEMP/candidate-timing.json" \
460 --title "Release candidate timing"
461 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
462 with:
463 name: release-candidate-timing-${{ needs.resolve.outputs.candidate_id || github.run_id }}-${{ github.run_attempt }}
464 path: ${{ runner.temp }}/candidate-timing.json
465 if-no-files-found: ignore
466 overwrite: true
467 retention-days: 90
468
468 lines YAML