返回 CodeWhale
release.yml
根目录 / .github / workflows / release.yml
1 name: Release
2
3 on:
4 push:
5 tags: ['v*']
6 workflow_dispatch:
7 inputs:
8 version:
9 description: 'Release version, without v; dispatch from the matching existing vX.Y.Z tag ref'
10 required: true
11 type: string
12
13 concurrency:
14 group: release-${{ github.ref_name }}
15 cancel-in-progress: false
16
17 permissions:
18 contents: read
19
20 env:
21 CARGO_TERM_COLOR: always
22 CARGO_INCREMENTAL: 0
23 RUSTFLAGS: -Dwarnings
24
25 jobs:
26 resolve:
27 timeout-minutes: 10
28 runs-on: ubuntu-latest
29 permissions:
30 contents: read
31 # Read release-candidate.yml runs for the RC receipt check below.
32 actions: read
33 outputs:
34 tag: ${{ steps.release.outputs.tag }}
35 sha: ${{ steps.release.outputs.sha }}
36 version: ${{ steps.release.outputs.version }}
37 steps:
38 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
39 with:
40 fetch-depth: 0
41 - name: Resolve release source
42 id: release
43 shell: bash
44 env:
45 INPUT_VERSION: ${{ inputs.version }}
46 run: |
47 set -euo pipefail
48
49 if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
50 if ! [[ "${INPUT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
51 echo "::error::Release version '${INPUT_VERSION}' must use X.Y.Z." >&2
52 exit 1
53 fi
54 tag="v${INPUT_VERSION}"
55 if [[ "${GITHUB_REF}" != "refs/tags/${tag}" ]]; then
56 echo "::error::Dispatch release.yml from --ref ${tag}, not ${GITHUB_REF}." >&2
57 exit 1
58 fi
59 else
60 tag="${GITHUB_REF_NAME}"
61 fi
62
63 if ! [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
64 echo "::error::Release tag '${tag}' must use vX.Y.Z." >&2
65 exit 1
66 fi
67 if ! git rev-parse --verify "refs/tags/${tag}^{commit}" >/dev/null 2>&1; then
68 echo "::error::Release tag ${tag} does not exist. Create it from the frozen main commit before dispatching." >&2
69 exit 1
70 fi
71
72 sha="$(git rev-parse "refs/tags/${tag}^{commit}")"
73 event_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")"
74 if [[ "${event_sha}" != "${sha}" ]]; then
75 echo "::error::Trigger SHA ${event_sha} does not match ${tag} at ${sha}; the tag moved after this run was created." >&2
76 exit 1
77 fi
78
79 {
80 echo "tag=${tag}"
81 echo "sha=${sha}"
82 echo "version=${tag#v}"
83 } >> "${GITHUB_OUTPUT}"
84 - name: Validate tagged release metadata
85 shell: bash
86 env:
87 SHA: ${{ steps.release.outputs.sha }}
88 TAG: ${{ steps.release.outputs.tag }}
89 run: |
90 set -euo pipefail
91 git checkout --detach "${SHA}"
92 expected="${TAG#v}"
93 workspace_version="$(grep -E '^version = "' Cargo.toml | head -n1 | sed -E 's/^version = "([^"]+)".*/\1/')"
94 npm_version="$(node -p "require('./npm/codewhale/package.json').version")"
95 binary_version="$(node -p "require('./npm/codewhale/package.json').codewhaleBinaryVersion")"
96 sdk_version="$(node -p "require('./npm/runtime-sdk/package.json').version")"
97 vscode_version="$(node -p "require('./extensions/vscode/package.json').version")"
98 for pair in \
99 "workspace:${workspace_version}" \
100 "npm:${npm_version}" \
101 "npm binary:${binary_version}" \
102 "runtime-sdk:${sdk_version}" \
103 "vscode:${vscode_version}"; do
104 label="${pair%%:*}"
105 actual="${pair#*:}"
106 if [[ "${actual}" != "${expected}" ]]; then
107 echo "::error::${label} version ${actual} does not match tag ${TAG}." >&2
108 exit 1
109 fi
110 done
111 ./scripts/release/check-versions.sh --require-dated-release
112 - name: Require release source on main
113 run: ./scripts/release/ensure-release-on-main.sh "${{ steps.release.outputs.sha }}"
114 - name: Require a green release-candidate receipt for this exact SHA
115 # Fail fast, before the long parity and artifact builds: the tag must
116 # point at a commit a release-candidate run already validated,
117 # Parity included. A missing receipt means run the RC on this SHA,
118 # never move the tag (v0.10.0 was re-pointed after the RC skipped
119 # parity).
120 env:
121 GH_TOKEN: ${{ github.token }}
122 SHA: ${{ steps.release.outputs.sha }}
123 run: ./scripts/release/require-rc-receipt.sh "${GITHUB_REPOSITORY}" "${SHA}"
124 - name: Refuse an existing public asset set
125 env:
126 GH_TOKEN: ${{ github.token }}
127 TAG: ${{ steps.release.outputs.tag }}
128 run: node scripts/release/ensure-release-assets-absent.js "${GITHUB_REPOSITORY}" "${TAG}"
129
130 parity:
131 name: Parity
132 needs: resolve
133 uses: ./.github/workflows/release-parity.yml
134
135 artifacts:
136 permissions:
137 contents: read
138 actions: read
139 needs: [parity, resolve]
140 if: ${{ !cancelled() && needs.resolve.result == 'success' && needs.parity.result == 'success' }}
141 uses: ./.github/workflows/release-artifacts.yml
142 with:
143 source_sha: ${{ needs.resolve.outputs.sha }}
144 version: ${{ needs.resolve.outputs.version }}
145 compiled_host_delivery: ${{ vars.CODEWHALE_COMPILED_HOST_DELIVERY == '1' }}
146 compiled_host_run_id: ${{ vars.CODEWHALE_COMPILED_HOST_QUALIFICATION_RUN_ID || '' }}
147 compiled_host_targets: ${{ vars.CODEWHALE_COMPILED_HOST_TARGETS || 'linux-x64,macos-arm64,windows-x64' }}
148 retention_days: 14
149
150 docker-build:
151 needs: [artifacts, resolve]
152 if: ${{ !cancelled() && needs.artifacts.result == 'success' }}
153 name: Docker ${{ matrix.platform }}
154 timeout-minutes: 60
155 strategy:
156 fail-fast: false
157 matrix:
158 include:
159 - runner: ubuntu-latest
160 platform: linux/amd64
161 architecture: amd64
162 cli_artifact: codewhale-linux-x64
163 shim_artifact: codew-linux-x64
164 - runner: ubuntu-24.04-arm
165 platform: linux/arm64
166 architecture: arm64
167 cli_artifact: codewhale-linux-arm64
168 shim_artifact: codew-linux-arm64
169 runs-on: ${{ matrix.runner }}
170 permissions:
171 contents: read
172 packages: write
173 steps:
174 - name: Checkout release infrastructure
175 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
176 with:
177 ref: ${{ needs.resolve.outputs.sha }}
178 path: infra
179 - name: Download Codewhale release binary
180 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
181 with:
182 name: ${{ matrix.cli_artifact }}
183 path: docker-context/bin
184 - name: Download codew release alias
185 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
186 with:
187 name: ${{ matrix.shim_artifact }}
188 path: docker-context/bin
189 - name: Verify native release bytes
190 shell: bash
191 env:
192 CLI_ARTIFACT: ${{ matrix.cli_artifact }}
193 SHIM_ARTIFACT: ${{ matrix.shim_artifact }}
194 run: |
195 set -euo pipefail
196 mv -- "docker-context/bin/${CLI_ARTIFACT}" docker-context/bin/codewhale
197 mv -- "docker-context/bin/${SHIM_ARTIFACT}" docker-context/bin/codew
198 chmod 0755 docker-context/bin/codewhale docker-context/bin/codew
199 cmp docker-context/bin/codewhale docker-context/bin/codew
200 docker-context/bin/codewhale --version
201 docker-context/bin/codew --version
202 - name: Set up Docker Buildx
203 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4
204 - name: Log in to GitHub Container Registry
205 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
206 with:
207 registry: ghcr.io
208 username: ${{ github.repository_owner }}
209 password: ${{ secrets.GITHUB_TOKEN }}
210 - name: Normalize image name
211 id: image
212 shell: bash
213 run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
214 - name: Extract image labels
215 id: meta
216 uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
217 with:
218 images: |
219 ${{ steps.image.outputs.name }}
220 tags: |
221 type=raw,value=${{ needs.resolve.outputs.version }}
222 - name: Revalidate release tag before container upload
223 env:
224 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
225 TAG: ${{ needs.resolve.outputs.tag }}
226 run: |
227 ./infra/scripts/release/verify-remote-tag.sh \
228 "https://github.com/${GITHUB_REPOSITORY}.git" \
229 "${TAG}" \
230 "${EXPECTED_SHA}"
231 - name: Assemble and push native image by digest
232 id: build
233 uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
234 env:
235 DOCKER_BUILD_RECORD_UPLOAD: false
236 DOCKER_BUILD_SUMMARY: false
237 with:
238 context: docker-context
239 file: infra/packaging/docker/Dockerfile.release
240 platforms: ${{ matrix.platform }}
241 provenance: mode=max
242 sbom: true
243 labels: ${{ steps.meta.outputs.labels }}
244 outputs: type=image,name=${{ steps.image.outputs.name }},push-by-digest=true,name-canonical=true,push=true
245 - name: Smoke native image digest
246 shell: bash
247 env:
248 IMAGE: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }}
249 run: |
250 set -euo pipefail
251 docker pull "${IMAGE}"
252 docker run --rm --entrypoint codewhale "${IMAGE}" --version
253 docker run --rm --entrypoint codew "${IMAGE}" --version
254 - name: Export image digest
255 shell: bash
256 env:
257 DIGEST: ${{ steps.build.outputs.digest }}
258 run: |
259 set -euo pipefail
260 if ! [[ "${DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
261 echo "Unexpected image digest: ${DIGEST}" >&2
262 exit 1
263 fi
264 mkdir -p digests
265 touch "digests/${DIGEST#sha256:}"
266 - name: Upload image digest
267 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
268 with:
269 name: docker-digest-${{ matrix.architecture }}
270 path: digests/*
271 if-no-files-found: error
272 retention-days: 1
273 overwrite: true
274
275 # Derived channel: the tagged/latest manifest is published only after the
276 # canonical GitHub Release exists. docker-build pushes by digest (untagged)
277 # and smokes each image before the release, so a broken image still stops
278 # the release, but no public tag can point at a version whose GitHub
279 # Release was never published.
280 docker:
281 timeout-minutes: 30
282 needs: [docker-build, release, resolve]
283 if: ${{ !cancelled() && needs.docker-build.result == 'success' && needs.release.result == 'success' }}
284 runs-on: ubuntu-latest
285 permissions:
286 contents: read
287 packages: write
288 steps:
289 - name: Checkout release infrastructure
290 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
291 with:
292 ref: ${{ needs.resolve.outputs.sha }}
293 path: infra
294 - name: Download native image digests
295 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
296 with:
297 path: digests
298 pattern: docker-digest-*
299 merge-multiple: true
300 - name: Set up Docker Buildx
301 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4
302 - name: Log in to GitHub Container Registry
303 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
304 with:
305 registry: ghcr.io
306 username: ${{ github.repository_owner }}
307 password: ${{ secrets.GITHUB_TOKEN }}
308 - name: Normalize image name
309 id: image
310 shell: bash
311 run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
312 - name: Extract metadata
313 id: meta
314 uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
315 with:
316 images: |
317 ${{ steps.image.outputs.name }}
318 tags: |
319 type=semver,pattern={{version}}
320 type=semver,pattern={{major}}.{{minor}}
321 type=semver,pattern=v{{major}}
322 type=ref,event=tag
323 type=semver,pattern={{version}},value=${{ needs.resolve.outputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
324 type=semver,pattern={{major}}.{{minor}},value=${{ needs.resolve.outputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
325 type=semver,pattern=v{{major}},value=${{ needs.resolve.outputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
326 type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
327 type=raw,value=v${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
328 type=raw,value=latest
329 - name: Revalidate release tag before container publish
330 env:
331 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
332 TAG: ${{ needs.resolve.outputs.tag }}
333 run: |
334 ./infra/scripts/release/verify-remote-tag.sh \
335 "https://github.com/${GITHUB_REPOSITORY}.git" \
336 "${TAG}" \
337 "${EXPECTED_SHA}"
338 - name: Publish multi-architecture manifest
339 shell: bash
340 env:
341 IMAGE: ${{ steps.image.outputs.name }}
342 TAGS: ${{ steps.meta.outputs.tags }}
343 run: |
344 set -euo pipefail
345 mapfile -t digest_files < <(find digests -maxdepth 1 -type f -printf '%f\n' | sort)
346 if [[ "${#digest_files[@]}" -ne 2 ]]; then
347 echo "Expected exactly two native image digests; found ${#digest_files[@]}." >&2
348 exit 1
349 fi
350
351 sources=()
352 for digest in "${digest_files[@]}"; do
353 if ! [[ "${digest}" =~ ^[0-9a-f]{64}$ ]]; then
354 echo "Unexpected image digest file: ${digest}" >&2
355 exit 1
356 fi
357 sources+=("${IMAGE}@sha256:${digest}")
358 done
359
360 tag_args=()
361 while IFS= read -r tag; do
362 [[ -n "${tag}" ]] && tag_args+=(--tag "${tag}")
363 done <<< "${TAGS}"
364 if [[ "${#tag_args[@]}" -eq 0 ]]; then
365 echo "No container tags were generated." >&2
366 exit 1
367 fi
368
369 docker buildx imagetools create "${tag_args[@]}" "${sources[@]}"
370 - name: Verify and smoke published container
371 shell: bash
372 env:
373 IMAGE: ${{ steps.image.outputs.name }}:${{ needs.resolve.outputs.tag }}
374 run: |
375 set -euo pipefail
376 docker buildx imagetools inspect "${IMAGE}"
377 raw_manifest="$(docker buildx imagetools inspect --raw "${IMAGE}")"
378 jq -e \
379 '[.manifests[] | select(.platform.os == "linux") | "linux/\(.platform.architecture)"] | unique | sort == ["linux/amd64", "linux/arm64"]' \
380 <<< "${raw_manifest}"
381 docker pull "${IMAGE}"
382 docker run --rm --entrypoint codewhale "${IMAGE}" --version
383 docker run --rm --entrypoint codew "${IMAGE}" --version
384
385 release:
386 timeout-minutes: 30
387 needs: [artifacts, docker-build, resolve]
388 if: ${{ !cancelled() && needs.artifacts.result == 'success' && needs.docker-build.result == 'success' }}
389 runs-on: ubuntu-latest
390 permissions:
391 contents: write
392 steps:
393 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
394 with:
395 ref: ${{ needs.resolve.outputs.sha }}
396 path: repo
397 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
398 with:
399 node-version: 22
400 package-manager-cache: false
401 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
402 with:
403 name: codewhale-release-assets
404 path: artifacts
405 - name: Revalidate exact authoritative asset set
406 run: node repo/scripts/release/assemble-release-assets.js --verify artifacts
407 - name: Generate release body from CHANGELOG
408 shell: bash
409 run: |
410 ./repo/scripts/release/generate-release-body.sh \
411 "${{ needs.resolve.outputs.tag }}" repo/CHANGELOG.md > release-body.md
412 - name: Revalidate release tag before GitHub Release write
413 env:
414 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
415 TAG: ${{ needs.resolve.outputs.tag }}
416 run: |
417 ./repo/scripts/release/verify-remote-tag.sh \
418 "https://github.com/${GITHUB_REPOSITORY}.git" \
419 "${TAG}" \
420 "${EXPECTED_SHA}"
421 - name: Reconfirm public asset set is still empty
422 env:
423 GH_TOKEN: ${{ github.token }}
424 TAG: ${{ needs.resolve.outputs.tag }}
425 run: node repo/scripts/release/ensure-release-assets-absent.js "${GITHUB_REPOSITORY}" "${TAG}"
426 # Upload into a DRAFT: 34 sequential uploads that die halfway must leave
427 # nothing public (and nothing a rerun's immutability guard refuses).
428 - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3
429 with:
430 tag_name: ${{ needs.resolve.outputs.tag }}
431 files: artifacts/*
432 draft: true
433 prerelease: false
434 body_path: release-body.md
435 overwrite_files: false
436 fail_on_unmatched_files: true
437 - name: Verify the draft's exact asset set, then publish it at once
438 env:
439 GH_TOKEN: ${{ github.token }}
440 TAG: ${{ needs.resolve.outputs.tag }}
441 run: |
442 node repo/scripts/release/verify-release-inventory.js \
443 --draft --asset-dir artifacts --publish \
444 "${GITHUB_REPOSITORY}" "${TAG}"
445
446 # CNB tag pushes start its public binary pipeline, so mirror only after
447 # the canonical release is complete. Calling the reusable workflow here
448 # also works when GITHUB_TOKEN publication suppresses release events.
449 cnb:
450 needs: [release, resolve]
451 if: ${{ !cancelled() && needs.release.result == 'success' }}
452 uses: ./.github/workflows/sync-cnb.yml
453 with:
454 release_tag: ${{ needs.resolve.outputs.tag }}
455 source_sha: ${{ needs.resolve.outputs.sha }}
456 secrets:
457 CNB_GIT_TOKEN: ${{ secrets.CNB_GIT_TOKEN }}
458
459 npm:
460 timeout-minutes: 20
461 needs: [release, resolve]
462 if: ${{ !cancelled() && needs.release.result == 'success' }}
463 runs-on: ubuntu-latest
464 permissions:
465 contents: read
466 id-token: write
467 steps:
468 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
469 with:
470 ref: ${{ needs.resolve.outputs.sha }}
471 fetch-depth: 0
472 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
473 with:
474 node-version: 24
475 registry-url: https://registry.npmjs.org
476 package-manager-cache: false
477 - name: Pin OIDC-capable npm CLI
478 run: npm install --global npm@12.0.2
479 - name: Revalidate release tag before npm publish
480 env:
481 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
482 TAG: ${{ needs.resolve.outputs.tag }}
483 run: |
484 ./scripts/release/verify-remote-tag.sh \
485 "https://github.com/${GITHUB_REPOSITORY}.git" \
486 "${TAG}" \
487 "${EXPECTED_SHA}"
488 - name: Revalidate public release assets
489 env:
490 GH_TOKEN: ${{ github.token }}
491 run: ./scripts/release/verify-release-assets.sh "${{ needs.resolve.outputs.version }}"
492 - name: Test npm wrapper
493 working-directory: npm/codewhale
494 run: npm test
495 - name: Publish npm wrapper with trusted publishing
496 working-directory: npm/codewhale
497 env:
498 # npm runs prepublishOnly in this step; that guard revalidates the
499 # public GitHub Release and therefore needs the same read token as
500 # the explicit asset gate above.
501 GH_TOKEN: ${{ github.token }}
502 run: npm publish --access public
503
504 homebrew:
505 timeout-minutes: 20
506 needs: [release, resolve]
507 if: ${{ !cancelled() && needs.release.result == 'success' }}
508 runs-on: ubuntu-latest
509 permissions:
510 contents: read
511 steps:
512 - name: Check Homebrew tap token
513 # A real release must update the tap. Skipping silently (the old
514 # behaviour) left `brew install codewhale` on the previous version
515 # with a green release run; fail loudly like release-republish.yml.
516 env:
517 TOKEN: ${{ secrets.HOMEBREW_TAP_PAT || secrets.RELEASE_TAG_PAT }}
518 run: |
519 if [[ -z "${TOKEN:-}" ]]; then
520 echo "::error::No Homebrew tap token configured (HOMEBREW_TAP_PAT or RELEASE_TAG_PAT); cannot update the tap for this release." >&2
521 exit 1
522 fi
523 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
524 with:
525 ref: ${{ needs.resolve.outputs.sha }}
526 - name: Download checksum manifest
527 env:
528 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
529 run: |
530 gh release download "${{ needs.resolve.outputs.tag }}" \
531 --repo "${{ github.repository }}" \
532 --pattern 'codewhale-artifacts-sha256.txt' \
533 --dir /tmp
534 - name: Revalidate release tag before Homebrew tap write
535 env:
536 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
537 TAG: ${{ needs.resolve.outputs.tag }}
538 run: |
539 ./scripts/release/verify-remote-tag.sh \
540 "https://github.com/${GITHUB_REPOSITORY}.git" \
541 "${TAG}" \
542 "${EXPECTED_SHA}"
543 - name: Update Homebrew tap
544 env:
545 TAG: ${{ needs.resolve.outputs.tag }}
546 MANIFEST: /tmp/codewhale-artifacts-sha256.txt
547 TAP_REPO: Hmbown/homebrew-deepseek-tui
548 TOKEN: ${{ secrets.HOMEBREW_TAP_PAT || secrets.RELEASE_TAG_PAT }}
549 run: bash .github/scripts/update-homebrew-tap.sh
550
551 # Every publish used to turn Web Frontend red: `check:latest-release`
552 # compares the checked-in release record with GitHub's latest release, and
553 # nothing wrote the record, so someone hand-committed it after each release.
554 # This job writes the record, proves it against the web gates, and proposes
555 # it to main as a bot PR. It never pushes to the default branch: the ruleset
556 # requires a PR there. While the PR is open (up to 24h after publish, and
557 # only when the record is exactly one release behind), `check:latest-release`
558 # warns instead of failing on every event (see web/scripts/sync-latest-release.mjs).
559 #
560 # Known limit: repo settings stop GITHUB_TOKEN from opening PRs, so the PR is
561 # opened with RELEASE_TAG_PAT. Without it the job pushes the branch and fails
562 # with the compare link, which is still one click instead of a hand commit.
563 sync-release-record:
564 timeout-minutes: 15
565 needs: [release, resolve]
566 if: ${{ !cancelled() && needs.release.result == 'success' }}
567 runs-on: ubuntu-latest
568 permissions:
569 contents: write
570 steps:
571 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
572 with:
573 ref: ${{ github.event.repository.default_branch }}
574 # derive-facts.mjs dates model ids from git history (web.yml pins 0
575 # for the same reason); a shallow clone rewrites every addedAt.
576 fetch-depth: 0
577 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
578 with:
579 node-version: 22
580 package-manager-cache: false
581 - name: Refresh the release record from the published release
582 env:
583 GITHUB_TOKEN: ${{ github.token }}
584 run: |
585 set -euo pipefail
586 node web/scripts/sync-latest-release.mjs
587 node web/scripts/derive-facts.mjs
588 - name: Prove the record passes the web fact gates
589 env:
590 GITHUB_TOKEN: ${{ github.token }}
591 run: |
592 set -euo pipefail
593 node web/scripts/sync-latest-release.mjs --check
594 node web/scripts/check-cloud-facts.mjs
595 node web/scripts/check-facts.mjs
596 - name: Propose the record to the default branch
597 env:
598 TAG: ${{ needs.resolve.outputs.tag }}
599 DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
600 PR_TOKEN: ${{ secrets.RELEASE_TAG_PAT }}
601 run: |
602 set -euo pipefail
603 paths=(
604 web/data/latest-published-release.json
605 docs/public-surface-facts.json
606 docs/cloud-facts/stable.json
607 web/lib/facts.generated.ts
608 )
609 if git diff --quiet -- "${paths[@]}"; then
610 echo "Release record already current on ${DEFAULT_BRANCH}; nothing to propose."
611 exit 0
612 fi
613 # GitHub's latest may already be newer than this run's tag; the
614 # record always follows GitHub, so name the branch after what it says.
615 recorded="$(node -p "require('./web/data/latest-published-release.json').tag")"
616 branch="chore/release-record-${recorded}"
617 title="chore(web): record ${recorded} as the published release"
618
619 git config user.name "CodeWhale Bot"
620 git config user.email "bot@codewhale.net"
621 git switch --quiet -c "${branch}"
622 git add -- "${paths[@]}"
623 git commit --quiet \
624 -m "${title}" \
625 -m "Written by release.yml sync-release-record after ${TAG} published. Gates run in the job: sync-latest-release --check, check-cloud-facts, check-facts."
626 git show --stat --format='%h %s' HEAD
627
628 if git ls-remote --exit-code --heads origin "${branch}" >/dev/null; then
629 echo "::notice title=Release record already proposed::Branch ${branch} exists; leaving it for review."
630 exit 0
631 fi
632 git push origin "HEAD:refs/heads/${branch}"
633
634 compare="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${DEFAULT_BRANCH}...${branch}?expand=1"
635 if [[ -n "${PR_TOKEN}" ]] && GH_TOKEN="${PR_TOKEN}" gh pr create \
636 --repo "${GITHUB_REPOSITORY}" --base "${DEFAULT_BRANCH}" --head "${branch}" \
637 --title "${title}" \
638 --body "Written by release.yml \`sync-release-record\` after ${TAG} published. Merging it turns \`check:latest-release\` green on ${DEFAULT_BRANCH}. No-Issue: release automation."; then
639 echo "::notice title=Release record proposed::Opened a PR from ${branch}."
640 exit 0
641 fi
642 echo "::error title=Release record PR not opened::Branch ${branch} is pushed; open and merge it: ${compare}"
643 exit 1
644
644 lines YAML