返回 CodeWhale
release-artifacts.yml
根目录 / .github / workflows / release-artifacts.yml
1 name: Release artifacts
2
3 on:
4 workflow_call:
5 inputs:
6 source_sha:
7 description: Exact 40-character source commit to build
8 required: true
9 type: string
10 version:
11 description: Workspace version without a v prefix
12 required: true
13 type: string
14 retention_days:
15 description: Retention for Actions-only intermediate and assembled artifacts
16 required: false
17 default: 7
18 type: number
19
20 permissions:
21 contents: read
22
23 env:
24 CARGO_TERM_COLOR: always
25 CARGO_INCREMENTAL: 0
26 RUSTFLAGS: -Dwarnings
27 # Build identity is the trusted workflow SHA. Callers pass source_sha only
28 # so `pin` can refuse a mismatch; it must not retarget checkout or caches.
29 CODEWHALE_BUILD_SHA: ${{ github.sha }}
30
31 jobs:
32 pin:
33 name: Pin caller SHA to this run
34 timeout-minutes: 10
35 runs-on: ubuntu-latest
36 steps:
37 - name: Require source_sha equals github.sha
38 env:
39 SOURCE_SHA: ${{ inputs.source_sha }}
40 run: |
41 set -euo pipefail
42 if [[ "${#SOURCE_SHA}" -ne 40 || "${SOURCE_SHA}" =~ [^0-9a-fA-F] ]]; then
43 echo "::error::source_sha must be a full 40-character commit SHA." >&2
44 exit 1
45 fi
46 expected="$(printf '%s' "${SOURCE_SHA}" | tr '[:upper:]' '[:lower:]')"
47 actual="$(printf '%s' "${GITHUB_SHA}" | tr '[:upper:]' '[:lower:]')"
48 if [[ "${actual}" != "${expected}" ]]; then
49 echo "::error::Reusable workflow SHA ${actual} does not match source_sha ${SOURCE_SHA}." >&2
50 exit 1
51 fi
52
53 build:
54 name: Build ${{ matrix.platform }}
55 timeout-minutes: 90
56 # FreeBSD is a source-build target validated via `cargo check --target x86_64-unknown-freebsd -p codewhale-cli --locked`
57 # (see packaging/freebsd/README.md and docs/INSTALL.md#freebsd). The 7×1 prebuilt matrix stays 7 targets;
58 # FreeBSD has no prebuilt asset, no npm binary, and no matrix bloat — it builds from source.
59 strategy:
60 fail-fast: false
61 matrix:
62 include:
63 - os: ubuntu-latest
64 target: x86_64-unknown-linux-musl
65 platform: linux-x64
66 cli_binary: codewhale
67 shim_binary: codew
68 cli_artifact: codewhale-linux-x64
69 shim_artifact: codew-linux-x64
70 compat_tui_artifact: codewhale-tui-linux-x64
71 - os: ubuntu-24.04-arm
72 target: aarch64-unknown-linux-musl
73 platform: linux-arm64
74 cli_binary: codewhale
75 shim_binary: codew
76 cli_artifact: codewhale-linux-arm64
77 shim_artifact: codew-linux-arm64
78 compat_tui_artifact: codewhale-tui-linux-arm64
79 - os: ubuntu-latest
80 target: aarch64-linux-android
81 platform: android-arm64
82 cli_binary: codewhale
83 shim_binary: codew
84 cli_artifact: codewhale-android-arm64
85 shim_artifact: codew-android-arm64
86 compat_tui_artifact: codewhale-tui-android-arm64
87 - os: macos-latest
88 target: x86_64-apple-darwin
89 platform: macos-x64
90 cli_binary: codewhale
91 shim_binary: codew
92 cli_artifact: codewhale-macos-x64
93 shim_artifact: codew-macos-x64
94 compat_tui_artifact: codewhale-tui-macos-x64
95 - os: macos-latest
96 target: aarch64-apple-darwin
97 platform: macos-arm64
98 cli_binary: codewhale
99 shim_binary: codew
100 cli_artifact: codewhale-macos-arm64
101 shim_artifact: codew-macos-arm64
102 compat_tui_artifact: codewhale-tui-macos-arm64
103 - os: windows-latest
104 target: x86_64-pc-windows-msvc
105 platform: windows-x64
106 cli_binary: codewhale.exe
107 shim_binary: codew.exe
108 cli_artifact: codewhale-windows-x64.exe
109 shim_artifact: codew-windows-x64.exe
110 compat_tui_artifact: codewhale-tui-windows-x64.exe
111 - os: windows-11-arm
112 target: aarch64-pc-windows-msvc
113 platform: windows-arm64
114 cli_binary: codewhale.exe
115 shim_binary: codew.exe
116 cli_artifact: codewhale-windows-arm64.exe
117 shim_artifact: codew-windows-arm64.exe
118 compat_tui_artifact: codewhale-tui-windows-arm64.exe
119 runs-on: ${{ matrix.os }}
120 needs: pin
121 steps:
122 # No ref: — GITHUB_SHA only. CodeQL treats workflow_call checkout-with-ref
123 # and any ref named *sha* as an untrusted checkout (cache-poisoning).
124 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
125 - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-07-18
126 with:
127 toolchain: stable
128 targets: ${{ matrix.target }}
129 - uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
130 id: sccache
131 continue-on-error: true
132 - name: Enable sccache
133 if: steps.sccache.outcome == 'success'
134 shell: bash
135 run: |
136 {
137 echo "SCCACHE_GHA_ENABLED=true"
138 echo "RUSTC_WRAPPER=sccache"
139 echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1"
140 } >> "${GITHUB_ENV}"
141 # Restore after the trusted lockfile is on disk. Key is OS + arch +
142 # explicit stable toolchain + rust-cache's Cargo.lock / rust-toolchain
143 # hash. Never interpolate github.event, github.ref, github.sha, or inputs.
144 - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
145 with:
146 cache-bin: false
147 prefix-key: v1-${{ runner.os }}-${{ runner.arch }}-stable
148 - name: Build static Linux binaries (musl)
149 if: endsWith(matrix.target, '-unknown-linux-musl')
150 shell: bash
151 run: |
152 sudo apt-get update
153 sudo apt-get install -y binutils musl-tools
154 rustup target add --toolchain stable ${{ matrix.target }}
155 cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli
156 - name: Configure Android NDK linker
157 if: matrix.target == 'aarch64-linux-android' && runner.os == 'Linux'
158 shell: bash
159 env:
160 ANDROID_NDK_VERSION: 27.2.12479018
161 run: |
162 set -euo pipefail
163 sudo apt-get update
164 sudo apt-get install -y libclang-dev
165 ndk="${ANDROID_NDK_ROOT:-${ANDROID_NDK_HOME:-}}"
166 linker=""
167 if [[ -n "${ndk}" ]]; then
168 linker="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
169 fi
170 if [[ -z "${linker}" || ! -x "${linker}" ]]; then
171 if ! command -v sdkmanager >/dev/null 2>&1; then
172 echo "sdkmanager is required to install Android NDK ${ANDROID_NDK_VERSION}" >&2
173 exit 1
174 fi
175 android_home="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
176 if [[ -z "${android_home}" ]]; then
177 echo "ANDROID_HOME or ANDROID_SDK_ROOT is required to install Android NDK ${ANDROID_NDK_VERSION}" >&2
178 exit 1
179 fi
180 yes | sdkmanager --licenses >/dev/null || true
181 sdkmanager --install "ndk;${ANDROID_NDK_VERSION}"
182 ndk="${android_home}/ndk/${ANDROID_NDK_VERSION}"
183 linker="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
184 fi
185 ar="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
186 if [[ ! -x "${linker}" ]]; then
187 echo "Android linker not found: ${linker}" >&2
188 exit 1
189 fi
190 if [[ ! -x "${ar}" ]]; then
191 echo "Android archiver not found: ${ar}" >&2
192 exit 1
193 fi
194 {
195 echo "ANDROID_NDK_ROOT=${ndk}"
196 echo "ANDROID_NDK_HOME=${ndk}"
197 echo "CC_aarch64_linux_android=${linker}"
198 echo "AR_aarch64_linux_android=${ar}"
199 echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${linker}"
200 echo "BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android=--target=aarch64-linux-android24 --sysroot=${ndk}/toolchains/llvm/prebuilt/linux-x86_64/sysroot"
201 } >> "${GITHUB_ENV}"
202 - name: Build
203 if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') }}
204 shell: bash
205 run: cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli
206 - name: Materialize codew command alias
207 shell: bash
208 run: |
209 bin_dir="target/${{ matrix.target }}/dist"
210 cp "${bin_dir}/${{ matrix.cli_binary }}" "${bin_dir}/${{ matrix.shim_binary }}"
211 cmp "${bin_dir}/${{ matrix.cli_binary }}" "${bin_dir}/${{ matrix.shim_binary }}"
212 - name: Verify static Linux binaries and launch on matching native runners
213 if: >-
214 endsWith(matrix.target, '-unknown-linux-musl') &&
215 ((startsWith(matrix.target, 'x86_64-') && runner.arch == 'X64') ||
216 (startsWith(matrix.target, 'aarch64-') && runner.arch == 'ARM64'))
217 shell: bash
218 run: |
219 set -euo pipefail
220 bin_dir="target/${{ matrix.target }}/dist"
221 for binary in "${{ matrix.cli_binary }}" "${{ matrix.shim_binary }}"; do
222 bin_path="${bin_dir}/${binary}"
223 if readelf -l "${bin_path}" | grep -Fq 'INTERP'; then
224 echo "Expected a static musl binary, but ${bin_path} has an ELF interpreter" >&2
225 exit 1
226 fi
227 "${bin_path}" --version
228 done
229 - name: Smoke binaries on matching native runners
230 if: >-
231 matrix.target != 'aarch64-linux-android' &&
232 ((startsWith(matrix.target, 'x86_64-') && runner.arch == 'X64') ||
233 (startsWith(matrix.target, 'aarch64-') && runner.arch == 'ARM64'))
234 shell: bash
235 run: |
236 bin_dir="target/${{ matrix.target }}/dist"
237 "${bin_dir}/${{ matrix.cli_binary }}" --version
238 "${bin_dir}/${{ matrix.shim_binary }}" --version
239 - name: Stage binaries
240 shell: bash
241 run: |
242 stage_binary() {
243 local binary="$1"
244 local artifact="$2"
245 local bin_path="target/${{ matrix.target }}/dist/${binary}"
246 if [[ ! -f "${bin_path}" ]]; then
247 echo "Binary not at ${bin_path}; searching target/ for ${binary}:" >&2
248 find target -name "${binary}" -type f
249 exit 1
250 fi
251 cp "${bin_path}" "${artifact}"
252 }
253
254 stage_binary "${{ matrix.cli_binary }}" "${{ matrix.cli_artifact }}"
255 stage_binary "${{ matrix.shim_binary }}" "${{ matrix.shim_artifact }}"
256 # Compatibility bridge for v0.9.4's hard-coded release
257 # completeness/updater contract. This is the same runtime, not a
258 # separately compiled or installed TUI command.
259 stage_binary "${{ matrix.cli_binary }}" "${{ matrix.compat_tui_artifact }}"
260 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
261 with:
262 name: ${{ matrix.cli_artifact }}
263 path: ${{ matrix.cli_artifact }}
264 if-no-files-found: error
265 retention-days: ${{ inputs.retention_days }}
266 overwrite: true
267 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
268 with:
269 name: ${{ matrix.shim_artifact }}
270 path: ${{ matrix.shim_artifact }}
271 if-no-files-found: error
272 retention-days: ${{ inputs.retention_days }}
273 overwrite: true
274 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
275 with:
276 name: ${{ matrix.compat_tui_artifact }}
277 path: ${{ matrix.compat_tui_artifact }}
278 if-no-files-found: error
279 retention-days: ${{ inputs.retention_days }}
280 overwrite: true
281
282 bundle:
283 timeout-minutes: 15
284 needs: build
285 if: ${{ !cancelled() && needs.build.result == 'success' }}
286 runs-on: ubuntu-latest
287 steps:
288 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
289 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
290 with:
291 path: artifacts
292 pattern: '*'
293 - name: Create and checksum platform archives
294 shell: bash
295 env:
296 SOURCE_SHA: ${{ github.sha }}
297 run: |
298 set -euo pipefail
299 source_date_epoch="$(git show -s --format=%ct "${SOURCE_SHA}")"
300 if [[ ! "${source_date_epoch}" =~ ^[0-9]+$ ]]; then
301 echo "Could not read a Unix timestamp for source commit ${SOURCE_SHA}" >&2
302 exit 1
303 fi
304 SOURCE_DATE_EPOCH="${source_date_epoch}" \
305 bash scripts/release/create-release-bundles.sh artifacts bundles
306 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
307 with:
308 name: codewhale-bundles
309 path: |
310 bundles/*.tar.gz
311 bundles/*.zip
312 bundles/codewhale-bundles-sha256.txt
313 if-no-files-found: error
314 retention-days: ${{ inputs.retention_days }}
315 overwrite: true
316
317 windows-installer:
318 timeout-minutes: 15
319 needs: build
320 if: ${{ !cancelled() && needs.build.result == 'success' }}
321 runs-on: windows-latest
322 steps:
323 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
324 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
325 with:
326 path: artifacts
327 pattern: '*windows-x64.exe'
328 - name: Install NSIS
329 shell: pwsh
330 run: choco install nsis -y --no-progress
331 - name: Build NSIS installer
332 shell: pwsh
333 run: |
334 $ErrorActionPreference = "Stop"
335 Copy-Item "artifacts\codewhale-windows-x64.exe\codewhale-windows-x64.exe" "scripts\installer\codewhale.exe"
336 Copy-Item "artifacts\codew-windows-x64.exe\codew-windows-x64.exe" "scripts\installer\codew.exe"
337 $makensis = "${env:ProgramFiles(x86)}\NSIS\makensis.exe"
338 if (!(Test-Path $makensis)) {
339 $makensis = "${env:ProgramFiles}\NSIS\makensis.exe"
340 }
341 if (!(Test-Path $makensis)) {
342 throw "makensis.exe not found after NSIS install"
343 }
344 Push-Location scripts\installer
345 & $makensis "/DVERSION=${{ inputs.version }}" "codewhale.nsi"
346 Pop-Location
347 if (!(Test-Path "scripts\installer\CodeWhaleSetup.exe")) {
348 throw "CodeWhaleSetup.exe was not produced"
349 }
350 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
351 with:
352 name: CodeWhaleSetup.exe
353 path: scripts/installer/CodeWhaleSetup.exe
354 if-no-files-found: error
355 retention-days: ${{ inputs.retention_days }}
356 overwrite: true
357
358 assemble:
359 timeout-minutes: 15
360 needs: [bundle, windows-installer]
361 if: ${{ !cancelled() && needs.bundle.result == 'success' && needs.windows-installer.result == 'success' }}
362 runs-on: ubuntu-latest
363 steps:
364 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
365 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
366 with:
367 node-version: 20
368 package-manager-cache: false
369 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
370 with:
371 path: intermediate-artifacts
372 pattern: '*'
373 - name: Assemble exact authoritative release inventory
374 run: node scripts/release/assemble-release-assets.js intermediate-artifacts release-assets
375 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
376 with:
377 name: codewhale-release-assets
378 path: release-assets/*
379 if-no-files-found: error
380 retention-days: ${{ inputs.retention_days }}
381 compression-level: 0
382 overwrite: true
383
384 smoke:
385 timeout-minutes: 15
386 needs: assemble
387 if: ${{ !cancelled() && needs.assemble.result == 'success' }}
388 runs-on: ubuntu-latest
389 steps:
390 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
391 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
392 with:
393 node-version: 20
394 package-manager-cache: false
395 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
396 with:
397 name: codewhale-release-assets
398 path: release-assets
399 - name: Verify 34-asset bridge inventory and checksum manifests (single binary)
400 run: node scripts/release/assemble-release-assets.js --verify release-assets
401 - name: Test release inventory contracts
402 run: |
403 node --test scripts/release/assemble-release-assets.test.js
404 node --test npm/codewhale/test/artifacts.test.js npm/codewhale/test/release-assets.test.js
405 - name: Render AUR metadata from candidate Linux archives
406 run: bash packaging/aur/render.sh release-assets "${RUNNER_TEMP}/codewhale-bin"
407 - name: Smoke packed npm wrapper against candidate assets
408 env:
409 CODEWHALE_SMOKE_ASSETS_DIR: ${{ github.workspace }}/release-assets
410 run: node scripts/release/npm-wrapper-smoke.js
411 - name: Record non-public candidate identity
412 shell: bash
413 run: |
414 {
415 echo "### Release artifact candidate"
416 echo ""
417 echo "- Source: \`${{ github.sha }}\`"
418 echo "- Version metadata: \`${{ inputs.version }}\`"
419 echo "- Inventory: 7 targets / 34 files (single binary; 7 legacy alias assets)"
420 echo "- Publication: none (Actions artifact \`codewhale-release-assets\` only)"
421 } >> "${GITHUB_STEP_SUMMARY}"
422
422 lines YAML