返回 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 runs-on: ubuntu-latest
28 outputs:
29 tag: ${{ steps.release.outputs.tag }}
30 sha: ${{ steps.release.outputs.sha }}
31 version: ${{ steps.release.outputs.version }}
32 steps:
33 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
34 with:
35 fetch-depth: 0
36 - name: Resolve release source
37 id: release
38 shell: bash
39 env:
40 INPUT_VERSION: ${{ inputs.version }}
41 run: |
42 set -euo pipefail
43
44 if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
45 if ! [[ "${INPUT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
46 echo "::error::Release version '${INPUT_VERSION}' must use X.Y.Z." >&2
47 exit 1
48 fi
49 tag="v${INPUT_VERSION}"
50 if [[ "${GITHUB_REF}" != "refs/tags/${tag}" ]]; then
51 echo "::error::Dispatch release.yml from --ref ${tag}, not ${GITHUB_REF}." >&2
52 exit 1
53 fi
54 else
55 tag="${GITHUB_REF_NAME}"
56 fi
57
58 if ! [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
59 echo "::error::Release tag '${tag}' must use vX.Y.Z." >&2
60 exit 1
61 fi
62 if ! git rev-parse --verify "refs/tags/${tag}^{commit}" >/dev/null 2>&1; then
63 echo "::error::Release tag ${tag} does not exist. Create it from the frozen main commit before dispatching." >&2
64 exit 1
65 fi
66
67 sha="$(git rev-parse "refs/tags/${tag}^{commit}")"
68 event_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")"
69 if [[ "${event_sha}" != "${sha}" ]]; then
70 echo "::error::Trigger SHA ${event_sha} does not match ${tag} at ${sha}; the tag moved after this run was created." >&2
71 exit 1
72 fi
73
74 {
75 echo "tag=${tag}"
76 echo "sha=${sha}"
77 echo "version=${tag#v}"
78 } >> "${GITHUB_OUTPUT}"
79 - name: Validate tagged release metadata
80 shell: bash
81 env:
82 SHA: ${{ steps.release.outputs.sha }}
83 TAG: ${{ steps.release.outputs.tag }}
84 run: |
85 set -euo pipefail
86 git checkout --detach "${SHA}"
87 expected="${TAG#v}"
88 workspace_version="$(grep -E '^version = "' Cargo.toml | head -n1 | sed -E 's/^version = "([^"]+)".*/\1/')"
89 npm_version="$(node -p "require('./npm/codewhale/package.json').version")"
90 binary_version="$(node -p "require('./npm/codewhale/package.json').codewhaleBinaryVersion")"
91 sdk_version="$(node -p "require('./npm/runtime-sdk/package.json').version")"
92 vscode_version="$(node -p "require('./extensions/vscode/package.json').version")"
93 for pair in \
94 "workspace:${workspace_version}" \
95 "npm:${npm_version}" \
96 "npm binary:${binary_version}" \
97 "runtime-sdk:${sdk_version}" \
98 "vscode:${vscode_version}"; do
99 label="${pair%%:*}"
100 actual="${pair#*:}"
101 if [[ "${actual}" != "${expected}" ]]; then
102 echo "::error::${label} version ${actual} does not match tag ${TAG}." >&2
103 exit 1
104 fi
105 done
106 ./scripts/release/check-versions.sh --require-dated-release
107 - name: Require release source on main
108 run: ./scripts/release/ensure-release-on-main.sh "${{ steps.release.outputs.sha }}"
109 - name: Refuse an existing public asset set
110 env:
111 GH_TOKEN: ${{ github.token }}
112 TAG: ${{ steps.release.outputs.tag }}
113 run: node scripts/release/ensure-release-assets-absent.js "${GITHUB_REPOSITORY}" "${TAG}"
114
115 parity:
116 needs: resolve
117 runs-on: ubuntu-latest
118 steps:
119 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
120 with:
121 ref: ${{ needs.resolve.outputs.sha }}
122 - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master 2026-07-18
123 with:
124 toolchain: stable
125 components: clippy, rustfmt
126 - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
127 id: sccache
128 continue-on-error: true
129 - name: Enable sccache
130 if: steps.sccache.outcome == 'success'
131 shell: bash
132 run: |
133 {
134 echo "SCCACHE_GHA_ENABLED=true"
135 echo "RUSTC_WRAPPER=sccache"
136 echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1"
137 } >> "${GITHUB_ENV}"
138 - name: Install Linux system dependencies
139 run: |
140 for i in 1 2 3 4 5; do
141 sudo apt-get update && break
142 echo "apt-get update failed (attempt $i); retrying in 15s"
143 sleep 15
144 done
145 sudo apt-get install -y libdbus-1-dev pkg-config
146 - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
147 with:
148 cache-bin: false
149 - name: Format check
150 run: cargo fmt --all -- --check
151 - name: Compile check
152 run: cargo check --workspace --all-targets --locked
153 - name: OHOS dependency graph
154 run: ./scripts/release/check-ohos-deps.sh
155 - name: Clippy
156 run: |
157 cargo clippy --workspace --all-targets --all-features --locked -- \
158 -D warnings \
159 -A clippy::uninlined_format_args \
160 -A clippy::too_many_arguments \
161 -A clippy::unnecessary_map_or \
162 -A clippy::collapsible_if \
163 -A clippy::assertions_on_constants
164 - name: Workspace tests
165 run: cargo test --workspace --all-features --locked
166 env:
167 # Match the CI test lane: test threads get the same stack the product
168 # gives itself (main.rs CODEWHALE_MAIN_STACK_BYTES). See the note in
169 # ci.yml's "Run tests" step. Without it this gate runs the deep
170 # engine/runtime futures on a stack that never ships.
171 RUST_MIN_STACK: '16777216'
172 - name: Protocol schema parity
173 run: cargo test -p codewhale-protocol --test parity_protocol --locked
174 - name: State persistence parity
175 run: cargo test -p codewhale-state --test parity_state --locked
176 - name: Lockfile drift guard
177 run: git diff --exit-code -- Cargo.lock
178
179 artifacts:
180 needs: [parity, resolve]
181 if: ${{ !cancelled() && needs.resolve.result == 'success' && needs.parity.result == 'success' }}
182 uses: ./.github/workflows/release-artifacts.yml
183 with:
184 source_sha: ${{ needs.resolve.outputs.sha }}
185 version: ${{ needs.resolve.outputs.version }}
186 retention_days: 14
187
188 docker:
189 needs: [artifacts, resolve]
190 if: ${{ !cancelled() && needs.artifacts.result == 'success' }}
191 runs-on: ubuntu-latest
192 permissions:
193 contents: read
194 packages: write
195 steps:
196 - name: Checkout release source
197 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
198 with:
199 ref: ${{ needs.resolve.outputs.sha }}
200 path: source
201 - name: Checkout release infrastructure
202 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
203 with:
204 ref: ${{ needs.resolve.outputs.sha }}
205 path: infra
206 - name: Set up QEMU
207 uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
208 - name: Set up Docker Buildx
209 uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
210 - name: Log in to GitHub Container Registry
211 uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4
212 with:
213 registry: ghcr.io
214 username: ${{ github.repository_owner }}
215 password: ${{ secrets.GITHUB_TOKEN }}
216 - name: Normalize image name
217 id: image
218 shell: bash
219 run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
220 - name: Extract metadata
221 id: meta
222 uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
223 with:
224 images: |
225 ${{ steps.image.outputs.name }}
226 tags: |
227 type=semver,pattern={{version}}
228 type=semver,pattern={{major}}.{{minor}}
229 type=semver,pattern=v{{major}}
230 type=ref,event=tag
231 type=semver,pattern={{version}},value=${{ needs.resolve.outputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
232 type=semver,pattern={{major}}.{{minor}},value=${{ needs.resolve.outputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
233 type=semver,pattern=v{{major}},value=${{ needs.resolve.outputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
234 type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
235 type=raw,value=v${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
236 type=raw,value=latest
237 - name: Revalidate release tag before container publish
238 env:
239 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
240 TAG: ${{ needs.resolve.outputs.tag }}
241 run: |
242 ./infra/scripts/release/verify-remote-tag.sh \
243 "https://github.com/${GITHUB_REPOSITORY}.git" \
244 "${TAG}" \
245 "${EXPECTED_SHA}"
246 - name: Build and push
247 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
248 env:
249 DOCKER_BUILD_RECORD_UPLOAD: false
250 DOCKER_BUILD_SUMMARY: false
251 with:
252 context: source
253 file: infra/Dockerfile
254 platforms: linux/amd64,linux/arm64
255 push: true
256 provenance: mode=max
257 sbom: true
258 build-args: |
259 DEEPSEEK_BUILD_SHA=${{ needs.resolve.outputs.sha }}
260 tags: ${{ steps.meta.outputs.tags }}
261 labels: ${{ steps.meta.outputs.labels }}
262 cache-from: type=gha
263 cache-to: type=gha,mode=max
264 - name: Smoke published container entrypoints
265 shell: bash
266 env:
267 IMAGE: ${{ steps.image.outputs.name }}:${{ needs.resolve.outputs.tag }}
268 run: |
269 set -euo pipefail
270 docker pull "${IMAGE}"
271 docker run --rm --entrypoint codewhale "${IMAGE}" --version
272 docker run --rm --entrypoint codew "${IMAGE}" --version
273 docker run --rm --entrypoint codewhale-tui "${IMAGE}" --version
274
275 release:
276 needs: [artifacts, docker, resolve]
277 if: ${{ !cancelled() && needs.artifacts.result == 'success' && needs.docker.result == 'success' }}
278 runs-on: ubuntu-latest
279 permissions:
280 contents: write
281 steps:
282 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
283 with:
284 ref: ${{ needs.resolve.outputs.sha }}
285 path: repo
286 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
287 with:
288 node-version: 20
289 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
290 with:
291 name: codewhale-release-assets
292 path: artifacts
293 - name: Revalidate exact authoritative asset set
294 run: node repo/scripts/release/assemble-release-assets.js --verify artifacts
295 - name: Generate release body from CHANGELOG
296 shell: bash
297 run: |
298 ./repo/scripts/release/generate-release-body.sh \
299 "${{ needs.resolve.outputs.tag }}" repo/CHANGELOG.md > release-body.md
300 - name: Revalidate release tag before GitHub Release write
301 env:
302 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
303 TAG: ${{ needs.resolve.outputs.tag }}
304 run: |
305 ./repo/scripts/release/verify-remote-tag.sh \
306 "https://github.com/${GITHUB_REPOSITORY}.git" \
307 "${TAG}" \
308 "${EXPECTED_SHA}"
309 - name: Reconfirm public asset set is still empty
310 env:
311 GH_TOKEN: ${{ github.token }}
312 TAG: ${{ needs.resolve.outputs.tag }}
313 run: node repo/scripts/release/ensure-release-assets-absent.js "${GITHUB_REPOSITORY}" "${TAG}"
314 - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
315 with:
316 tag_name: ${{ needs.resolve.outputs.tag }}
317 files: artifacts/*
318 prerelease: false
319 body_path: release-body.md
320 overwrite_files: false
321 fail_on_unmatched_files: true
322
323 homebrew:
324 needs: [release, resolve]
325 if: ${{ !cancelled() && needs.release.result == 'success' }}
326 runs-on: ubuntu-latest
327 permissions:
328 contents: read
329 steps:
330 - name: Check Homebrew tap token
331 id: homebrew-token
332 env:
333 TOKEN: ${{ secrets.HOMEBREW_TAP_PAT || secrets.RELEASE_TAG_PAT }}
334 run: |
335 if [[ -z "${TOKEN:-}" ]]; then
336 echo "No Homebrew tap token configured; skipping tap update."
337 echo "available=false" >> "${GITHUB_OUTPUT}"
338 else
339 echo "available=true" >> "${GITHUB_OUTPUT}"
340 fi
341 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
342 if: steps.homebrew-token.outputs.available == 'true'
343 with:
344 ref: ${{ needs.resolve.outputs.sha }}
345 - name: Download checksum manifest
346 if: steps.homebrew-token.outputs.available == 'true'
347 env:
348 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
349 run: |
350 gh release download "${{ needs.resolve.outputs.tag }}" \
351 --repo "${{ github.repository }}" \
352 --pattern 'codewhale-artifacts-sha256.txt' \
353 --dir /tmp
354 - name: Revalidate release tag before Homebrew tap write
355 if: steps.homebrew-token.outputs.available == 'true'
356 env:
357 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
358 TAG: ${{ needs.resolve.outputs.tag }}
359 run: |
360 ./scripts/release/verify-remote-tag.sh \
361 "https://github.com/${GITHUB_REPOSITORY}.git" \
362 "${TAG}" \
363 "${EXPECTED_SHA}"
364 - name: Update Homebrew tap
365 if: steps.homebrew-token.outputs.available == 'true'
366 env:
367 TAG: ${{ needs.resolve.outputs.tag }}
368 MANIFEST: /tmp/codewhale-artifacts-sha256.txt
369 TAP_REPO: Hmbown/homebrew-deepseek-tui
370 TOKEN: ${{ secrets.HOMEBREW_TAP_PAT || secrets.RELEASE_TAG_PAT }}
371 run: bash .github/scripts/update-homebrew-tap.sh
372
373 # npm publish is intentionally not automated. The npm account requires 2FA OTP
374 # on every publish. Publish the wrapper manually only after the immutable public
375 # GitHub asset gate in docs/RELEASE_RUNBOOK.md succeeds.
376
376 lines YAML