| 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 | compiled_host_delivery: |
| 15 | description: Explicitly include qualified optional companions; Node stays default |
| 16 | required: false |
| 17 | default: false |
| 18 | type: boolean |
| 19 | compiled_host_run_id: |
| 20 | description: Green official CI run at this exact SHA containing Native image proof |
| 21 | required: false |
| 22 | default: '' |
| 23 | type: string |
| 24 | compiled_host_targets: |
| 25 | description: Comma-separated matching-native targets with qualified CI receipts |
| 26 | required: false |
| 27 | default: linux-x64,macos-arm64,windows-x64 |
| 28 | type: string |
| 29 | retention_days: |
| 30 | description: Retention for Actions-only intermediate and assembled artifacts |
| 31 | required: false |
| 32 | default: 7 |
| 33 | type: number |
| 34 | |
| 35 | permissions: |
| 36 | contents: read |
| 37 | actions: read |
| 38 | |
| 39 | env: |
| 40 | CARGO_TERM_COLOR: always |
| 41 | CARGO_INCREMENTAL: 0 |
| 42 | RUSTFLAGS: -Dwarnings |
| 43 | # Build identity is the trusted workflow SHA. Callers pass source_sha only |
| 44 | # so `pin` can refuse a mismatch; it must not retarget checkout or caches. |
| 45 | CODEWHALE_BUILD_SHA: ${{ github.sha }} |
| 46 | |
| 47 | jobs: |
| 48 | pin: |
| 49 | name: Pin caller SHA to this run |
| 50 | timeout-minutes: 10 |
| 51 | runs-on: ubuntu-latest |
| 52 | steps: |
| 53 | - name: Require source_sha equals github.sha |
| 54 | env: |
| 55 | SOURCE_SHA: ${{ inputs.source_sha }} |
| 56 | run: | |
| 57 | set -euo pipefail |
| 58 | if [[ "${#SOURCE_SHA}" -ne 40 || "${SOURCE_SHA}" =~ [^0-9a-fA-F] ]]; then |
| 59 | echo "::error::source_sha must be a full 40-character commit SHA." >&2 |
| 60 | exit 1 |
| 61 | fi |
| 62 | expected="$(printf '%s' "${SOURCE_SHA}" | tr '[:upper:]' '[:lower:]')" |
| 63 | actual="$(printf '%s' "${GITHUB_SHA}" | tr '[:upper:]' '[:lower:]')" |
| 64 | if [[ "${actual}" != "${expected}" ]]; then |
| 65 | echo "::error::Reusable workflow SHA ${actual} does not match source_sha ${SOURCE_SHA}." >&2 |
| 66 | exit 1 |
| 67 | fi |
| 68 | - name: Require explicit matching-native qualification selection |
| 69 | if: inputs.compiled_host_delivery |
| 70 | env: |
| 71 | QUALIFICATION_RUN: ${{ inputs.compiled_host_run_id }} |
| 72 | HOST_TARGETS: ${{ inputs.compiled_host_targets }} |
| 73 | run: | |
| 74 | set -euo pipefail |
| 75 | [[ "${QUALIFICATION_RUN}" =~ ^[0-9]+$ ]] || { echo 'Enabled host delivery needs an exact green CI run id' >&2; exit 1; } |
| 76 | [[ -n "${HOST_TARGETS}" ]] || { echo 'No compiled host targets selected' >&2; exit 1; } |
| 77 | declare -A seen |
| 78 | IFS=',' read -ra selected <<< "${HOST_TARGETS}" |
| 79 | for target in "${selected[@]}"; do |
| 80 | case "${target}" in linux-x64|linux-arm64|macos-x64|macos-arm64|windows-x64|windows-arm64) ;; *) echo "Unsupported compiled host target ${target}" >&2; exit 1 ;; esac |
| 81 | [[ -z "${seen[${target}]:-}" ]] || { echo "Duplicate compiled host target ${target}" >&2; exit 1; } |
| 82 | seen["${target}"]=1 |
| 83 | done |
| 84 | |
| 85 | build: |
| 86 | name: Build ${{ matrix.platform }} |
| 87 | timeout-minutes: 90 |
| 88 | # FreeBSD is a source-build target validated via `cargo check --target x86_64-unknown-freebsd -p codewhale-cli --locked` |
| 89 | # (see packaging/freebsd/README.md and docs/INSTALL.md#freebsd). The 7×1 prebuilt matrix stays 7 targets; |
| 90 | # FreeBSD has no prebuilt asset, no npm binary, and no matrix bloat — it builds from source. |
| 91 | strategy: |
| 92 | fail-fast: false |
| 93 | matrix: |
| 94 | include: |
| 95 | - os: ubuntu-latest |
| 96 | target: x86_64-unknown-linux-musl |
| 97 | platform: linux-x64 |
| 98 | cli_binary: codewhale |
| 99 | shim_binary: codew |
| 100 | cli_artifact: codewhale-linux-x64 |
| 101 | shim_artifact: codew-linux-x64 |
| 102 | compat_tui_artifact: codewhale-tui-linux-x64 |
| 103 | - os: ubuntu-24.04-arm |
| 104 | target: aarch64-unknown-linux-musl |
| 105 | platform: linux-arm64 |
| 106 | cli_binary: codewhale |
| 107 | shim_binary: codew |
| 108 | cli_artifact: codewhale-linux-arm64 |
| 109 | shim_artifact: codew-linux-arm64 |
| 110 | compat_tui_artifact: codewhale-tui-linux-arm64 |
| 111 | - os: ubuntu-latest |
| 112 | target: aarch64-linux-android |
| 113 | platform: android-arm64 |
| 114 | cli_binary: codewhale |
| 115 | shim_binary: codew |
| 116 | cli_artifact: codewhale-android-arm64 |
| 117 | shim_artifact: codew-android-arm64 |
| 118 | compat_tui_artifact: codewhale-tui-android-arm64 |
| 119 | - os: macos-latest |
| 120 | target: x86_64-apple-darwin |
| 121 | platform: macos-x64 |
| 122 | cli_binary: codewhale |
| 123 | shim_binary: codew |
| 124 | cli_artifact: codewhale-macos-x64 |
| 125 | shim_artifact: codew-macos-x64 |
| 126 | compat_tui_artifact: codewhale-tui-macos-x64 |
| 127 | - os: macos-latest |
| 128 | target: aarch64-apple-darwin |
| 129 | platform: macos-arm64 |
| 130 | cli_binary: codewhale |
| 131 | shim_binary: codew |
| 132 | cli_artifact: codewhale-macos-arm64 |
| 133 | shim_artifact: codew-macos-arm64 |
| 134 | compat_tui_artifact: codewhale-tui-macos-arm64 |
| 135 | - os: windows-latest |
| 136 | target: x86_64-pc-windows-msvc |
| 137 | platform: windows-x64 |
| 138 | cli_binary: codewhale.exe |
| 139 | shim_binary: codew.exe |
| 140 | cli_artifact: codewhale-windows-x64.exe |
| 141 | shim_artifact: codew-windows-x64.exe |
| 142 | compat_tui_artifact: codewhale-tui-windows-x64.exe |
| 143 | - os: windows-11-arm |
| 144 | target: aarch64-pc-windows-msvc |
| 145 | platform: windows-arm64 |
| 146 | cli_binary: codewhale.exe |
| 147 | shim_binary: codew.exe |
| 148 | cli_artifact: codewhale-windows-arm64.exe |
| 149 | shim_artifact: codew-windows-arm64.exe |
| 150 | compat_tui_artifact: codewhale-tui-windows-arm64.exe |
| 151 | runs-on: ${{ matrix.os }} |
| 152 | needs: pin |
| 153 | steps: |
| 154 | # No ref: — GITHUB_SHA only. CodeQL treats workflow_call checkout-with-ref |
| 155 | # and any ref named *sha* as an untrusted checkout (cache-poisoning). |
| 156 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 157 | - uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master 2026-07-18 |
| 158 | with: |
| 159 | toolchain: stable |
| 160 | targets: ${{ matrix.target }} |
| 161 | - uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 |
| 162 | id: sccache |
| 163 | continue-on-error: true |
| 164 | - name: Enable sccache |
| 165 | if: steps.sccache.outcome == 'success' |
| 166 | shell: bash |
| 167 | run: | |
| 168 | { |
| 169 | echo "SCCACHE_GHA_ENABLED=true" |
| 170 | echo "RUSTC_WRAPPER=sccache" |
| 171 | echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" |
| 172 | } >> "${GITHUB_ENV}" |
| 173 | # Restore after the trusted lockfile is on disk. Key is OS + arch + |
| 174 | # explicit stable toolchain + rust-cache's Cargo.lock / rust-toolchain |
| 175 | # hash. Never interpolate github.event, github.ref, github.sha, or inputs. |
| 176 | - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 |
| 177 | with: |
| 178 | cache-bin: false |
| 179 | prefix-key: v1-${{ runner.os }}-${{ runner.arch }}-stable |
| 180 | - name: Build static Linux binaries (musl) |
| 181 | if: endsWith(matrix.target, '-unknown-linux-musl') |
| 182 | shell: bash |
| 183 | run: | |
| 184 | sudo apt-get update |
| 185 | sudo apt-get install -y binutils musl-tools |
| 186 | rustup target add --toolchain stable ${{ matrix.target }} |
| 187 | cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli |
| 188 | - name: Configure Android NDK linker |
| 189 | if: matrix.target == 'aarch64-linux-android' && runner.os == 'Linux' |
| 190 | shell: bash |
| 191 | env: |
| 192 | ANDROID_NDK_VERSION: 27.2.12479018 |
| 193 | run: | |
| 194 | set -euo pipefail |
| 195 | sudo apt-get update |
| 196 | sudo apt-get install -y libclang-dev |
| 197 | ndk="${ANDROID_NDK_ROOT:-${ANDROID_NDK_HOME:-}}" |
| 198 | linker="" |
| 199 | if [[ -n "${ndk}" ]]; then |
| 200 | linker="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" |
| 201 | fi |
| 202 | if [[ -z "${linker}" || ! -x "${linker}" ]]; then |
| 203 | if ! command -v sdkmanager >/dev/null 2>&1; then |
| 204 | echo "sdkmanager is required to install Android NDK ${ANDROID_NDK_VERSION}" >&2 |
| 205 | exit 1 |
| 206 | fi |
| 207 | android_home="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" |
| 208 | if [[ -z "${android_home}" ]]; then |
| 209 | echo "ANDROID_HOME or ANDROID_SDK_ROOT is required to install Android NDK ${ANDROID_NDK_VERSION}" >&2 |
| 210 | exit 1 |
| 211 | fi |
| 212 | yes | sdkmanager --licenses >/dev/null || true |
| 213 | sdkmanager --install "ndk;${ANDROID_NDK_VERSION}" |
| 214 | ndk="${android_home}/ndk/${ANDROID_NDK_VERSION}" |
| 215 | linker="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" |
| 216 | fi |
| 217 | ar="${ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" |
| 218 | if [[ ! -x "${linker}" ]]; then |
| 219 | echo "Android linker not found: ${linker}" >&2 |
| 220 | exit 1 |
| 221 | fi |
| 222 | if [[ ! -x "${ar}" ]]; then |
| 223 | echo "Android archiver not found: ${ar}" >&2 |
| 224 | exit 1 |
| 225 | fi |
| 226 | { |
| 227 | echo "ANDROID_NDK_ROOT=${ndk}" |
| 228 | echo "ANDROID_NDK_HOME=${ndk}" |
| 229 | echo "CC_aarch64_linux_android=${linker}" |
| 230 | echo "AR_aarch64_linux_android=${ar}" |
| 231 | echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${linker}" |
| 232 | echo "BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android=--target=aarch64-linux-android24 --sysroot=${ndk}/toolchains/llvm/prebuilt/linux-x86_64/sysroot" |
| 233 | } >> "${GITHUB_ENV}" |
| 234 | - name: Build |
| 235 | if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') }} |
| 236 | shell: bash |
| 237 | run: cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli |
| 238 | - name: Materialize codew command alias |
| 239 | shell: bash |
| 240 | run: | |
| 241 | bin_dir="target/${{ matrix.target }}/dist" |
| 242 | cp "${bin_dir}/${{ matrix.cli_binary }}" "${bin_dir}/${{ matrix.shim_binary }}" |
| 243 | cmp "${bin_dir}/${{ matrix.cli_binary }}" "${bin_dir}/${{ matrix.shim_binary }}" |
| 244 | - name: Verify static Linux binaries and launch on matching native runners |
| 245 | if: >- |
| 246 | endsWith(matrix.target, '-unknown-linux-musl') && |
| 247 | ((startsWith(matrix.target, 'x86_64-') && runner.arch == 'X64') || |
| 248 | (startsWith(matrix.target, 'aarch64-') && runner.arch == 'ARM64')) |
| 249 | shell: bash |
| 250 | run: | |
| 251 | set -euo pipefail |
| 252 | bin_dir="target/${{ matrix.target }}/dist" |
| 253 | for binary in "${{ matrix.cli_binary }}" "${{ matrix.shim_binary }}"; do |
| 254 | bin_path="${bin_dir}/${binary}" |
| 255 | if readelf -l "${bin_path}" | grep -Fq 'INTERP'; then |
| 256 | echo "Expected a static musl binary, but ${bin_path} has an ELF interpreter" >&2 |
| 257 | exit 1 |
| 258 | fi |
| 259 | "${bin_path}" --version |
| 260 | done |
| 261 | - name: Smoke binaries on matching native runners |
| 262 | if: >- |
| 263 | matrix.target != 'aarch64-linux-android' && |
| 264 | ((startsWith(matrix.target, 'x86_64-') && runner.arch == 'X64') || |
| 265 | (startsWith(matrix.target, 'aarch64-') && runner.arch == 'ARM64')) |
| 266 | shell: bash |
| 267 | run: | |
| 268 | bin_dir="target/${{ matrix.target }}/dist" |
| 269 | "${bin_dir}/${{ matrix.cli_binary }}" --version |
| 270 | "${bin_dir}/${{ matrix.shim_binary }}" --version |
| 271 | - name: Stage binaries |
| 272 | shell: bash |
| 273 | run: | |
| 274 | stage_binary() { |
| 275 | local binary="$1" |
| 276 | local artifact="$2" |
| 277 | local bin_path="target/${{ matrix.target }}/dist/${binary}" |
| 278 | if [[ ! -f "${bin_path}" ]]; then |
| 279 | echo "Binary not at ${bin_path}; searching target/ for ${binary}:" >&2 |
| 280 | find target -name "${binary}" -type f |
| 281 | exit 1 |
| 282 | fi |
| 283 | cp "${bin_path}" "${artifact}" |
| 284 | } |
| 285 | |
| 286 | stage_binary "${{ matrix.cli_binary }}" "${{ matrix.cli_artifact }}" |
| 287 | stage_binary "${{ matrix.shim_binary }}" "${{ matrix.shim_artifact }}" |
| 288 | # Compatibility bridge for v0.9.4's hard-coded release |
| 289 | # completeness/updater contract. This is the same runtime, not a |
| 290 | # separately compiled or installed TUI command. |
| 291 | stage_binary "${{ matrix.cli_binary }}" "${{ matrix.compat_tui_artifact }}" |
| 292 | - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2 |
| 293 | if: inputs.compiled_host_delivery && contains(format(',{0},', inputs.compiled_host_targets), format(',{0},', matrix.platform)) |
| 294 | with: |
| 295 | bun-version: 1.4.0 |
| 296 | - name: Stage exact qualified optional host image |
| 297 | if: inputs.compiled_host_delivery && contains(format(',{0},', inputs.compiled_host_targets), format(',{0},', matrix.platform)) |
| 298 | shell: bash |
| 299 | env: |
| 300 | GH_TOKEN: ${{ github.token }} |
| 301 | QUALIFICATION_RUN: ${{ inputs.compiled_host_run_id }} |
| 302 | TARGET_PLATFORM: ${{ matrix.platform }} |
| 303 | RELEASE_VERSION: ${{ inputs.version }} |
| 304 | RUNTIME_CLOSURE_DIR: ${{ vars.CODEWHALE_COMPILED_HOST_CLOSURE_DIR }} |
| 305 | run: | |
| 306 | set -euo pipefail |
| 307 | proof_dir="${RUNNER_TEMP}/compiled-host-proof-${TARGET_PLATFORM}" |
| 308 | node scripts/release/fetch-compiled-host-proof.js --repo "${GITHUB_REPOSITORY}" --run-id "${QUALIFICATION_RUN}" --source-sha "${GITHUB_SHA}" --target "${TARGET_PLATFORM}" --output "${proof_dir}" |
| 309 | test -n "${RUNTIME_CLOSURE_DIR}" || { echo 'Enabled compiled-host delivery requires reviewed complete local runtime notices/relink-source closure' >&2; exit 1; } |
| 310 | bun_binary="$(bun -p 'process.execPath')" |
| 311 | extension="" |
| 312 | if [[ "${TARGET_PLATFORM}" == windows-* ]]; then extension=.exe; fi |
| 313 | node scripts/release/stage-compiled-host.mjs --bun "${bun_binary}" --compiled-image "${proof_dir}/codewhale-extension-host${extension}" --native-receipt "${proof_dir}/native-receipt.json" --runtime-closure "${RUNTIME_CLOSURE_DIR}/${TARGET_PLATFORM}.json" --target "${TARGET_PLATFORM}" --version "${RELEASE_VERSION}" --source-sha "${GITHUB_SHA}" --output-dir "compiled-host-${TARGET_PLATFORM}" |
| 314 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 315 | if: inputs.compiled_host_delivery && contains(format(',{0},', inputs.compiled_host_targets), format(',{0},', matrix.platform)) |
| 316 | with: |
| 317 | name: codewhale-compiled-host-${{ matrix.platform }} |
| 318 | path: | |
| 319 | compiled-host-${{ matrix.platform }}/codewhale-extension-host-* |
| 320 | compiled-host-${{ matrix.platform }}/codewhale-extension-hosts.json |
| 321 | if-no-files-found: error |
| 322 | retention-days: ${{ inputs.retention_days }} |
| 323 | overwrite: true |
| 324 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 325 | with: |
| 326 | name: ${{ matrix.cli_artifact }} |
| 327 | path: ${{ matrix.cli_artifact }} |
| 328 | if-no-files-found: error |
| 329 | retention-days: ${{ inputs.retention_days }} |
| 330 | overwrite: true |
| 331 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 332 | with: |
| 333 | name: ${{ matrix.shim_artifact }} |
| 334 | path: ${{ matrix.shim_artifact }} |
| 335 | if-no-files-found: error |
| 336 | retention-days: ${{ inputs.retention_days }} |
| 337 | overwrite: true |
| 338 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 339 | with: |
| 340 | name: ${{ matrix.compat_tui_artifact }} |
| 341 | path: ${{ matrix.compat_tui_artifact }} |
| 342 | if-no-files-found: error |
| 343 | retention-days: ${{ inputs.retention_days }} |
| 344 | overwrite: true |
| 345 | |
| 346 | bundle: |
| 347 | timeout-minutes: 15 |
| 348 | needs: build |
| 349 | if: ${{ !cancelled() && needs.build.result == 'success' }} |
| 350 | runs-on: ubuntu-latest |
| 351 | steps: |
| 352 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 353 | - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 354 | with: |
| 355 | path: artifacts |
| 356 | pattern: '*' |
| 357 | - name: Collect only qualified requested compiled hosts |
| 358 | if: inputs.compiled_host_delivery |
| 359 | shell: bash |
| 360 | run: node scripts/release/stage-compiled-host.mjs --collect artifacts --output-dir artifacts/codewhale-compiled-hosts |
| 361 | - name: Create and checksum platform archives |
| 362 | shell: bash |
| 363 | env: |
| 364 | SOURCE_SHA: ${{ github.sha }} |
| 365 | run: | |
| 366 | set -euo pipefail |
| 367 | source_date_epoch="$(git show -s --format=%ct "${SOURCE_SHA}")" |
| 368 | if [[ ! "${source_date_epoch}" =~ ^[0-9]+$ ]]; then |
| 369 | echo "Could not read a Unix timestamp for source commit ${SOURCE_SHA}" >&2 |
| 370 | exit 1 |
| 371 | fi |
| 372 | SOURCE_DATE_EPOCH="${source_date_epoch}" \ |
| 373 | bash scripts/release/create-release-bundles.sh artifacts bundles |
| 374 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 375 | with: |
| 376 | name: codewhale-bundles |
| 377 | path: | |
| 378 | bundles/*.tar.gz |
| 379 | bundles/*.zip |
| 380 | bundles/codewhale-bundles-sha256.txt |
| 381 | if-no-files-found: error |
| 382 | retention-days: ${{ inputs.retention_days }} |
| 383 | overwrite: true |
| 384 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 385 | if: inputs.compiled_host_delivery |
| 386 | with: |
| 387 | name: codewhale-compiled-hosts |
| 388 | path: artifacts/codewhale-compiled-hosts/* |
| 389 | if-no-files-found: error |
| 390 | retention-days: ${{ inputs.retention_days }} |
| 391 | overwrite: true |
| 392 | |
| 393 | windows-installer: |
| 394 | timeout-minutes: 15 |
| 395 | needs: build |
| 396 | if: ${{ !cancelled() && needs.build.result == 'success' }} |
| 397 | runs-on: windows-latest |
| 398 | steps: |
| 399 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 400 | - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 401 | with: |
| 402 | path: artifacts |
| 403 | pattern: '*windows-x64*' |
| 404 | - name: Install NSIS |
| 405 | shell: pwsh |
| 406 | run: choco install nsis -y --no-progress |
| 407 | - name: Build NSIS installer |
| 408 | shell: pwsh |
| 409 | run: | |
| 410 | $ErrorActionPreference = "Stop" |
| 411 | Copy-Item "artifacts\codewhale-windows-x64.exe\codewhale-windows-x64.exe" "scripts\installer\codewhale.exe" |
| 412 | Copy-Item "artifacts\codew-windows-x64.exe\codew-windows-x64.exe" "scripts\installer\codew.exe" |
| 413 | $makensis = "${env:ProgramFiles(x86)}\NSIS\makensis.exe" |
| 414 | if (!(Test-Path $makensis)) { |
| 415 | $makensis = "${env:ProgramFiles}\NSIS\makensis.exe" |
| 416 | } |
| 417 | if (!(Test-Path $makensis)) { |
| 418 | throw "makensis.exe not found after NSIS install" |
| 419 | } |
| 420 | $hostDefine = @() |
| 421 | $hostCatalog = "artifacts\codewhale-compiled-host-windows-x64\codewhale-extension-hosts.json" |
| 422 | if (Test-Path $hostCatalog) { |
| 423 | node -e "const h=require('./npm/codewhale/scripts/compiled-hosts'); const fs=require('fs'); const p=require('path'); const f=process.argv[1]; h.verifyDirectory(p.dirname(f), h.parseCatalog(fs.readFileSync(f),'${{ inputs.version }}'));" $hostCatalog |
| 424 | if ($LASTEXITCODE -ne 0) { throw 'Compiled host payload/catalog verification failed' } |
| 425 | $catalog = Get-Content -Raw $hostCatalog | ConvertFrom-Json |
| 426 | $hostEntry = @($catalog.hosts | Where-Object { $_.target -eq 'windows-x64' }) |
| 427 | if ($hostEntry.Count -ne 1) { throw 'No unique qualified Windows host' } |
| 428 | $hostDir = Split-Path $hostCatalog |
| 429 | Copy-Item "$hostDir\$($hostEntry[0].asset)" "scripts\installer\codewhale-extension-host.exe" |
| 430 | Copy-Item "$hostDir\$($hostEntry[0].notices_asset)" "scripts\installer\codewhale-extension-host.LICENSES.txt" |
| 431 | Copy-Item "$hostDir\$($hostEntry[0].source_asset)" "scripts\installer\codewhale-extension-host.relink-source.tar.gz" |
| 432 | Copy-Item $hostCatalog "scripts\installer\codewhale-extension-host.release.json" |
| 433 | $hostDefine = @('/DCOMPILED_HOST=1') |
| 434 | } |
| 435 | Push-Location scripts\installer |
| 436 | & $makensis "/DVERSION=${{ inputs.version }}" @hostDefine "codewhale.nsi" |
| 437 | Pop-Location |
| 438 | if (!(Test-Path "scripts\installer\CodeWhaleSetup.exe")) { |
| 439 | throw "CodeWhaleSetup.exe was not produced" |
| 440 | } |
| 441 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 442 | with: |
| 443 | name: CodeWhaleSetup.exe |
| 444 | path: scripts/installer/CodeWhaleSetup.exe |
| 445 | if-no-files-found: error |
| 446 | retention-days: ${{ inputs.retention_days }} |
| 447 | overwrite: true |
| 448 | |
| 449 | assemble: |
| 450 | timeout-minutes: 15 |
| 451 | needs: [bundle, windows-installer] |
| 452 | if: ${{ !cancelled() && needs.bundle.result == 'success' && needs.windows-installer.result == 'success' }} |
| 453 | runs-on: ubuntu-latest |
| 454 | steps: |
| 455 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 456 | - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 |
| 457 | with: |
| 458 | node-version: 22 |
| 459 | package-manager-cache: false |
| 460 | - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 461 | with: |
| 462 | path: intermediate-artifacts |
| 463 | pattern: '*' |
| 464 | - name: Assemble exact authoritative release inventory |
| 465 | run: node scripts/release/assemble-release-assets.js intermediate-artifacts release-assets |
| 466 | - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 467 | with: |
| 468 | name: codewhale-release-assets |
| 469 | path: release-assets/* |
| 470 | if-no-files-found: error |
| 471 | retention-days: ${{ inputs.retention_days }} |
| 472 | compression-level: 0 |
| 473 | overwrite: true |
| 474 | |
| 475 | smoke: |
| 476 | timeout-minutes: 15 |
| 477 | needs: assemble |
| 478 | if: ${{ !cancelled() && needs.assemble.result == 'success' }} |
| 479 | runs-on: ubuntu-latest |
| 480 | steps: |
| 481 | - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
| 482 | - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 |
| 483 | with: |
| 484 | node-version: 22 |
| 485 | package-manager-cache: false |
| 486 | - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 487 | with: |
| 488 | name: codewhale-release-assets |
| 489 | path: release-assets |
| 490 | - name: Verify 34-asset bridge inventory and checksum manifests (single binary) |
| 491 | run: node scripts/release/assemble-release-assets.js --verify release-assets |
| 492 | - name: Test release inventory contracts |
| 493 | run: | |
| 494 | node --test scripts/release/assemble-release-assets.test.js |
| 495 | node --test npm/codewhale/test/artifacts.test.js npm/codewhale/test/release-assets.test.js |
| 496 | - name: Render AUR metadata from candidate Linux archives |
| 497 | run: bash packaging/aur/render.sh release-assets "${RUNNER_TEMP}/codewhale-bin" |
| 498 | - name: Smoke packed npm wrapper against candidate assets |
| 499 | env: |
| 500 | CODEWHALE_SMOKE_ASSETS_DIR: ${{ github.workspace }}/release-assets |
| 501 | run: node scripts/release/npm-wrapper-smoke.js |
| 502 | - name: Record non-public candidate identity |
| 503 | shell: bash |
| 504 | run: | |
| 505 | { |
| 506 | echo "### Release artifact candidate" |
| 507 | echo "" |
| 508 | echo "- Source: \`${{ github.sha }}\`" |
| 509 | echo "- Version metadata: \`${{ inputs.version }}\`" |
| 510 | echo "- Inventory: 7 targets / 34 files (single binary; 7 legacy alias assets)" |
| 511 | echo "- Publication: none (Actions artifact \`codewhale-release-assets\` only)" |
| 512 | } >> "${GITHUB_STEP_SUMMARY}" |
| 513 |