authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-13 21:24:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-13 21:24:23-07:00
log2c54f2ff17b3ae87bdbe4e072e38bd6f3045eb5b
tree6c2093ba73f50a9661c15790e172cf5364db9596
parent07549200dcdc70dce57285276879004024beeaa7
signature Signed by PGP key B5690EEEBB952194

ci(release): build CLI archives for five targets, each on its own architecture (#11605)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

4 files changed, 225 insertions(+), 28 deletions(-)

.github/workflows/release.yml+207-18
......@@ -359,13 +359,25 @@ jobs:
359359 # the Windows desktop entry embeds it as the WSL runtime. Building it once
360360 # here means the WSL backend runs the exact bytes a Linux user downloads.
361361 build_linux_cli:
362 name: Build CLI archive (linux-x64)
362 name: Build CLI archive (linux-${{ matrix.arch }})
363363 # Same gating as relay_public_config: only the release commit is needed, so
364364 # this runs alongside preflight. See the condition comment there.
365365 needs: [resolve_commit, preflight, relay_public_config]
366366 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
367 runs-on: blacksmith-32vcpu-ubuntu-2404
368 timeout-minutes: 20
367 runs-on: ${{ matrix.runner }}
368 timeout-minutes: 30
369 strategy:
370 fail-fast: false
371 matrix:
372 include:
373 - arch: x64
374 runner: blacksmith-32vcpu-ubuntu-2404
375 rust_target: x86_64-unknown-linux-gnu
376 # node-pty has no Linux prebuild and compiles from source, so the
377 # arm64 archive is built on arm64 hardware rather than cross-built.
378 - arch: arm64
379 runner: ubuntu-24.04-arm
380 rust_target: aarch64-unknown-linux-gnu
369381 env:
370382 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
371383 T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
......@@ -396,18 +408,18 @@ jobs:
396408 id: resource_monitor_cache
397409 uses: actions/cache@v6
398410 with:
399 path: native/resource-monitor/target/x86_64-unknown-linux-gnu/release/t3-resource-monitor
400 key: resource-monitor-x86_64-unknown-linux-gnu-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }}
411 path: native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor
412 key: resource-monitor-${{ matrix.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }}
401413
402414 - name: Setup Rust
403415 if: steps.resource_monitor_cache.outputs.cache-hit != 'true'
404416 uses: dtolnay/rust-toolchain@stable
405417 with:
406 targets: x86_64-unknown-linux-gnu
418 targets: ${{ matrix.rust_target }}
407419
408420 - name: Build resource monitor
409421 if: steps.resource_monitor_cache.outputs.cache-hit != 'true'
410 run: cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml --target x86_64-unknown-linux-gnu
422 run: cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml --target ${{ matrix.rust_target }}
411423
412424 - name: Download relay client tracing config
413425 uses: actions/download-artifact@v8
......@@ -442,15 +454,15 @@ jobs:
442454 - name: Stage resource monitor for the CLI archive
443455 run: |
444456 set -euo pipefail
445 target_dir="$RUNNER_TEMP/cli-resource-monitor/linux-x64"
457 target_dir="$RUNNER_TEMP/cli-resource-monitor/linux-${{ matrix.arch }}"
446458 mkdir -p "$target_dir"
447 cp native/resource-monitor/target/x86_64-unknown-linux-gnu/release/t3-resource-monitor "$target_dir/"
459 cp native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor "$target_dir/"
448460
449461 - name: Build CLI archive
450462 run: |
451463 node scripts/build-cli-archive.ts \
452464 --platform linux \
453 --arch x64 \
465 --arch ${{ matrix.arch }} \
454466 --version "${{ needs.preflight.outputs.version }}" \
455467 --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \
456468 --output-dir release-cli
......@@ -461,7 +473,183 @@ jobs:
461473 - name: Upload CLI archive
462474 uses: actions/upload-artifact@v7
463475 with:
464 name: cli-linux-x64
476 name: cli-linux-${{ matrix.arch }}
477 path: release-cli/*
478 if-no-files-found: error
479
480 # Windows arm64 has no desktop build yet (the NSIS arm64 row is still off),
481 # but the CLI archive is built here on arm64 hardware so it is signed and
482 # smoke-tested on the architecture it targets, like every other archive.
483 build_windows_arm64_cli:
484 name: Build CLI archive (win32-arm64)
485 needs: [resolve_commit, preflight, relay_public_config]
486 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
487 runs-on: windows-11-arm
488 timeout-minutes: 30
489 env:
490 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
491 T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
492 T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
493 T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
494 steps:
495 - name: Checkout
496 uses: actions/checkout@v6
497 with:
498 ref: ${{ needs.preflight.outputs.ref }}
499 sparse-checkout: |
500 /*
501 !/.repos/
502 sparse-checkout-cone-mode: false
503
504 - name: Setup Vite+
505 uses: voidzero-dev/setup-vp@v1
506 with:
507 node-version-file: package.json
508 cache: false
509 run-install: false
510
511 - name: Download dependency verification
512 continue-on-error: true
513 uses: actions/download-artifact@v8
514 with:
515 name: release-dependency-verification
516 path: ${{ runner.temp }}/pnpm-metadata
517
518 - name: Install dependencies
519 env:
520 pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
521 run: vp install --filter=t3... --filter=@t3tools/web... --filter=@t3tools/scripts...
522
523 - name: Setup Rust
524 uses: dtolnay/rust-toolchain@stable
525 with:
526 targets: aarch64-pc-windows-msvc
527
528 - name: Build resource monitor
529 run: cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml --target aarch64-pc-windows-msvc
530
531 - name: Download relay client tracing config
532 uses: actions/download-artifact@v8
533 with:
534 name: relay-client-tracing-config
535 path: ${{ runner.temp }}/relay-client-tracing
536
537 - name: Load relay client tracing config
538 shell: bash
539 run: |
540 config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env"
541 tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")"
542 echo "::add-mask::$tracing_token"
543 cat "$config_path" >> "$GITHUB_ENV"
544
545 - name: Align package versions to release version
546 run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
547
548 # The t3 build task depends on @t3tools/web#build, so the web client is
549 # built as part of this step.
550 - name: Build CLI package
551 run: vp run --filter t3 build
552
553 - name: Build CLI single-executable
554 shell: bash
555 env:
556 VP_NODE_VERSION: "26.8.2"
557 run: node apps/server/scripts/cli.ts build-exe --verbose
558
559 - name: Prepare Azure Trusted Signing
560 shell: pwsh
561 env:
562 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
563 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
564 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
565 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
566 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
567 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
568 AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }}
569 run: |
570 $ErrorActionPreference = "Stop"
571
572 $requiredSecrets = @(
573 $env:AZURE_TENANT_ID,
574 $env:AZURE_CLIENT_ID,
575 $env:AZURE_CLIENT_SECRET,
576 $env:AZURE_TRUSTED_SIGNING_ENDPOINT,
577 $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME,
578 $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME,
579 $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME
580 )
581 if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) {
582 Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation."
583 exit 0
584 }
585
586 try {
587 Install-PackageProvider `
588 -Name NuGet `
589 -MinimumVersion 2.8.5.201 `
590 -Force `
591 -Scope CurrentUser `
592 -ErrorAction Stop
593 } catch {
594 Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)"
595 }
596
597 Install-Module `
598 -Name TrustedSigning `
599 -MinimumVersion 0.5.0 `
600 -Force `
601 -AllowClobber `
602 -Repository PSGallery `
603 -Scope CurrentUser `
604 -ErrorAction Stop
605
606 Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force
607 Get-Command Invoke-TrustedSigning -ErrorAction Stop
608
609 $moduleRoots = @(
610 [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"),
611 [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"),
612 [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"),
613 [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules")
614 )
615 $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) |
616 Where-Object { $_ -and (Test-Path $_) } |
617 Select-Object -Unique
618 "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV
619
620 - name: Stage resource monitor for the CLI archive
621 shell: bash
622 run: |
623 set -euo pipefail
624 target_dir="$RUNNER_TEMP/cli-resource-monitor/win32-arm64"
625 mkdir -p "$target_dir"
626 cp native/resource-monitor/target/aarch64-pc-windows-msvc/release/t3-resource-monitor.exe "$target_dir/"
627
628 - name: Build CLI archive
629 shell: bash
630 env:
631 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
632 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
633 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
634 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
635 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
636 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
637 run: |
638 node scripts/build-cli-archive.ts \
639 --platform win \
640 --arch arm64 \
641 --version "${{ needs.preflight.outputs.version }}" \
642 --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \
643 --output-dir release-cli
644
645 - name: Smoke-test CLI archive
646 shell: bash
647 run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ needs.preflight.outputs.version }}"
648
649 - name: Upload CLI archive
650 uses: actions/upload-artifact@v7
651 with:
652 name: cli-win-arm64
465653 path: release-cli/*
466654 if-no-files-found: error
467655
......@@ -488,11 +676,9 @@ jobs:
488676 matrix:
489677 include:
490678 # cli_archive: whether the job also builds the self-contained CLI
491 # archive. The executable is built on the runner's own Node, so only
492 # native runners qualify. macOS x64 has no native runner: a
493 # cross-built executable crashed under Rosetta in the smoke test and
494 # cannot be verified on real x64 hardware in CI, so it is skipped
495 # until it can be.
679 # archive for its own platform/arch, on this runner, and smoke-tests
680 # it here. Every archive is built on hardware of its own
681 # architecture: Linux and Windows arm64 have their own jobs below.
496682 - label: macOS arm64
497683 runner: blacksmith-12vcpu-macos-26
498684 platform: mac
......@@ -501,6 +687,9 @@ jobs:
501687 rust_target: aarch64-apple-darwin
502688 resource_key: darwin-arm64
503689 cli_archive: true
690 # No CLI archive: Node single-executables are unsupported on x64
691 # macOS (the SEA docs list macOS as arm64 only) and the built binary
692 # segfaults on start. The x64 desktop app is Electron and unaffected.
504693 - label: macOS x64
505694 runner: blacksmith-12vcpu-macos-26
506695 platform: mac
......@@ -1058,8 +1247,8 @@ jobs:
10581247
10591248 release:
10601249 name: Publish GitHub Release
1061 needs: [preflight, build, publish_cli]
1062 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' && (needs.publish_cli.result == 'success' || (needs.preflight.outputs.release_channel == 'preview' && needs.publish_cli.result == 'skipped')) }}
1250 needs: [preflight, build, build_windows_arm64_cli, publish_cli]
1251 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' && needs.build_windows_arm64_cli.result == 'success' && (needs.publish_cli.result == 'success' || (needs.preflight.outputs.release_channel == 'preview' && needs.publish_cli.result == 'skipped')) }}
10631252 runs-on: blacksmith-8vcpu-ubuntu-2404
10641253 timeout-minutes: 30
10651254 permissions:
docs/operations/release.md+1-1
......@@ -34,7 +34,7 @@ This document covers the unified release workflow for stable and nightly desktop
3434 - Nightly runs are always GitHub prereleases and never marked latest.
3535 - Automatically generated release notes are pinned to the previous tag in the same channel, so stable compares to the previous stable tag and nightly compares to the previous nightly tag.
3636- Includes Electron auto-update metadata (for example `latest*.yml`, `nightly*.yml`, and `*.blockmap`) in release assets.
37- Builds a self-contained CLI archive per platform (`t3-<version>-<platform>-<arch>.tar.gz`, `.zip` on Windows) on the same runners as the desktop artifacts and attaches them to the GitHub Release with a `SHA256SUMS` file, on every channel. Only native runners build one (macOS arm64, Linux x64, Windows x64); macOS x64 is skipped because a cross-built executable cannot be verified on real x64 hardware in CI.
37- Builds a self-contained CLI archive per platform (`t3-<version>-<platform>-<arch>.tar.gz`, `.zip` on Windows) on the same runners as the desktop artifacts and attaches them to the GitHub Release with a `SHA256SUMS` file, on every channel, for five targets: macOS arm64, Linux x64 and arm64, Windows x64 and arm64. Every archive is built, signed, and smoke-tested on hardware of its own architecture (`build_linux_cli` and `build_windows_arm64_cli` have their own runners). There is no macOS x64 archive: Node single-executables are unsupported on x64 macOS (the SEA docs list macOS as arm64 only) and the binary segfaults on start; the x64 desktop app is Electron and unaffected.
3838 - The archive holds the server as a Node single-executable (`scripts/build-cli-archive.ts`), so unpacking it needs neither Node, npm, nor a compiler. It is the only form in which T3 Code manages a runtime: the desktop's SSH environments, the boot service, `t3 update`, and the install scripts all download and verify this archive against `SHA256SUMS`. The npm package exists for people who run `npx t3` or `npm install -g t3` themselves; nothing in the product installs from npm. The `curl | sh` installers are `scripts/install.sh` and `scripts/install.ps1`; the marketing site copies them into its `public/` at build time (`apps/marketing/scripts/stage-install-scripts.mjs`) and serves them at `t3.codes/install.sh` and `/install.ps1`.
3939 - The executable is built with a Node that supports `--build-sea` (`VP_NODE_VERSION=26.8.2`, kept in step with `SEA_NODE_VERSION` in `apps/server/vite.config.ts`), while the repo stays on `engines.node`.
4040 - macOS archives are signed with the Developer ID certificate and notarized when the Apple secrets are present (ad hoc otherwise, which still runs from `curl`/`tar` installs). Windows executables use the same Azure Trusted Signing setup as the installer. Every native addon in the macOS archive is signed too, since the hardened runtime refuses unsigned libraries.
packages/shared/src/cliRelease.test.ts+3-4
......@@ -23,11 +23,10 @@ describe("cliRelease", () => {
2323 expect(cliArchivePlatformKey("darwin", "arm64")).toBe("darwin-arm64");
2424 expect(cliArchivePlatformKey("linux", "x64")).toBe("linux-x64");
2525 expect(cliArchivePlatformKey("win32", "x64")).toBe("win32-x64");
26 // Built but not published (macOS x64 segfaults under Rosetta when
27 // cross-injected; the arm64 Linux and Windows runners do not exist yet).
26 // Node single-executables are unsupported on x64 macOS.
2827 expect(cliArchivePlatformKey("darwin", "x64")).toBeUndefined();
29 expect(cliArchivePlatformKey("linux", "arm64")).toBeUndefined();
30 expect(cliArchivePlatformKey("win32", "arm64")).toBeUndefined();
28 expect(cliArchivePlatformKey("linux", "arm64")).toBe("linux-arm64");
29 expect(cliArchivePlatformKey("win32", "arm64")).toBe("win32-arm64");
3130 expect(cliArchivePlatformKey("freebsd", "x64")).toBeUndefined();
3231 expect(cliArchivePlatformKey("linux", "ia32")).toBeUndefined();
3332 });
packages/shared/src/cliRelease.ts+14-5
......@@ -11,12 +11,21 @@ export const CLI_RELEASE_CHECKSUMS_FILE = "SHA256SUMS";
1111export const CLI_RELEASE_BASE_URL_ENV = "T3CODE_RELEASE_BASE_URL";
1212
1313/**
14 * The archives a release actually attaches. Kept in step with the
15 * `cli_archive` matrix flags in .github/workflows/release.yml: a key here
16 * without a build there produces download URLs that 404, and a build there
17 * without a key here is unreachable from every installer.
14 * The archives a release attaches. Kept in step with the build_linux_cli
15 * matrix, build_windows_arm64_cli, and the `cli_archive` rows in
16 * .github/workflows/release.yml: a key here without a build there produces
17 * download URLs that 404, and a build there without a key here is
18 * unreachable from every installer.
1819 */
19const CLI_ARCHIVE_PLATFORM_KEYS = ["darwin-arm64", "linux-x64", "win32-x64"] as const;
20// No darwin-x64: Node single-executables are unsupported on x64 macOS (the
21// SEA docs list macOS as arm64 only) and the binary segfaults on start.
22const CLI_ARCHIVE_PLATFORM_KEYS = [
23 "darwin-arm64",
24 "linux-arm64",
25 "linux-x64",
26 "win32-arm64",
27 "win32-x64",
28] as const;
2029export type CliArchivePlatformKey = (typeof CLI_ARCHIVE_PLATFORM_KEYS)[number];
2130
2231export function cliArchivePlatformKey(