diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml new file mode 100644 index 000000000000..5d790e49a061 --- /dev/null +++ b/.github/workflows/release-desktop.yml @@ -0,0 +1,528 @@ +name: Release desktop build + +# One desktop platform/arch build, called once per target from release.yml so +# each target is its own job with its own `needs`. The JS bundle (server, web +# client, Electron main) comes from the `js-bundle` artifact that build_bundle +# produced; this job only packages it, builds the native helpers, and, where +# `cli_archive` is set, the self-contained CLI archive for its platform. + +on: + workflow_call: + inputs: + label: + required: true + type: string + runner: + required: true + type: string + platform: + required: true + type: string + target: + required: true + type: string + arch: + required: true + type: string + rust_target: + required: true + type: string + resource_key: + required: true + type: string + # Whether the job also builds the self-contained CLI archive for its own + # platform/arch, on this runner, and smoke-tests it here. Every archive + # is built on hardware of its own architecture. + cli_archive: + required: false + default: false + type: boolean + version: + required: true + type: string + ref: + required: true + type: string + release_channel: + required: true + type: string + clerk_publishable_key: + required: true + type: string + clerk_jwt_template: + required: true + type: string + clerk_cli_oauth_client_id: + required: true + type: string + relay_url: + required: true + type: string + +permissions: + contents: read + +jobs: + build: + name: Build ${{ inputs.label }} + runs-on: ${{ inputs.runner }} + timeout-minutes: 30 + env: + T3CODE_CLERK_PUBLISHABLE_KEY: ${{ inputs.clerk_publishable_key }} + T3CODE_CLERK_JWT_TEMPLATE: ${{ inputs.clerk_jwt_template }} + T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ inputs.clerk_cli_oauth_client_id }} + T3CODE_RELAY_URL: ${{ inputs.relay_url }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1 + with: + node-version-file: package.json + cache: ${{ inputs.platform != 'win' }} + run-install: false + + - name: Resolve Windows package cache path + if: inputs.platform == 'win' + id: package_cache_path + shell: pwsh + run: '"path=$(vp pm cache dir)" >> $env:GITHUB_OUTPUT' + + - name: Cache Windows packages + if: inputs.platform == 'win' + uses: actions/cache@v6 + with: + path: ${{ steps.package_cache_path.outputs.path }} + key: windows-release-packages-v1-${{ inputs.arch }}-${{ hashFiles('pnpm-lock.yaml') }} + + # pnpm checks the lockfile and policy before reusing this result. A missing + # artifact leaves the cache empty, so installation runs the checks again. + - name: Download dependency verification + continue-on-error: true + uses: actions/download-artifact@v8 + with: + name: release-dependency-verification + path: ${{ runner.temp }}/pnpm-metadata + + - name: Install desktop dependencies + env: + pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata + run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts... + + - name: Cache resource monitor + id: resource_monitor_cache + uses: actions/cache@v6 + with: + path: native/resource-monitor/target/${{ inputs.rust_target }}/release/t3-resource-monitor${{ inputs.platform == 'win' && '.exe' || '' }} + key: resource-monitor-${{ inputs.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }} + + - name: Cache Linux capture helpers + if: inputs.platform == 'linux' + id: capture_helper_cache + uses: actions/cache@v6 + with: + path: | + native/kde-snap-shot/target/${{ inputs.rust_target }}/release/t3-kde-snap-shot + native/hyprland-snap-shot/target/${{ inputs.rust_target }}/release/t3-hyprland-snap-shot + key: linux-capture-helpers-${{ inputs.rust_target }}-${{ hashFiles('native/kde-snap-shot/Cargo.lock', 'native/kde-snap-shot/Cargo.toml', 'native/kde-snap-shot/src/**', 'native/hyprland-snap-shot/Cargo.lock', 'native/hyprland-snap-shot/Cargo.toml', 'native/hyprland-snap-shot/src/**', 'native/hyprland-snap-shot/protocols/**') }} + + - name: Setup Rust + if: steps.resource_monitor_cache.outputs.cache-hit != 'true' || (inputs.platform == 'linux' && steps.capture_helper_cache.outputs.cache-hit != 'true') + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ inputs.rust_target }} + + - name: Download relay client tracing config + uses: actions/download-artifact@v8 + with: + name: relay-client-tracing-config + path: ${{ runner.temp }}/relay-client-tracing + + - name: Load relay client tracing config + shell: bash + run: | + config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" + tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" + echo "::add-mask::$tracing_token" + cat "$config_path" >> "$GITHUB_ENV" + + - name: Align package versions to release version + run: node scripts/update-release-package-versions.ts "${{ inputs.version }}" + + # The artifact root is `apps/` (upload-artifact keeps the least common + # ancestor of its paths), so extracting into `apps` restores + # apps/server/dist and apps/desktop/dist-electron at their build paths. + - name: Download JS bundle + uses: actions/download-artifact@v8 + with: + name: js-bundle + path: apps + + # The WSL backend runs the Linux CLI archive inside the distro, so the + # Windows desktop embeds the same-arch archive the release attaches. + - name: Download Linux CLI archive for WSL + if: inputs.platform == 'win' + uses: actions/download-artifact@v8 + with: + name: cli-linux-${{ inputs.arch }} + path: wsl-runtime + + - name: Install Spectre-mitigated MSVC libs + if: inputs.platform == 'win' + shell: pwsh + run: | + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $installPath = & $vswhere -products * -latest -property installationPath + $setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe" + $proc = Start-Process -FilePath $setupExe ` + -ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", ` + "Microsoft.VisualStudio.Component.VC.Runtimes.${{ inputs.arch == 'arm64' && 'ARM64' || 'x86.x64' }}.Spectre", "--quiet", "--norestart" ` + -Wait -PassThru -NoNewWindow + if ($null -eq $proc -or $proc.ExitCode -ne 0) { + $code = if ($null -ne $proc) { $proc.ExitCode } else { 1 } + Write-Error "Visual Studio Installer failed with exit code $code" + exit $code + } + + - uses: ./.github/actions/setup-apt-mirrors + if: inputs.platform == 'linux' + + - name: Install Linux desktop build libraries + if: inputs.platform == 'linux' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y libsecret-1-dev pkg-config + if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then + sudo apt-get install -y imagemagick + fi + + if command -v magick >/dev/null 2>&1; then + magick -version + else + convert -version + fi + + - name: Prepare Azure Trusted Signing + if: inputs.platform == 'win' + shell: pwsh + env: + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} + run: | + $ErrorActionPreference = "Stop" + + $requiredSecrets = @( + $env:AZURE_TENANT_ID, + $env:AZURE_CLIENT_ID, + $env:AZURE_CLIENT_SECRET, + $env:AZURE_TRUSTED_SIGNING_ENDPOINT, + $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, + $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME, + $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME + ) + if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) { + Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation." + exit 0 + } + + try { + Install-PackageProvider ` + -Name NuGet ` + -MinimumVersion 2.8.5.201 ` + -Force ` + -Scope CurrentUser ` + -ErrorAction Stop + } catch { + Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)" + } + + Install-Module ` + -Name TrustedSigning ` + -MinimumVersion 0.5.0 ` + -Force ` + -AllowClobber ` + -Repository PSGallery ` + -Scope CurrentUser ` + -ErrorAction Stop + + Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force + Get-Command Invoke-TrustedSigning -ErrorAction Stop + + $moduleRoots = @( + [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"), + [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"), + [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"), + [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules") + ) + $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) | + Where-Object { $_ -and (Test-Path $_) } | + Select-Object -Unique + "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV + + - name: Build desktop artifact + shell: bash + env: + pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata + T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }} + T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS: ${{ steps.capture_helper_cache.outputs.cache-hit == 'true' }} + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} + MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }} + T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} + run: | + args=( + --platform "${{ inputs.platform }}" + --target "${{ inputs.target }}" + --arch "${{ inputs.arch }}" + --build-version "${{ inputs.version }}" + --skip-build + --verbose + ) + + has_all() { + for value in "$@"; do + if [[ -z "$value" ]]; then + return 1 + fi + done + return 0 + } + + if [[ "${{ inputs.platform }}" == "mac" ]]; then + if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then + if ! has_all "$APPLE_TEAM_ID" "$MACOS_PROVISIONING_PROFILE"; then + echo "macOS signing is configured, but APPLE_TEAM_ID or MACOS_PROVISIONING_PROFILE is missing." >&2 + exit 1 + fi + + key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" + printf '%s' "$APPLE_API_KEY" > "$key_path" + export APPLE_API_KEY="$key_path" + + profile_path="$RUNNER_TEMP/t3code.provisionprofile" + printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path" + security cms -D -i "$profile_path" >/dev/null + export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID" + export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_path" + + echo "macOS signing enabled." + args+=(--signed) + else + echo "macOS signing disabled (missing one or more Apple signing secrets)." + fi + elif [[ "${{ inputs.platform }}" == "win" ]]; then + # Embed the Linux CLI archive built by the same-arch Linux job as + # the WSL runtime. Required for a working WSL backend on Windows. + args+=(--wsl-runtime "$GITHUB_WORKSPACE"/wsl-runtime/t3-*-linux-${{ inputs.arch }}.tar.gz) + if has_all \ + "$AZURE_TENANT_ID" \ + "$AZURE_CLIENT_ID" \ + "$AZURE_CLIENT_SECRET" \ + "$AZURE_TRUSTED_SIGNING_ENDPOINT" \ + "$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \ + "$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \ + "$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then + echo "Windows signing enabled (Azure Trusted Signing)." + args+=(--signed) + else + echo "Windows signing disabled (missing one or more Azure Trusted Signing secrets)." + fi + else + echo "Signing disabled for ${{ inputs.platform }}." + fi + + vp run dist:desktop:artifact "${args[@]}" + + # The single-executable is built with a Node that supports --build-sea + # (25.7+); the repo itself stays on the engines.node version. It always + # injects into the runner's own Node: tsdown's cross-target download path + # runs `tar` on a drive-letter path on Windows, which GNU tar reads as a + # remote host, and a cross-built macOS binary cannot be smoke-tested. + - name: Build CLI single-executable + if: inputs.cli_archive + shell: bash + env: + # The exact version, not a major: vp downloads it from nodejs.org/dist on + # the runner, and only exact versions have a dist directory. Keep in + # step with SEA_NODE_VERSION in apps/server/vite.config.ts. + VP_NODE_VERSION: "26.8.2" + run: node apps/server/scripts/cli.ts build-exe --verbose + + - name: Import macOS signing certificate for the CLI archive + if: inputs.cli_archive && inputs.platform == 'mac' + shell: bash + env: + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + run: | + set -euo pipefail + if [[ -z "$CSC_LINK" || -z "$CSC_KEY_PASSWORD" ]]; then + echo "macOS CLI signing disabled (missing CSC_LINK); the archive is signed ad hoc." + exit 0 + fi + keychain="$RUNNER_TEMP/t3-cli-signing.keychain-db" + keychain_password="$(openssl rand -hex 16)" + cert_path="$RUNNER_TEMP/t3-cli-signing.p12" + printf '%s' "$CSC_LINK" | base64 --decode > "$cert_path" + security create-keychain -p "$keychain_password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + security import "$cert_path" -k "$keychain" -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null + security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') + identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application: [^"]*\)".*/\1/p' | head -n 1)" + if [[ -z "$identity" ]]; then + echo "No Developer ID Application identity found in CSC_LINK." >&2 + exit 1 + fi + echo "::add-mask::$keychain_password" + echo "T3CODE_CLI_MAC_SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" + echo "macOS CLI signing enabled." + + - name: Stage resource monitor for the CLI archive + if: inputs.cli_archive + shell: bash + run: | + set -euo pipefail + binary_name="t3-resource-monitor" + if [[ "${{ inputs.platform }}" == "win" ]]; then + binary_name="${binary_name}.exe" + fi + target_dir="$RUNNER_TEMP/cli-resource-monitor/${{ inputs.resource_key }}" + mkdir -p "$target_dir" + cp "native/resource-monitor/target/${{ inputs.rust_target }}/release/${binary_name}" "$target_dir/$binary_name" + + - name: Build CLI archive + if: inputs.cli_archive + shell: bash + env: + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + run: | + set -euo pipefail + if [[ "${{ inputs.platform }}" == "mac" && -n "${APPLE_API_KEY:-}" ]]; then + key_path="$RUNNER_TEMP/AuthKey_cli_${APPLE_API_KEY_ID}.p8" + printf '%s' "$APPLE_API_KEY" > "$key_path" + export APPLE_API_KEY="$key_path" + fi + node scripts/build-cli-archive.ts \ + --platform "${{ inputs.platform }}" \ + --arch "${{ inputs.arch }}" \ + --version "${{ inputs.version }}" \ + --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \ + --output-dir release-cli + + - name: Smoke-test CLI archive + if: inputs.cli_archive + shell: bash + run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ inputs.version }}" + + - name: Upload CLI archive + if: inputs.cli_archive + uses: actions/upload-artifact@v7 + with: + name: cli-${{ inputs.platform }}-${{ inputs.arch }} + path: release-cli/* + if-no-files-found: error + + - name: Collect release assets + shell: bash + run: | + set -euo pipefail + mkdir -p release-publish + + shopt -s nullglob + patterns=( + "release/*.dmg" + "release/*.zip" + "release/*.AppImage" + "release/*.exe" + ) + # Preview builds have no publish config, so electron-builder writes + # no feed manifest for them, but it still emits blockmaps beside the + # installers. Neither belongs on a release no updater may follow. + if [[ "${{ inputs.release_channel }}" != "preview" ]]; then + patterns+=("release/*.blockmap" "release/*.yml") + fi + for pattern in "${patterns[@]}"; do + for file in $pattern; do + cp "$file" release-publish/ + done + done + + if [[ "${{ inputs.platform }}" == "mac" && "${{ inputs.arch }}" != "arm64" ]]; then + shopt -s nullglob + for manifest in release-publish/*-mac.yml; do + mv "$manifest" "${manifest%.yml}-${{ inputs.arch }}.yml" + done + fi + + # Windows updater metadata is channel-specific (for example + # "latest.yml" or "nightly.yml") and carries no arch, so the x64 and + # arm64 jobs would upload the same name. Suffix each per-arch copy; + # the release job merges them back into one manifest per channel. + # builder-debug.yml is electron-builder's config dump, not a feed. + if [[ "${{ inputs.platform }}" == "win" ]]; then + for manifest in release-publish/*.yml; do + [[ "$manifest" == */builder-debug.yml ]] && continue + mv "$manifest" "${manifest%.yml}-win-${{ inputs.arch }}.yml" + done + fi + + - name: Collect resource monitor + shell: bash + run: | + set -euo pipefail + binary_name="t3-resource-monitor" + if [[ "${{ inputs.platform }}" == "win" ]]; then + binary_name="${binary_name}.exe" + fi + source_path="native/resource-monitor/target/${{ inputs.rust_target }}/release/${binary_name}" + target_dir="resource-monitor-publish/${{ inputs.resource_key }}" + mkdir -p "$target_dir" + cp "$source_path" "$target_dir/$binary_name" + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: desktop-${{ inputs.platform }}-${{ inputs.arch }} + path: release-publish/* + if-no-files-found: error + + - name: Upload resource monitor + uses: actions/upload-artifact@v7 + with: + name: resource-monitor-${{ inputs.resource_key }} + path: resource-monitor-publish/${{ inputs.resource_key }}/* + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c920d010e7e..aff55e16c6d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -348,36 +348,18 @@ jobs: echo "clerk_cli_oauth_client_id=$CLERK_CLI_OAUTH_CLIENT_ID" >> "$GITHUB_OUTPUT" echo "relay_url=https://$relay_domain" >> "$GITHUB_OUTPUT" - # node-pty publishes no Linux prebuilt and the WSL backend runs under the - # distro's own (Linux) Node, which can't load the Windows/Electron binary. We - # build the Linux pty.node here, on Linux, and hand it to the Windows packaging - # job — the Windows artifact then ships a ready WSL backend binary with no - # cross-compiling and no first-launch compiler/node-gyp/network on the user's - # machine. node-pty is N-API, so one binary works across all WSL Node versions. - # The Linux CLI archive is built ahead of the desktop matrix because two - # consumers need it: the Linux desktop entry attaches it to the release, and - # the Windows desktop entry embeds it as the WSL runtime. Building it once - # here means the WSL backend runs the exact bytes a Linux user downloads. - build_linux_cli: - name: Build CLI archive (linux-${{ matrix.arch }}) + # The platform-independent JS (server bundle, web client, Electron main) is + # built exactly once here and handed to every platform job as `js-bundle`. + # The relay/Clerk values are baked into the bundle, so they belong to this + # job rather than to the packaging jobs. + build_bundle: + name: Build JS bundle # Same gating as relay_public_config: only the release commit is needed, so # this runs alongside preflight. See the condition comment there. - needs: [resolve_commit, preflight, relay_public_config] + needs: [preflight, relay_public_config] if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }} - runs-on: ${{ matrix.runner }} + runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - arch: x64 - runner: blacksmith-32vcpu-ubuntu-2404 - rust_target: x86_64-unknown-linux-gnu - # node-pty has no Linux prebuild and compiles from source, so the - # arm64 archive is built on arm64 hardware rather than cross-built. - - arch: arm64 - runner: ubuntu-24.04-arm - rust_target: aarch64-unknown-linux-gnu env: T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} @@ -398,358 +380,8 @@ jobs: with: node-version-file: package.json cache: true - run-install: | - args: - - --filter=t3... - - --filter=@t3tools/web... - - --filter=@t3tools/scripts... - - - name: Cache resource monitor - id: resource_monitor_cache - uses: actions/cache@v6 - with: - path: native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor - key: resource-monitor-${{ matrix.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }} - - - name: Setup Rust - if: steps.resource_monitor_cache.outputs.cache-hit != 'true' - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.rust_target }} - - - name: Build resource monitor - if: steps.resource_monitor_cache.outputs.cache-hit != 'true' - run: cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml --target ${{ matrix.rust_target }} - - - name: Download relay client tracing config - uses: actions/download-artifact@v8 - with: - name: relay-client-tracing-config - path: ${{ runner.temp }}/relay-client-tracing - - - name: Load relay client tracing config - shell: bash - run: | - config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" - tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" - echo "::add-mask::$tracing_token" - cat "$config_path" >> "$GITHUB_ENV" - - - name: Align package versions to release version - run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - # The t3 build task depends on @t3tools/web#build, so the web client is - # built as part of this step. - - name: Build CLI package - run: vp run --filter t3 build - - - name: Build CLI single-executable - env: - # The exact version, not a major: vp downloads it from nodejs.org/dist on - # the runner, and only exact versions have a dist directory. Keep in - # step with SEA_NODE_VERSION in apps/server/vite.config.ts. - VP_NODE_VERSION: "26.8.2" - run: node apps/server/scripts/cli.ts build-exe --verbose - - - name: Stage resource monitor for the CLI archive - run: | - set -euo pipefail - target_dir="$RUNNER_TEMP/cli-resource-monitor/linux-${{ matrix.arch }}" - mkdir -p "$target_dir" - cp native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor "$target_dir/" - - - name: Build CLI archive - run: | - node scripts/build-cli-archive.ts \ - --platform linux \ - --arch ${{ matrix.arch }} \ - --version "${{ needs.preflight.outputs.version }}" \ - --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \ - --output-dir release-cli - - - name: Smoke-test CLI archive - run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ needs.preflight.outputs.version }}" - - - name: Upload CLI archive - uses: actions/upload-artifact@v7 - with: - name: cli-linux-${{ matrix.arch }} - path: release-cli/* - if-no-files-found: error - - # Windows arm64 has no desktop build yet (the NSIS arm64 row is still off), - # but the CLI archive is built here on arm64 hardware so it is signed and - # smoke-tested on the architecture it targets, like every other archive. - build_windows_arm64_cli: - name: Build CLI archive (win32-arm64) - needs: [resolve_commit, preflight, relay_public_config] - if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }} - runs-on: windows-11-arm - timeout-minutes: 30 - env: - T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} - T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} - T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} - T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: false run-install: false - - name: Download dependency verification - continue-on-error: true - uses: actions/download-artifact@v8 - with: - name: release-dependency-verification - path: ${{ runner.temp }}/pnpm-metadata - - - name: Install dependencies - env: - pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata - run: vp install --filter=t3... --filter=@t3tools/web... --filter=@t3tools/scripts... - - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-pc-windows-msvc - - - name: Build resource monitor - run: cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml --target aarch64-pc-windows-msvc - - - name: Download relay client tracing config - uses: actions/download-artifact@v8 - with: - name: relay-client-tracing-config - path: ${{ runner.temp }}/relay-client-tracing - - - name: Load relay client tracing config - shell: bash - run: | - config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" - tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" - echo "::add-mask::$tracing_token" - cat "$config_path" >> "$GITHUB_ENV" - - - name: Align package versions to release version - run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - # The t3 build task depends on @t3tools/web#build, so the web client is - # built as part of this step. - - name: Build CLI package - run: vp run --filter t3 build - - - name: Build CLI single-executable - shell: bash - env: - VP_NODE_VERSION: "26.8.2" - run: node apps/server/scripts/cli.ts build-exe --verbose - - - name: Prepare Azure Trusted Signing - shell: pwsh - env: - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} - run: | - $ErrorActionPreference = "Stop" - - $requiredSecrets = @( - $env:AZURE_TENANT_ID, - $env:AZURE_CLIENT_ID, - $env:AZURE_CLIENT_SECRET, - $env:AZURE_TRUSTED_SIGNING_ENDPOINT, - $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, - $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME, - $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME - ) - if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) { - Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation." - exit 0 - } - - try { - Install-PackageProvider ` - -Name NuGet ` - -MinimumVersion 2.8.5.201 ` - -Force ` - -Scope CurrentUser ` - -ErrorAction Stop - } catch { - Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)" - } - - Install-Module ` - -Name TrustedSigning ` - -MinimumVersion 0.5.0 ` - -Force ` - -AllowClobber ` - -Repository PSGallery ` - -Scope CurrentUser ` - -ErrorAction Stop - - Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force - Get-Command Invoke-TrustedSigning -ErrorAction Stop - - $moduleRoots = @( - [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"), - [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"), - [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"), - [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules") - ) - $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) | - Where-Object { $_ -and (Test-Path $_) } | - Select-Object -Unique - "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV - - - name: Stage resource monitor for the CLI archive - shell: bash - run: | - set -euo pipefail - target_dir="$RUNNER_TEMP/cli-resource-monitor/win32-arm64" - mkdir -p "$target_dir" - cp native/resource-monitor/target/aarch64-pc-windows-msvc/release/t3-resource-monitor.exe "$target_dir/" - - - name: Build CLI archive - shell: bash - env: - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - run: | - node scripts/build-cli-archive.ts \ - --platform win \ - --arch arm64 \ - --version "${{ needs.preflight.outputs.version }}" \ - --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \ - --output-dir release-cli - - - name: Smoke-test CLI archive - shell: bash - run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ needs.preflight.outputs.version }}" - - - name: Upload CLI archive - uses: actions/upload-artifact@v7 - with: - name: cli-win-arm64 - path: release-cli/* - if-no-files-found: error - - build: - name: Build ${{ matrix.label }} - # build_linux_cli stays in `needs` so it runs first and its artifact is - # available to download, but only the Windows matrix entry consumes it (as - # the WSL runtime). The job is gated on preflight + relay WITHOUT requiring - # build_linux_cli, so a failed Linux archive doesn't skip the macOS builds. - # `!cancelled()` (not `!failure()`) lets the job run even when - # build_linux_cli failed; the Windows-only download step below then fails - # that single platform if the archive is missing. - needs: [preflight, relay_public_config, build_linux_cli] - if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 30 - env: - T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} - T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} - T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} - T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} - strategy: - fail-fast: false - matrix: - include: - # cli_archive: whether the job also builds the self-contained CLI - # archive for its own platform/arch, on this runner, and smoke-tests - # it here. Every archive is built on hardware of its own - # architecture: Linux and Windows arm64 have their own jobs below. - - label: macOS arm64 - runner: blacksmith-12vcpu-macos-26 - platform: mac - target: dmg - arch: arm64 - rust_target: aarch64-apple-darwin - resource_key: darwin-arm64 - cli_archive: true - # No CLI archive: Node single-executables are unsupported on x64 - # macOS (the SEA docs list macOS as arm64 only) and the built binary - # segfaults on start. The x64 desktop app is Electron and unaffected. - - label: macOS x64 - runner: blacksmith-12vcpu-macos-26 - platform: mac - target: dmg - arch: x64 - rust_target: x86_64-apple-darwin - resource_key: darwin-x64 - cli_archive: false - # The Linux CLI archive is produced by build_linux_cli, not here. - - label: Linux x64 - runner: blacksmith-32vcpu-ubuntu-2404 - platform: linux - target: AppImage - arch: x64 - rust_target: x86_64-unknown-linux-gnu - resource_key: linux-x64 - cli_archive: false - - label: Windows x64 - runner: blacksmith-32vcpu-windows-2025 - platform: win - target: nsis - arch: x64 - rust_target: x86_64-pc-windows-msvc - resource_key: win32-x64 - cli_archive: true - # - label: Windows arm64 - # runner: windows-11-arm - # platform: win - # target: nsis - # arch: arm64 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: ${{ matrix.platform != 'win' }} - run-install: false - - - name: Resolve Windows package cache path - if: matrix.platform == 'win' - id: package_cache_path - shell: pwsh - run: '"path=$(vp pm cache dir)" >> $env:GITHUB_OUTPUT' - - - name: Cache Windows packages - if: matrix.platform == 'win' - uses: actions/cache@v6 - with: - path: ${{ steps.package_cache_path.outputs.path }} - key: windows-release-packages-v1-${{ matrix.arch }}-${{ hashFiles('pnpm-lock.yaml') }} - # pnpm checks the lockfile and policy before reusing this result. A missing # artifact leaves the cache empty, so installation runs the checks again. - name: Download dependency verification @@ -759,33 +391,10 @@ jobs: name: release-dependency-verification path: ${{ runner.temp }}/pnpm-metadata - - name: Install desktop dependencies + - name: Install bundle dependencies env: pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata - run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts... - - - name: Cache resource monitor - id: resource_monitor_cache - uses: actions/cache@v6 - with: - path: native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor${{ matrix.platform == 'win' && '.exe' || '' }} - key: resource-monitor-${{ matrix.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }} - - - name: Cache Linux capture helpers - if: matrix.platform == 'linux' - id: capture_helper_cache - uses: actions/cache@v6 - with: - path: | - native/kde-snap-shot/target/${{ matrix.rust_target }}/release/t3-kde-snap-shot - native/hyprland-snap-shot/target/${{ matrix.rust_target }}/release/t3-hyprland-snap-shot - key: linux-capture-helpers-${{ matrix.rust_target }}-${{ hashFiles('native/kde-snap-shot/Cargo.lock', 'native/kde-snap-shot/Cargo.toml', 'native/kde-snap-shot/src/**', 'native/hyprland-snap-shot/Cargo.lock', 'native/hyprland-snap-shot/Cargo.toml', 'native/hyprland-snap-shot/src/**', 'native/hyprland-snap-shot/protocols/**') }} - - - name: Setup Rust - if: steps.resource_monitor_cache.outputs.cache-hit != 'true' || (matrix.platform == 'linux' && steps.capture_helper_cache.outputs.cache-hit != 'true') - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.rust_target }} + run: vp install --filter=t3... --filter=@t3tools/web... --filter=@t3tools/desktop... --filter=@t3tools/scripts... - name: Download relay client tracing config uses: actions/download-artifact@v8 @@ -804,373 +413,190 @@ jobs: - name: Align package versions to release version run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - # The WSL backend runs the Linux CLI archive inside the distro, so the - # Windows desktop embeds the same archive the release attaches. - - name: Download Linux CLI archive for WSL - if: matrix.platform == 'win' - uses: actions/download-artifact@v8 - with: - name: cli-linux-x64 - path: wsl-runtime - - - name: Install Spectre-mitigated MSVC libs - if: matrix.platform == 'win' - shell: pwsh - run: | - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - $installPath = & $vswhere -products * -latest -property installationPath - $setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe" - $proc = Start-Process -FilePath $setupExe ` - -ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", ` - "Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre", "--quiet", "--norestart" ` - -Wait -PassThru -NoNewWindow - if ($null -eq $proc -or $proc.ExitCode -ne 0) { - $code = if ($null -ne $proc) { $proc.ExitCode } else { 1 } - Write-Error "Visual Studio Installer failed with exit code $code" - exit $code - } - - uses: ./.github/actions/setup-apt-mirrors - if: matrix.platform == 'linux' - - - name: Install Linux desktop build libraries - if: matrix.platform == 'linux' - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y libsecret-1-dev pkg-config - if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then - sudo apt-get install -y imagemagick - fi - - if command -v magick >/dev/null 2>&1; then - magick -version - else - convert -version - fi - - - name: Prepare Azure Trusted Signing - if: matrix.platform == 'win' - shell: pwsh - env: - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} - run: | - $ErrorActionPreference = "Stop" - - $requiredSecrets = @( - $env:AZURE_TENANT_ID, - $env:AZURE_CLIENT_ID, - $env:AZURE_CLIENT_SECRET, - $env:AZURE_TRUSTED_SIGNING_ENDPOINT, - $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, - $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME, - $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME - ) - if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) { - Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation." - exit 0 - } - - try { - Install-PackageProvider ` - -Name NuGet ` - -MinimumVersion 2.8.5.201 ` - -Force ` - -Scope CurrentUser ` - -ErrorAction Stop - } catch { - Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)" - } - - Install-Module ` - -Name TrustedSigning ` - -MinimumVersion 0.5.0 ` - -Force ` - -AllowClobber ` - -Repository PSGallery ` - -Scope CurrentUser ` - -ErrorAction Stop - - Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force - Get-Command Invoke-TrustedSigning -ErrorAction Stop - - $moduleRoots = @( - [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"), - [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"), - [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"), - [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules") - ) - $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) | - Where-Object { $_ -and (Test-Path $_) } | - Select-Object -Unique - "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV - - - name: Build desktop artifact - shell: bash - env: - pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata - T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }} - T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS: ${{ steps.capture_helper_cache.outputs.cache-hit == 'true' }} - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} - MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }} - T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} - run: | - args=( - --platform "${{ matrix.platform }}" - --target "${{ matrix.target }}" - --arch "${{ matrix.arch }}" - --build-version "${{ needs.preflight.outputs.version }}" - --verbose - ) - - has_all() { - for value in "$@"; do - if [[ -z "$value" ]]; then - return 1 - fi - done - return 0 - } - - if [[ "${{ matrix.platform }}" == "mac" ]]; then - if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then - if ! has_all "$APPLE_TEAM_ID" "$MACOS_PROVISIONING_PROFILE"; then - echo "macOS signing is configured, but APPLE_TEAM_ID or MACOS_PROVISIONING_PROFILE is missing." >&2 - exit 1 - fi - - key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" - printf '%s' "$APPLE_API_KEY" > "$key_path" - export APPLE_API_KEY="$key_path" - - profile_path="$RUNNER_TEMP/t3code.provisionprofile" - printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path" - security cms -D -i "$profile_path" >/dev/null - export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID" - export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_path" - - echo "macOS signing enabled." - args+=(--signed) - else - echo "macOS signing disabled (missing one or more Apple signing secrets)." - fi - elif [[ "${{ matrix.platform }}" == "win" ]]; then - # Embed the Linux CLI archive built by build_linux_cli as the WSL - # runtime. Required for a working WSL backend on Windows. - args+=(--wsl-runtime "$GITHUB_WORKSPACE"/wsl-runtime/t3-*-linux-x64.tar.gz) - if has_all \ - "$AZURE_TENANT_ID" \ - "$AZURE_CLIENT_ID" \ - "$AZURE_CLIENT_SECRET" \ - "$AZURE_TRUSTED_SIGNING_ENDPOINT" \ - "$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \ - "$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \ - "$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then - echo "Windows signing enabled (Azure Trusted Signing)." - args+=(--signed) - else - echo "Windows signing disabled (missing one or more Azure Trusted Signing secrets)." - fi - else - echo "Signing disabled for ${{ matrix.platform }}." - fi - - vp run dist:desktop:artifact "${args[@]}" - - # The single-executable is built with a Node that supports --build-sea - # (25.7+); the repo itself stays on the engines.node version. It always - # injects into the runner's own Node: tsdown's cross-target download path - # runs `tar` on a drive-letter path on Windows, which GNU tar reads as a - # remote host, and a cross-built macOS binary cannot be smoke-tested. - - name: Build CLI single-executable - if: matrix.cli_archive - shell: bash - env: - # The exact version, not a major: vp downloads it from nodejs.org/dist on - # the runner, and only exact versions have a dist directory. Keep in - # step with SEA_NODE_VERSION in apps/server/vite.config.ts. - VP_NODE_VERSION: "26.8.2" - run: node apps/server/scripts/cli.ts build-exe --verbose - - - name: Import macOS signing certificate for the CLI archive - if: matrix.cli_archive && matrix.platform == 'mac' - shell: bash - env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - run: | - set -euo pipefail - if [[ -z "$CSC_LINK" || -z "$CSC_KEY_PASSWORD" ]]; then - echo "macOS CLI signing disabled (missing CSC_LINK); the archive is signed ad hoc." - exit 0 - fi - keychain="$RUNNER_TEMP/t3-cli-signing.keychain-db" - keychain_password="$(openssl rand -hex 16)" - cert_path="$RUNNER_TEMP/t3-cli-signing.p12" - printf '%s' "$CSC_LINK" | base64 --decode > "$cert_path" - security create-keychain -p "$keychain_password" "$keychain" - security set-keychain-settings -lut 21600 "$keychain" - security unlock-keychain -p "$keychain_password" "$keychain" - security import "$cert_path" -k "$keychain" -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null - security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') - identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application: [^"]*\)".*/\1/p' | head -n 1)" - if [[ -z "$identity" ]]; then - echo "No Developer ID Application identity found in CSC_LINK." >&2 - exit 1 - fi - echo "::add-mask::$keychain_password" - echo "T3CODE_CLI_MAC_SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" - echo "macOS CLI signing enabled." - - - name: Stage resource monitor for the CLI archive - if: matrix.cli_archive - shell: bash - run: | - set -euo pipefail - binary_name="t3-resource-monitor" - if [[ "${{ matrix.platform }}" == "win" ]]; then - binary_name="${binary_name}.exe" - fi - target_dir="$RUNNER_TEMP/cli-resource-monitor/${{ matrix.resource_key }}" - mkdir -p "$target_dir" - cp "native/resource-monitor/target/${{ matrix.rust_target }}/release/${binary_name}" "$target_dir/$binary_name" - - name: Build CLI archive - if: matrix.cli_archive - shell: bash - env: - APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - run: | - set -euo pipefail - if [[ "${{ matrix.platform }}" == "mac" && -n "${APPLE_API_KEY:-}" ]]; then - key_path="$RUNNER_TEMP/AuthKey_cli_${APPLE_API_KEY_ID}.p8" - printf '%s' "$APPLE_API_KEY" > "$key_path" - export APPLE_API_KEY="$key_path" - fi - node scripts/build-cli-archive.ts \ - --platform "${{ matrix.platform }}" \ - --arch "${{ matrix.arch }}" \ - --version "${{ needs.preflight.outputs.version }}" \ - --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \ - --output-dir release-cli - - - name: Smoke-test CLI archive - if: matrix.cli_archive - shell: bash - run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ needs.preflight.outputs.version }}" - - - name: Upload CLI archive - if: matrix.cli_archive - uses: actions/upload-artifact@v7 - with: - name: cli-${{ matrix.platform }}-${{ matrix.arch }} - path: release-cli/* - if-no-files-found: error - - - name: Collect release assets - shell: bash - run: | - set -euo pipefail - mkdir -p release-publish - - shopt -s nullglob - patterns=( - "release/*.dmg" - "release/*.zip" - "release/*.AppImage" - "release/*.exe" - ) - # Preview builds have no publish config, so electron-builder writes - # no feed manifest for them, but it still emits blockmaps beside the - # installers. Neither belongs on a release no updater may follow. - if [[ "${{ needs.preflight.outputs.release_channel }}" != "preview" ]]; then - patterns+=("release/*.blockmap" "release/*.yml") - fi - for pattern in "${patterns[@]}"; do - for file in $pattern; do - cp "$file" release-publish/ - done - done - - if [[ "${{ matrix.platform }}" == "mac" && "${{ matrix.arch }}" != "arm64" ]]; then - shopt -s nullglob - for manifest in release-publish/*-mac.yml; do - mv "$manifest" "${manifest%.yml}-${{ matrix.arch }}.yml" - done - fi + # @t3tools/desktop#build compiles the Linux browser secret helper on a + # Linux host before packing, and that needs libsecret headers. + - name: Install browser secret helper build libraries + run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config - # Enable if Windows arm64 builds are enabled. - # Windows updater metadata is channel-specific (for example - # "latest.yml" or "nightly.yml"). Suffix each per-arch copy so the - # release job can merge matching arm64/x64 manifests back into one - # canonical manifest per channel. - # if [[ "${{ matrix.platform }}" == "win" ]]; then - # shopt -s nullglob - # for manifest in release-publish/*.yml; do - # mv "$manifest" "${manifest%.yml}-win-${{ matrix.arch }}.yml" - # done - # fi - - - name: Collect resource monitor - shell: bash - run: | - set -euo pipefail - binary_name="t3-resource-monitor" - if [[ "${{ matrix.platform }}" == "win" ]]; then - binary_name="${binary_name}.exe" - fi - source_path="native/resource-monitor/target/${{ matrix.rust_target }}/release/${binary_name}" - target_dir="resource-monitor-publish/${{ matrix.resource_key }}" - mkdir -p "$target_dir" - cp "$source_path" "$target_dir/$binary_name" + # Runs t3#build (which depends on @t3tools/web#build) and + # @t3tools/desktop#build, so apps/server/dist holds the server bundle + # plus the web client and apps/desktop/dist-electron the Electron main. + - name: Build JS bundle + run: vp run build:desktop - - name: Upload build artifacts + # Two paths under apps/ so the artifact root is apps/; consumers download + # into `apps` to restore both at their original locations. + - name: Upload JS bundle uses: actions/upload-artifact@v7 with: - name: desktop-${{ matrix.platform }}-${{ matrix.arch }} - path: release-publish/* + name: js-bundle + path: | + apps/server/dist + apps/desktop/dist-electron if-no-files-found: error + retention-days: 1 - - name: Upload resource monitor - uses: actions/upload-artifact@v7 - with: - name: resource-monitor-${{ matrix.resource_key }} - path: resource-monitor-publish/${{ matrix.resource_key }}/* - if-no-files-found: error + # One job per platform and architecture (see release-desktop.yml), each on + # hardware of its own architecture, and each gated only on what it consumes: + # every platform needs the JS bundle, and the Windows jobs also need the + # same-arch Linux job, whose CLI archive they embed as the WSL runtime. Every + # job builds the desktop app; all but macOS x64 also build the CLI archive + # for their platform, so a target either ships fully or not at all. + desktop_mac_arm64: + name: Desktop macOS arm64 + needs: [preflight, relay_public_config, build_bundle] + if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }} + uses: ./.github/workflows/release-desktop.yml + secrets: inherit + with: + version: ${{ needs.preflight.outputs.version }} + ref: ${{ needs.preflight.outputs.ref }} + release_channel: ${{ needs.preflight.outputs.release_channel }} + clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} + clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} + clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} + relay_url: ${{ needs.relay_public_config.outputs.relay_url }} + label: macOS arm64 + runner: blacksmith-12vcpu-macos-26 + platform: mac + target: dmg + arch: arm64 + rust_target: aarch64-apple-darwin + resource_key: darwin-arm64 + cli_archive: true + + desktop_mac_x64: + name: Desktop macOS x64 + needs: [preflight, relay_public_config, build_bundle] + if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }} + uses: ./.github/workflows/release-desktop.yml + secrets: inherit + with: + version: ${{ needs.preflight.outputs.version }} + ref: ${{ needs.preflight.outputs.ref }} + release_channel: ${{ needs.preflight.outputs.release_channel }} + clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} + clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} + clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} + relay_url: ${{ needs.relay_public_config.outputs.relay_url }} + label: macOS x64 + runner: blacksmith-12vcpu-macos-26 + platform: mac + target: dmg + arch: x64 + rust_target: x86_64-apple-darwin + resource_key: darwin-x64 + # No CLI archive: Node single-executables are unsupported on x64 macOS + # (the SEA docs list macOS as arm64 only) and the built binary segfaults + # on start. The x64 desktop app is Electron and unaffected. + cli_archive: false + + desktop_linux_x64: + name: Desktop Linux x64 + needs: [preflight, relay_public_config, build_bundle] + if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }} + uses: ./.github/workflows/release-desktop.yml + secrets: inherit + with: + version: ${{ needs.preflight.outputs.version }} + ref: ${{ needs.preflight.outputs.ref }} + release_channel: ${{ needs.preflight.outputs.release_channel }} + clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} + clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} + clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} + relay_url: ${{ needs.relay_public_config.outputs.relay_url }} + label: Linux x64 + runner: blacksmith-32vcpu-ubuntu-2404 + platform: linux + target: AppImage + arch: x64 + rust_target: x86_64-unknown-linux-gnu + resource_key: linux-x64 + cli_archive: true + + # node-pty has no Linux prebuild and compiles from source, so the arm64 app + # and archive are built on arm64 hardware rather than cross-built. + desktop_linux_arm64: + name: Desktop Linux arm64 + needs: [preflight, relay_public_config, build_bundle] + if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }} + uses: ./.github/workflows/release-desktop.yml + secrets: inherit + with: + version: ${{ needs.preflight.outputs.version }} + ref: ${{ needs.preflight.outputs.ref }} + release_channel: ${{ needs.preflight.outputs.release_channel }} + clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} + clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} + clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} + relay_url: ${{ needs.relay_public_config.outputs.relay_url }} + label: Linux arm64 + runner: ubuntu-24.04-arm + platform: linux + target: AppImage + arch: arm64 + rust_target: aarch64-unknown-linux-gnu + resource_key: linux-arm64 + cli_archive: true + + # The Windows jobs embed the same-arch Linux CLI archive as the WSL runtime. + # `!cancelled()` (not `!failure()`) still lets them start when that Linux job + # failed; the download step inside then fails this single platform if the + # archive is missing. + desktop_win_x64: + name: Desktop Windows x64 + needs: [preflight, relay_public_config, build_bundle, desktop_linux_x64] + if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }} + uses: ./.github/workflows/release-desktop.yml + secrets: inherit + with: + version: ${{ needs.preflight.outputs.version }} + ref: ${{ needs.preflight.outputs.ref }} + release_channel: ${{ needs.preflight.outputs.release_channel }} + clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} + clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} + clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} + relay_url: ${{ needs.relay_public_config.outputs.relay_url }} + label: Windows x64 + runner: blacksmith-32vcpu-windows-2025 + platform: win + target: nsis + arch: x64 + rust_target: x86_64-pc-windows-msvc + resource_key: win32-x64 + cli_archive: true + + desktop_win_arm64: + name: Desktop Windows arm64 + needs: [preflight, relay_public_config, build_bundle, desktop_linux_arm64] + if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }} + uses: ./.github/workflows/release-desktop.yml + secrets: inherit + with: + version: ${{ needs.preflight.outputs.version }} + ref: ${{ needs.preflight.outputs.ref }} + release_channel: ${{ needs.preflight.outputs.release_channel }} + clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} + clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} + clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} + relay_url: ${{ needs.relay_public_config.outputs.relay_url }} + label: Windows arm64 + runner: windows-11-arm + platform: win + target: nsis + arch: arm64 + rust_target: aarch64-pc-windows-msvc + resource_key: win32-arm64 + cli_archive: true # Preview releases never reach npm: the archive on the GitHub Release is the # only way to obtain one, so no dist-tag can ever resolve to a preview build. publish_cli: name: Publish CLI to npm - needs: [preflight, relay_public_config, quality, build] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' && needs.preflight.outputs.release_channel != 'preview' }} + needs: [preflight, relay_public_config, quality, build_bundle] + if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.build_bundle.result == 'success' && needs.preflight.outputs.release_channel != 'preview' }} runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 10 permissions: @@ -1199,30 +625,19 @@ jobs: run-install: | args: - --filter=t3... - - --filter=@t3tools/web... - --filter=@t3tools/scripts... - - name: Download relay client tracing config - uses: actions/download-artifact@v8 - with: - name: relay-client-tracing-config - path: ${{ runner.temp }}/relay-client-tracing - - - name: Load relay client tracing config - shell: bash - run: | - config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" - tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" - echo "::add-mask::$tracing_token" - cat "$config_path" >> "$GITHUB_ENV" - - name: Align package versions to release version run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - # The t3 build task depends on @t3tools/web#build, so the web client is - # built (once) as part of this step. - - name: Build CLI package - run: vp run --filter t3 build + # The artifact root is `apps/` (upload-artifact keeps the least common + # ancestor of its paths), so extracting into `apps` restores + # apps/server/dist and apps/desktop/dist-electron at their build paths. + - name: Download JS bundle + uses: actions/download-artifact@v8 + with: + name: js-bundle + path: apps - name: Download resource monitors uses: actions/download-artifact@v8 @@ -1247,8 +662,18 @@ jobs: release: name: Publish GitHub Release - needs: [preflight, build, build_windows_arm64_cli, publish_cli] - 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')) }} + needs: + [ + preflight, + desktop_mac_arm64, + desktop_mac_x64, + desktop_linux_x64, + desktop_linux_arm64, + desktop_win_x64, + desktop_win_arm64, + publish_cli, + ] + if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.desktop_mac_arm64.result == 'success' && needs.desktop_mac_x64.result == 'success' && needs.desktop_linux_x64.result == 'success' && needs.desktop_linux_arm64.result == 'success' && needs.desktop_win_x64.result == 'success' && needs.desktop_win_arm64.result == 'success' && (needs.publish_cli.result == 'success' || (needs.preflight.outputs.release_channel == 'preview' && needs.publish_cli.result == 'skipped')) }} runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 30 permissions: @@ -1319,6 +744,9 @@ jobs: exit 1 fi + # electron-updater reads one manifest per platform and channel and picks + # the file entry whose name carries the running arch, so the per-arch + # manifests the build jobs wrote are merged back into that one file. - name: Merge macOS updater manifests if: needs.preflight.outputs.release_channel != 'preview' run: | @@ -1331,6 +759,21 @@ jobs: fi done + - name: Merge Windows updater manifests + if: needs.preflight.outputs.release_channel != 'preview' + run: | + shopt -s nullglob + for x64_manifest in release-assets/*-win-x64.yml; do + arm64_manifest="${x64_manifest%-x64.yml}-arm64.yml" + merged_manifest="${x64_manifest%-win-x64.yml}.yml" + if [[ -f "$arm64_manifest" ]]; then + node scripts/merge-update-manifests.ts --platform win "$x64_manifest" "$arm64_manifest" "$merged_manifest" + rm -f "$x64_manifest" "$arm64_manifest" + else + mv "$x64_manifest" "$merged_manifest" + fi + done + # Updater manifests and blockmaps are what electron-updater consumes. # They are only listed for channels an updater is meant to follow. - id: release_files diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index cb90f3687184..669bdce8a72d 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -444,7 +444,7 @@ const mobileEndorsementRows = [ return assets.find((a) => a.name.endsWith("-arm64.dmg"))?.browser_download_url ?? null; } if (platform.os === "linux") { - return assets.find((a) => a.name.endsWith(".AppImage"))?.browser_download_url ?? null; + return assets.find((a) => a.name.endsWith("-x86_64.AppImage"))?.browser_download_url ?? null; } return null; } diff --git a/docs/operations/release.md b/docs/operations/release.md index abe9ae00a8d4..1b74c27b903f 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -23,18 +23,19 @@ This document covers the unified release workflow for stable and nightly desktop the commit to ship is not the latest nightly, such as a cherry-picked fix on a release branch. - Runs lint, typecheck, and tests alongside artifact builds. Publishing waits for every check. - Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients. -- Builds four artifacts in parallel for both channels: +- Builds the platform-independent JS (server bundle, web client, Electron main) once in the `build_bundle` job and hands it to every platform job as the `js-bundle` artifact; the platform jobs only package it, so no runner rebuilds it. +- Builds six desktop artifacts in parallel for both channels, each as its own job (`desktop__`, one call of `release-desktop.yml`) on hardware of its own architecture, gated only on the bundle (the Windows jobs also wait for the same-arch Linux job, whose CLI archive they embed as the WSL runtime): - macOS `arm64` DMG - macOS `x64` DMG - - Linux `x64` AppImage - - Windows `x64` NSIS installer + - Linux `x64` and `arm64` AppImage + - Windows `x64` and `arm64` NSIS installer - Publishes one GitHub Release with all produced files. - Stable tags with a suffix after `X.Y.Z` (for example `1.2.3-alpha.1`) are published as GitHub prereleases. - Only plain stable `X.Y.Z` releases are marked as the repository's latest release. - Nightly runs are always GitHub prereleases and never marked latest. - 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. - Includes Electron auto-update metadata (for example `latest*.yml`, `nightly*.yml`, and `*.blockmap`) in release assets. -- Builds a self-contained CLI archive per platform (`t3---.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. +- Builds a self-contained CLI archive per platform (`t3---.tar.gz`, `.zip` on Windows) in the same job as that target's desktop artifact 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. 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. - 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`. - 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`. - 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. @@ -253,9 +254,10 @@ executables declared as unpacked by that archive must be present at the matching paths below `resources/server.asar.unpacked`. The Windows-native backend reads the archive in place through Electron. Packaged Windows builds also ship `resources/wsl-runtime.tar.gz` plus its SHA-256 sidecar: the Linux CLI archive -(`t3--linux-x64.tar.gz`) built by the `build_linux_cli` job and handed -to the Windows desktop build as `--wsl-runtime`, copied in verbatim so WSL runs -the exact bytes a Linux user downloads. WSL verifies and extracts that archive +(`t3--linux-.tar.gz`, the same arch as the Windows host) built +by the Linux desktop job and handed to the Windows desktop build as +`--wsl-runtime`, copied in verbatim so WSL runs the exact bytes a Linux user +downloads. WSL verifies and extracts that archive into `~/.t3/wsl-runtime/sha256-` inside the selected distro, then reuses it for later launches of the same update. @@ -413,7 +415,7 @@ Checklist: 4. Verify workflow steps: - preflight passes - release quality checks pass - - all matrix builds pass + - `build_bundle` and all platform builds pass - `publish_cli` publishes the exact release version before the release job - release job uploads expected files 5. Smoke test downloaded artifacts.