| 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 |