| 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 | DEEPSEEK_BUILD_SHA: ${{ inputs.source_sha }} |
| 28 | |
| 29 | jobs: |
| 30 | build: |
| 31 | name: Build ${{ matrix.platform }} |
| 32 | strategy: |
| 33 | fail-fast: false |
| 34 | matrix: |
| 35 | include: |
| 36 | - os: ubuntu-latest |
| 37 | target: x86_64-unknown-linux-musl |
| 38 | platform: linux-x64 |
| 39 | cli_binary: codewhale |
| 40 | shim_binary: codew |
| 41 | tui_binary: codewhale-tui |
| 42 | cli_artifact: codewhale-linux-x64 |
| 43 | shim_artifact: codew-linux-x64 |
| 44 | tui_artifact: codewhale-tui-linux-x64 |
| 45 | - os: ubuntu-24.04-arm |
| 46 | target: aarch64-unknown-linux-gnu |
| 47 | platform: linux-arm64 |
| 48 | cli_binary: codewhale |
| 49 | shim_binary: codew |
| 50 | tui_binary: codewhale-tui |
| 51 | cli_artifact: codewhale-linux-arm64 |
| 52 | shim_artifact: codew-linux-arm64 |
| 53 | tui_artifact: codewhale-tui-linux-arm64 |
| 54 | - os: ubuntu-latest |
| 55 | target: aarch64-linux-android |
| 56 | platform: android-arm64 |
| 57 | cli_binary: codewhale |
| 58 | shim_binary: codew |
| 59 | tui_binary: codewhale-tui |
| 60 | cli_artifact: codewhale-android-arm64 |
| 61 | shim_artifact: codew-android-arm64 |
| 62 | tui_artifact: codewhale-tui-android-arm64 |
| 63 | - os: macos-latest |
| 64 | target: x86_64-apple-darwin |
| 65 | platform: macos-x64 |
| 66 | cli_binary: codewhale |
| 67 | shim_binary: codew |
| 68 | tui_binary: codewhale-tui |
| 69 | cli_artifact: codewhale-macos-x64 |
| 70 | shim_artifact: codew-macos-x64 |
| 71 | tui_artifact: codewhale-tui-macos-x64 |
| 72 | - os: macos-latest |
| 73 | target: aarch64-apple-darwin |
| 74 | platform: macos-arm64 |
| 75 | cli_binary: codewhale |
| 76 | shim_binary: codew |
| 77 | tui_binary: codewhale-tui |
| 78 | cli_artifact: codewhale-macos-arm64 |
| 79 | shim_artifact: codew-macos-arm64 |
| 80 | tui_artifact: codewhale-tui-macos-arm64 |
| 81 | - os: windows-latest |
| 82 | target: x86_64-pc-windows-msvc |
| 83 | platform: windows-x64 |
| 84 | cli_binary: codewhale.exe |
| 85 | shim_binary: codew.exe |
| 86 | tui_binary: codewhale-tui.exe |
| 87 | cli_artifact: codewhale-windows-x64.exe |
| 88 | shim_artifact: codew-windows-x64.exe |
| 89 | tui_artifact: codewhale-tui-windows-x64.exe |
| 90 | - os: windows-11-arm |
| 91 | target: aarch64-pc-windows-msvc |
| 92 | platform: windows-arm64 |
| 93 | cli_binary: codewhale.exe |
| 94 | shim_binary: codew.exe |
| 95 | tui_binary: codewhale-tui.exe |
| 96 | cli_artifact: codewhale-windows-arm64.exe |
| 97 | shim_artifact: codew-windows-arm64.exe |
| 98 | tui_artifact: codewhale-tui-windows-arm64.exe |
| 99 | runs-on: ${{ matrix.os }} |
| 100 | steps: |
| 101 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 102 | with: |
| 103 | ref: ${{ inputs.source_sha }} |
| 104 | - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master 2026-07-18 |
| 105 | with: |
| 106 | toolchain: stable |
| 107 | targets: ${{ matrix.target }} |
| 108 | - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 |
| 109 | id: sccache |
| 110 | continue-on-error: true |
| 111 | - name: Enable sccache |
| 112 | if: steps.sccache.outcome == 'success' |
| 113 | shell: bash |
| 114 | run: | |
| 115 | { |
| 116 | echo "SCCACHE_GHA_ENABLED=true" |
| 117 | echo "RUSTC_WRAPPER=sccache" |
| 118 | echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" |
| 119 | } >> "${GITHUB_ENV}" |
| 120 | - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 |
| 121 | with: |
| 122 | cache-bin: false |
| 123 | - name: Install Linux ARM64 system dependencies |
| 124 | if: matrix.target == 'aarch64-unknown-linux-gnu' |
| 125 | run: | |
| 126 | for i in 1 2 3 4 5; do |
| 127 | sudo apt-get update && break |
| 128 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 129 | sleep 15 |
| 130 | done |
| 131 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 132 | - name: Build static Linux x64 binaries (musl) |
| 133 | if: matrix.target == 'x86_64-unknown-linux-musl' |
| 134 | shell: bash |
| 135 | run: | |
| 136 | sudo apt-get update |
| 137 | sudo apt-get install -y musl-tools |
| 138 | rustup target add --toolchain stable x86_64-unknown-linux-musl |
| 139 | cargo build --profile dist --locked --target x86_64-unknown-linux-musl -p codewhale-cli -p codewhale-tui |
| 140 | - name: Configure Android NDK linker |
| 141 | if: matrix.target == 'aarch64-linux-android' && runner.os == 'Linux' |
| 142 | shell: bash |
| 143 | env: |
| 144 | ANDROID_NDK_VERSION: 27.2.12479018 |
| 145 | run: | |
| 146 | set -euo pipefail |
| 147 | sudo apt-get update |
| 148 | sudo apt-get install -y libclang-dev |
| 149 | ndk="${ANDROID_NDK_ROOT:-${ANDROID_NDK_HOME:-}}" |
| 150 | linker="" |
| 151 | if [[ -n "${ndk}" ]]; then |
| 152 | linker="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" |
| 153 | fi |
| 154 | if [[ -z "${linker}" || ! -x "${linker}" ]]; then |
| 155 | if ! command -v sdkmanager >/dev/null 2>&1; then |
| 156 | echo "sdkmanager is required to install Android NDK ${ANDROID_NDK_VERSION}" >&2 |
| 157 | exit 1 |
| 158 | fi |
| 159 | android_home="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" |
| 160 | if [[ -z "${android_home}" ]]; then |
| 161 | echo "ANDROID_HOME or ANDROID_SDK_ROOT is required to install Android NDK ${ANDROID_NDK_VERSION}" >&2 |
| 162 | exit 1 |
| 163 | fi |
| 164 | yes | sdkmanager --licenses >/dev/null || true |
| 165 | sdkmanager --install "ndk;${ANDROID_NDK_VERSION}" |
| 166 | ndk="${android_home}/ndk/${ANDROID_NDK_VERSION}" |
| 167 | linker="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" |
| 168 | fi |
| 169 | ar="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" |
| 170 | if [[ ! -x "${linker}" ]]; then |
| 171 | echo "Android linker not found: ${linker}" >&2 |
| 172 | exit 1 |
| 173 | fi |
| 174 | if [[ ! -x "${ar}" ]]; then |
| 175 | echo "Android archiver not found: ${ar}" >&2 |
| 176 | exit 1 |
| 177 | fi |
| 178 | { |
| 179 | echo "ANDROID_NDK_ROOT=${ndk}" |
| 180 | echo "ANDROID_NDK_HOME=${ndk}" |
| 181 | echo "CC_aarch64_linux_android=${linker}" |
| 182 | echo "AR_aarch64_linux_android=${ar}" |
| 183 | echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${linker}" |
| 184 | echo "BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android=--target=aarch64-linux-android24 --sysroot=${ndk}/toolchains/llvm/prebuilt/linux-x86_64/sysroot" |
| 185 | } >> "${GITHUB_ENV}" |
| 186 | - name: Build |
| 187 | if: matrix.target != 'x86_64-unknown-linux-musl' |
| 188 | shell: bash |
| 189 | run: cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli -p codewhale-tui |
| 190 | - name: Smoke binaries on matching native runners |
| 191 | if: >- |
| 192 | matrix.target != 'aarch64-linux-android' && |
| 193 | ((startsWith(matrix.target, 'x86_64-') && runner.arch == 'X64') || |
| 194 | (startsWith(matrix.target, 'aarch64-') && runner.arch == 'ARM64')) |
| 195 | shell: bash |
| 196 | run: | |
| 197 | bin_dir="target/${{ matrix.target }}/dist" |
| 198 | "${bin_dir}/${{ matrix.cli_binary }}" --version |
| 199 | "${bin_dir}/${{ matrix.shim_binary }}" --version |
| 200 | "${bin_dir}/${{ matrix.tui_binary }}" --version |
| 201 | - name: Stage binaries |
| 202 | shell: bash |
| 203 | run: | |
| 204 | stage_binary() { |
| 205 | local binary="$1" |
| 206 | local artifact="$2" |
| 207 | local bin_path="target/${{ matrix.target }}/dist/${binary}" |
| 208 | if [[ ! -f "${bin_path}" ]]; then |
| 209 | echo "Binary not at ${bin_path}; searching target/ for ${binary}:" >&2 |
| 210 | find target -name "${binary}" -type f |
| 211 | exit 1 |
| 212 | fi |
| 213 | cp "${bin_path}" "${artifact}" |
| 214 | } |
| 215 | |
| 216 | stage_binary "${{ matrix.cli_binary }}" "${{ matrix.cli_artifact }}" |
| 217 | stage_binary "${{ matrix.shim_binary }}" "${{ matrix.shim_artifact }}" |
| 218 | stage_binary "${{ matrix.tui_binary }}" "${{ matrix.tui_artifact }}" |
| 219 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 220 | with: |
| 221 | name: ${{ matrix.cli_artifact }} |
| 222 | path: ${{ matrix.cli_artifact }} |
| 223 | if-no-files-found: error |
| 224 | retention-days: ${{ inputs.retention_days }} |
| 225 | overwrite: true |
| 226 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 227 | with: |
| 228 | name: ${{ matrix.shim_artifact }} |
| 229 | path: ${{ matrix.shim_artifact }} |
| 230 | if-no-files-found: error |
| 231 | retention-days: ${{ inputs.retention_days }} |
| 232 | overwrite: true |
| 233 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 234 | with: |
| 235 | name: ${{ matrix.tui_artifact }} |
| 236 | path: ${{ matrix.tui_artifact }} |
| 237 | if-no-files-found: error |
| 238 | retention-days: ${{ inputs.retention_days }} |
| 239 | overwrite: true |
| 240 | |
| 241 | bundle: |
| 242 | needs: build |
| 243 | if: ${{ !cancelled() && needs.build.result == 'success' }} |
| 244 | runs-on: ubuntu-latest |
| 245 | steps: |
| 246 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 247 | with: |
| 248 | ref: ${{ inputs.source_sha }} |
| 249 | - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 250 | with: |
| 251 | path: artifacts |
| 252 | pattern: '*' |
| 253 | - name: Create and checksum platform archives |
| 254 | shell: bash |
| 255 | run: bash scripts/release/create-release-bundles.sh artifacts bundles |
| 256 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 257 | with: |
| 258 | name: codewhale-bundles |
| 259 | path: | |
| 260 | bundles/*.tar.gz |
| 261 | bundles/*.zip |
| 262 | bundles/codewhale-bundles-sha256.txt |
| 263 | if-no-files-found: error |
| 264 | retention-days: ${{ inputs.retention_days }} |
| 265 | overwrite: true |
| 266 | |
| 267 | windows-installer: |
| 268 | needs: build |
| 269 | if: ${{ !cancelled() && needs.build.result == 'success' }} |
| 270 | runs-on: windows-latest |
| 271 | steps: |
| 272 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 273 | with: |
| 274 | ref: ${{ inputs.source_sha }} |
| 275 | - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 276 | with: |
| 277 | path: artifacts |
| 278 | pattern: '*windows-x64.exe' |
| 279 | - name: Install NSIS |
| 280 | shell: pwsh |
| 281 | run: choco install nsis -y --no-progress |
| 282 | - name: Build NSIS installer |
| 283 | shell: pwsh |
| 284 | run: | |
| 285 | $ErrorActionPreference = "Stop" |
| 286 | Copy-Item "artifacts\codewhale-windows-x64.exe\codewhale-windows-x64.exe" "scripts\installer\codewhale.exe" |
| 287 | Copy-Item "artifacts\codew-windows-x64.exe\codew-windows-x64.exe" "scripts\installer\codew.exe" |
| 288 | Copy-Item "artifacts\codewhale-tui-windows-x64.exe\codewhale-tui-windows-x64.exe" "scripts\installer\codewhale-tui.exe" |
| 289 | $makensis = "${env:ProgramFiles(x86)}\NSIS\makensis.exe" |
| 290 | if (!(Test-Path $makensis)) { |
| 291 | $makensis = "${env:ProgramFiles}\NSIS\makensis.exe" |
| 292 | } |
| 293 | if (!(Test-Path $makensis)) { |
| 294 | throw "makensis.exe not found after NSIS install" |
| 295 | } |
| 296 | Push-Location scripts\installer |
| 297 | & $makensis "/DVERSION=${{ inputs.version }}" "codewhale.nsi" |
| 298 | Pop-Location |
| 299 | if (!(Test-Path "scripts\installer\CodeWhaleSetup.exe")) { |
| 300 | throw "CodeWhaleSetup.exe was not produced" |
| 301 | } |
| 302 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 303 | with: |
| 304 | name: CodeWhaleSetup.exe |
| 305 | path: scripts/installer/CodeWhaleSetup.exe |
| 306 | if-no-files-found: error |
| 307 | retention-days: ${{ inputs.retention_days }} |
| 308 | overwrite: true |
| 309 | |
| 310 | assemble: |
| 311 | needs: [bundle, windows-installer] |
| 312 | if: ${{ !cancelled() && needs.bundle.result == 'success' && needs.windows-installer.result == 'success' }} |
| 313 | runs-on: ubuntu-latest |
| 314 | steps: |
| 315 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 316 | with: |
| 317 | ref: ${{ inputs.source_sha }} |
| 318 | - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 |
| 319 | with: |
| 320 | node-version: 20 |
| 321 | - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 322 | with: |
| 323 | path: intermediate-artifacts |
| 324 | pattern: '*' |
| 325 | - name: Assemble exact authoritative release inventory |
| 326 | run: node scripts/release/assemble-release-assets.js intermediate-artifacts release-assets |
| 327 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 328 | with: |
| 329 | name: codewhale-release-assets |
| 330 | path: release-assets/* |
| 331 | if-no-files-found: error |
| 332 | retention-days: ${{ inputs.retention_days }} |
| 333 | compression-level: 0 |
| 334 | overwrite: true |
| 335 | |
| 336 | smoke: |
| 337 | needs: assemble |
| 338 | if: ${{ !cancelled() && needs.assemble.result == 'success' }} |
| 339 | runs-on: ubuntu-latest |
| 340 | steps: |
| 341 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 342 | with: |
| 343 | ref: ${{ inputs.source_sha }} |
| 344 | - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 |
| 345 | with: |
| 346 | node-version: 20 |
| 347 | - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 348 | with: |
| 349 | name: codewhale-release-assets |
| 350 | path: release-assets |
| 351 | - name: Verify 34-asset inventory and checksum manifests |
| 352 | run: node scripts/release/assemble-release-assets.js --verify release-assets |
| 353 | - name: Test release inventory contracts |
| 354 | run: | |
| 355 | node --test scripts/release/assemble-release-assets.test.js |
| 356 | node --test npm/codewhale/test/artifacts.test.js npm/codewhale/test/release-assets.test.js |
| 357 | - name: Smoke packed npm wrapper against candidate assets |
| 358 | env: |
| 359 | CODEWHALE_SMOKE_ASSETS_DIR: ${{ github.workspace }}/release-assets |
| 360 | run: node scripts/release/npm-wrapper-smoke.js |
| 361 | - name: Record non-public candidate identity |
| 362 | shell: bash |
| 363 | run: | |
| 364 | { |
| 365 | echo "### Release artifact candidate" |
| 366 | echo "" |
| 367 | echo "- Source: \`${{ inputs.source_sha }}\`" |
| 368 | echo "- Version metadata: \`${{ inputs.version }}\`" |
| 369 | echo "- Inventory: 7 targets / 34 files" |
| 370 | echo "- Publication: none (Actions artifact \`codewhale-release-assets\` only)" |
| 371 | } >> "${GITHUB_STEP_SUMMARY}" |
| 372 |