| 1 | name: Release desktop build |
| 2 | |
| 3 | # One desktop platform/arch build, called once per target from release.yml so |
| 4 | # each target is its own job with its own `needs`. The JS bundle (server, web |
| 5 | # client, Electron main) comes from the `js-bundle` artifact that build_bundle |
| 6 | # produced; this job only packages it, builds the native helpers, and, where |
| 7 | # `cli_archive` is set, the self-contained CLI archive for its platform. |
| 8 | |
| 9 | on: |
| 10 | workflow_call: |
| 11 | secrets: |
| 12 | CSC_LINK: |
| 13 | required: false |
| 14 | CSC_KEY_PASSWORD: |
| 15 | required: false |
| 16 | APPLE_API_KEY: |
| 17 | required: false |
| 18 | APPLE_API_KEY_ID: |
| 19 | required: false |
| 20 | APPLE_API_ISSUER: |
| 21 | required: false |
| 22 | MACOS_PROVISIONING_PROFILE: |
| 23 | required: false |
| 24 | AZURE_TENANT_ID: |
| 25 | required: false |
| 26 | AZURE_CLIENT_ID: |
| 27 | required: false |
| 28 | AZURE_CLIENT_SECRET: |
| 29 | required: false |
| 30 | AZURE_TRUSTED_SIGNING_ENDPOINT: |
| 31 | required: false |
| 32 | AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: |
| 33 | required: false |
| 34 | AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: |
| 35 | required: false |
| 36 | AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: |
| 37 | required: false |
| 38 | inputs: |
| 39 | label: |
| 40 | required: true |
| 41 | type: string |
| 42 | runner: |
| 43 | required: true |
| 44 | type: string |
| 45 | platform: |
| 46 | required: true |
| 47 | type: string |
| 48 | target: |
| 49 | required: true |
| 50 | type: string |
| 51 | arch: |
| 52 | required: true |
| 53 | type: string |
| 54 | rust_target: |
| 55 | required: true |
| 56 | type: string |
| 57 | resource_key: |
| 58 | required: true |
| 59 | type: string |
| 60 | # Whether the job also builds the self-contained CLI archive for its own |
| 61 | # platform/arch, on this runner, and smoke-tests it here. Every archive |
| 62 | # is built on hardware of its own architecture. |
| 63 | cli_archive: |
| 64 | required: false |
| 65 | default: false |
| 66 | type: boolean |
| 67 | version: |
| 68 | required: true |
| 69 | type: string |
| 70 | ref: |
| 71 | required: true |
| 72 | type: string |
| 73 | release_channel: |
| 74 | required: true |
| 75 | type: string |
| 76 | # Whether a `relay-client-tracing-config` artifact from the production |
| 77 | # relay state is expected. PR previews carry no tracing config. |
| 78 | relay_client_tracing: |
| 79 | required: false |
| 80 | default: true |
| 81 | type: boolean |
| 82 | clerk_publishable_key: |
| 83 | required: true |
| 84 | type: string |
| 85 | clerk_jwt_template: |
| 86 | required: true |
| 87 | type: string |
| 88 | clerk_cli_oauth_client_id: |
| 89 | required: true |
| 90 | type: string |
| 91 | relay_url: |
| 92 | required: true |
| 93 | type: string |
| 94 | |
| 95 | permissions: |
| 96 | # Windows builds read this run's artifact list to wait for the Linux CLI archive. |
| 97 | actions: read |
| 98 | contents: read |
| 99 | |
| 100 | jobs: |
| 101 | build: |
| 102 | name: Build ${{ inputs.label }} |
| 103 | runs-on: ${{ inputs.runner }} |
| 104 | # Windows waits for the Linux CLI archive inside its own budget (see |
| 105 | # "Wait for Linux CLI archive"), so it gets that wait on top of the usual 30. |
| 106 | timeout-minutes: ${{ inputs.platform == 'win' && 70 || 30 }} |
| 107 | env: |
| 108 | T3CODE_CLERK_PUBLISHABLE_KEY: ${{ inputs.clerk_publishable_key }} |
| 109 | T3CODE_CLERK_JWT_TEMPLATE: ${{ inputs.clerk_jwt_template }} |
| 110 | T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ inputs.clerk_cli_oauth_client_id }} |
| 111 | T3CODE_RELAY_URL: ${{ inputs.relay_url }} |
| 112 | steps: |
| 113 | # This repository is public, so Git needs no credentials. checkout's |
| 114 | # credential cleanup runs submodule foreach even with submodules disabled, |
| 115 | # which fails on the orphaned gitlinks in our vendored .repos tree. |
| 116 | # checkout.workers=0 writes the files on every core. Alternating both ways |
| 117 | # in one job on the Windows runners, it wrote the files about 10s faster |
| 118 | # (about 40% on arm64). |
| 119 | - name: Checkout |
| 120 | shell: bash |
| 121 | env: |
| 122 | CHECKOUT_REF: ${{ inputs.ref }} |
| 123 | GIT_TERMINAL_PROMPT: "0" |
| 124 | run: | |
| 125 | set -euo pipefail |
| 126 | git init . |
| 127 | git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" |
| 128 | git fetch --no-tags --depth=1 origin "$CHECKOUT_REF" |
| 129 | git sparse-checkout set --no-cone '/*' '!/.repos/' |
| 130 | git -c checkout.workers=0 checkout --detach FETCH_HEAD |
| 131 | |
| 132 | # The runner's PowerShell 7.6/.NET 10 intermittently crashes during signing: |
| 133 | # https://github.com/dotnet/runtime/issues/131636 |
| 134 | # Put the .NET 8 release first for both workflow shells and electron-builder. |
| 135 | - name: Use PowerShell 7.4 for Windows ARM builds |
| 136 | if: inputs.platform == 'win' && inputs.arch == 'arm64' |
| 137 | shell: powershell |
| 138 | run: | |
| 139 | $ErrorActionPreference = "Stop" |
| 140 | $ProgressPreference = "SilentlyContinue" |
| 141 | $version = "7.4.20" |
| 142 | $archive = Join-Path $env:RUNNER_TEMP "PowerShell-$version-win-arm64.zip" |
| 143 | $installDir = Join-Path $env:RUNNER_TEMP "powershell-$version-arm64" |
| 144 | Invoke-WebRequest -UseBasicParsing ` |
| 145 | -Uri "https://github.com/PowerShell/PowerShell/releases/download/v$version/PowerShell-$version-win-arm64.zip" ` |
| 146 | -OutFile $archive |
| 147 | $expectedHash = "67eff749d5dae357501c95a3eec1dcd93f669824e3ca2b1d2adcdecdebf04f35" |
| 148 | if ((Get-FileHash $archive -Algorithm SHA256).Hash -ne $expectedHash) { |
| 149 | throw "PowerShell archive checksum mismatch" |
| 150 | } |
| 151 | Expand-Archive -Path $archive -DestinationPath $installDir |
| 152 | $installDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append |
| 153 | & "$installDir\pwsh.exe" -NoProfile -NonInteractive -Command '$PSVersionTable; [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture' |
| 154 | if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } |
| 155 | |
| 156 | - name: Verify Windows ARM PowerShell selection |
| 157 | if: inputs.platform == 'win' && inputs.arch == 'arm64' |
| 158 | shell: pwsh |
| 159 | run: | |
| 160 | $expectedPath = Join-Path $env:RUNNER_TEMP "powershell-7.4.20-arm64\pwsh.exe" |
| 161 | if ((Get-Command pwsh.exe).Source -ne $expectedPath -or $PSVersionTable.PSVersion -ne [version]"7.4.20") { |
| 162 | throw "Windows ARM builds must use the pinned PowerShell runtime" |
| 163 | } |
| 164 | |
| 165 | # Windows installs straight from the registry. There, linking the packages |
| 166 | # takes the time, not downloading them, so a ~700 MB package cache did not |
| 167 | # pay for its own restore. Across recent nightlies, restore plus install |
| 168 | # took 183-233s on arm64 and 116-172s on x64, while a plain install took |
| 169 | # 142-210s and 84-160s. The arm64 entries also filled 4 GB of the |
| 170 | # repository's 10 GB Actions cache. |
| 171 | - name: Setup Vite+ |
| 172 | uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 |
| 173 | with: |
| 174 | node-version-file: package.json |
| 175 | cache: ${{ inputs.platform != 'win' }} |
| 176 | run-install: false |
| 177 | |
| 178 | # pnpm checks the lockfile and policy before reusing this result. A missing |
| 179 | # artifact leaves the cache empty, so installation runs the checks again. |
| 180 | - name: Download dependency verification |
| 181 | continue-on-error: true |
| 182 | uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 183 | with: |
| 184 | name: release-dependency-verification |
| 185 | path: ${{ runner.temp }}/pnpm-metadata |
| 186 | |
| 187 | - name: Install desktop dependencies |
| 188 | env: |
| 189 | pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata |
| 190 | run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts... |
| 191 | |
| 192 | - name: Cache resource monitor |
| 193 | id: resource_monitor_cache |
| 194 | uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 |
| 195 | with: |
| 196 | path: native/resource-monitor/target/${{ inputs.rust_target }}/release/t3-resource-monitor${{ inputs.platform == 'win' && '.exe' || '' }} |
| 197 | key: resource-monitor-${{ inputs.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }} |
| 198 | |
| 199 | - name: Cache Linux capture helpers |
| 200 | if: inputs.platform == 'linux' |
| 201 | id: capture_helper_cache |
| 202 | uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 |
| 203 | with: |
| 204 | path: | |
| 205 | native/kde-snap-shot/target/${{ inputs.rust_target }}/release/t3-kde-snap-shot |
| 206 | native/hyprland-snap-shot/target/${{ inputs.rust_target }}/release/t3-hyprland-snap-shot |
| 207 | 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/**') }} |
| 208 | |
| 209 | - name: Setup Rust |
| 210 | if: steps.resource_monitor_cache.outputs.cache-hit != 'true' || (inputs.platform == 'linux' && steps.capture_helper_cache.outputs.cache-hit != 'true') |
| 211 | uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable |
| 212 | with: |
| 213 | toolchain: stable |
| 214 | targets: ${{ inputs.rust_target }} |
| 215 | |
| 216 | - name: Download relay client tracing config |
| 217 | if: inputs.relay_client_tracing |
| 218 | uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 219 | with: |
| 220 | name: relay-client-tracing-config |
| 221 | path: ${{ runner.temp }}/relay-client-tracing |
| 222 | |
| 223 | - name: Load relay client tracing config |
| 224 | if: inputs.relay_client_tracing |
| 225 | shell: bash |
| 226 | run: | |
| 227 | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" |
| 228 | tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" |
| 229 | echo "::add-mask::$tracing_token" |
| 230 | cat "$config_path" >> "$GITHUB_ENV" |
| 231 | |
| 232 | - name: Align package versions to release version |
| 233 | run: node scripts/update-release-package-versions.ts "${{ inputs.version }}" |
| 234 | |
| 235 | # The artifact root is `apps/` (upload-artifact keeps the least common |
| 236 | # ancestor of its paths), so extracting into `apps` restores |
| 237 | # apps/server/dist and apps/desktop/dist-electron at their build paths. |
| 238 | - name: Download JS bundle |
| 239 | uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 240 | with: |
| 241 | name: js-bundle |
| 242 | path: apps |
| 243 | |
| 244 | # Only the resource monitor build needs these libraries. With a cached |
| 245 | # monitor, the desktop build's preflight skips its MSVC check, and node-pty |
| 246 | # ships prebuilt Windows binaries, so the 30-60s install is skipped too. |
| 247 | - name: Install Spectre-mitigated MSVC libs |
| 248 | if: inputs.platform == 'win' && steps.resource_monitor_cache.outputs.cache-hit != 'true' |
| 249 | shell: pwsh |
| 250 | run: | |
| 251 | $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" |
| 252 | $installPath = & $vswhere -products * -latest -property installationPath |
| 253 | $setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe" |
| 254 | $proc = Start-Process -FilePath $setupExe ` |
| 255 | -ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", ` |
| 256 | "Microsoft.VisualStudio.Component.VC.Runtimes.${{ inputs.arch == 'arm64' && 'ARM64' || 'x86.x64' }}.Spectre", "--quiet", "--norestart" ` |
| 257 | -Wait -PassThru -NoNewWindow |
| 258 | if ($null -eq $proc -or $proc.ExitCode -ne 0) { |
| 259 | $code = if ($null -ne $proc) { $proc.ExitCode } else { 1 } |
| 260 | Write-Error "Visual Studio Installer failed with exit code $code" |
| 261 | exit $code |
| 262 | } |
| 263 | |
| 264 | - uses: ./.github/actions/setup-apt-mirrors |
| 265 | if: inputs.platform == 'linux' |
| 266 | |
| 267 | - name: Install Linux desktop build libraries |
| 268 | if: inputs.platform == 'linux' |
| 269 | shell: bash |
| 270 | run: | |
| 271 | sudo apt-get update |
| 272 | sudo apt-get install -y libsecret-1-dev pkg-config |
| 273 | if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then |
| 274 | sudo apt-get install -y imagemagick |
| 275 | fi |
| 276 | |
| 277 | if command -v magick >/dev/null 2>&1; then |
| 278 | magick -version |
| 279 | else |
| 280 | convert -version |
| 281 | fi |
| 282 | |
| 283 | - name: Prepare Azure Trusted Signing |
| 284 | if: inputs.platform == 'win' |
| 285 | shell: pwsh |
| 286 | env: |
| 287 | AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} |
| 288 | AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} |
| 289 | AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} |
| 290 | AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} |
| 291 | AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} |
| 292 | AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} |
| 293 | AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} |
| 294 | run: | |
| 295 | $ErrorActionPreference = "Stop" |
| 296 | |
| 297 | $requiredSecrets = @( |
| 298 | $env:AZURE_TENANT_ID, |
| 299 | $env:AZURE_CLIENT_ID, |
| 300 | $env:AZURE_CLIENT_SECRET, |
| 301 | $env:AZURE_TRUSTED_SIGNING_ENDPOINT, |
| 302 | $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, |
| 303 | $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME, |
| 304 | $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME |
| 305 | ) |
| 306 | if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) { |
| 307 | Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation." |
| 308 | exit 0 |
| 309 | } |
| 310 | |
| 311 | try { |
| 312 | Install-PackageProvider ` |
| 313 | -Name NuGet ` |
| 314 | -MinimumVersion 2.8.5.201 ` |
| 315 | -Force ` |
| 316 | -Scope CurrentUser ` |
| 317 | -ErrorAction Stop |
| 318 | } catch { |
| 319 | Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)" |
| 320 | } |
| 321 | |
| 322 | Install-Module ` |
| 323 | -Name TrustedSigning ` |
| 324 | -MinimumVersion 0.5.0 ` |
| 325 | -Force ` |
| 326 | -AllowClobber ` |
| 327 | -Repository PSGallery ` |
| 328 | -Scope CurrentUser ` |
| 329 | -ErrorAction Stop |
| 330 | |
| 331 | Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force |
| 332 | Get-Command Invoke-TrustedSigning -ErrorAction Stop |
| 333 | |
| 334 | $moduleRoots = @( |
| 335 | [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"), |
| 336 | [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"), |
| 337 | [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"), |
| 338 | [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules") |
| 339 | ) |
| 340 | $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) | |
| 341 | Where-Object { $_ -and (Test-Path $_) } | |
| 342 | Select-Object -Unique |
| 343 | "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV |
| 344 | |
| 345 | # The WSL backend runs the Linux CLI archive inside the distro, so the |
| 346 | # Windows desktop embeds the same-arch archive the release attaches. |
| 347 | # This job starts alongside that Linux job instead of after it, and the |
| 348 | # Linux job uploads the archive partway through, so the wait is placed |
| 349 | # after all other setup. It stops early if the Linux job ended without |
| 350 | # uploading the archive. |
| 351 | - name: Wait for Linux CLI archive |
| 352 | if: inputs.platform == 'win' |
| 353 | shell: pwsh |
| 354 | timeout-minutes: 40 |
| 355 | env: |
| 356 | GITHUB_TOKEN: ${{ github.token }} |
| 357 | ARTIFACT: cli-linux-${{ inputs.arch }} |
| 358 | LINUX_JOB: Build Linux ${{ inputs.arch }} |
| 359 | run: | |
| 360 | $headers = @{ Authorization = "Bearer $env:GITHUB_TOKEN"; Accept = "application/vnd.github+json" } |
| 361 | $run = "$env:GITHUB_API_URL/repos/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID" |
| 362 | function Test-Archive { |
| 363 | (Invoke-RestMethod -Headers $headers -Uri "$run/artifacts?name=$env:ARTIFACT").total_count -gt 0 |
| 364 | } |
| 365 | $missingSince = $null |
| 366 | while ($true) { |
| 367 | try { |
| 368 | if (Test-Archive) { |
| 369 | Write-Host "$env:ARTIFACT is ready." |
| 370 | exit 0 |
| 371 | } |
| 372 | $jobs = Invoke-RestMethod -Headers $headers -Uri "$run/attempts/$env:GITHUB_RUN_ATTEMPT/jobs?per_page=100" |
| 373 | $linux = $jobs.jobs | Where-Object { $_.name.EndsWith($env:LINUX_JOB) } | Select-Object -First 1 |
| 374 | if (-not $linux) { |
| 375 | # Both jobs start together, so a Linux job still missing after a |
| 376 | # few minutes means a renamed or skipped job, not a slow one. |
| 377 | if (-not $missingSince) { |
| 378 | $missingSince = Get-Date |
| 379 | } elseif (((Get-Date) - $missingSince).TotalMinutes -ge 5) { |
| 380 | Write-Host "::error::No job named '$env:LINUX_JOB' in this run, so $env:ARTIFACT will not arrive." |
| 381 | exit 1 |
| 382 | } |
| 383 | } elseif ($linux.conclusion) { |
| 384 | # The upload may have landed between the two requests above. |
| 385 | if (Test-Archive) { |
| 386 | Write-Host "$env:ARTIFACT is ready." |
| 387 | exit 0 |
| 388 | } |
| 389 | Write-Host "::error::$env:LINUX_JOB ended ($($linux.conclusion)) without uploading $env:ARTIFACT." |
| 390 | exit 1 |
| 391 | } |
| 392 | } catch { |
| 393 | Write-Warning "Could not read this run's artifacts or jobs: $($_.Exception.Message)" |
| 394 | } |
| 395 | Write-Host "Waiting for $env:ARTIFACT..." |
| 396 | Start-Sleep -Seconds 10 |
| 397 | } |
| 398 | |
| 399 | - name: Download Linux CLI archive for WSL |
| 400 | if: inputs.platform == 'win' |
| 401 | uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 |
| 402 | with: |
| 403 | name: cli-linux-${{ inputs.arch }} |
| 404 | path: wsl-runtime |
| 405 | |
| 406 | - name: Build desktop artifact |
| 407 | shell: bash |
| 408 | env: |
| 409 | pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata |
| 410 | T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }} |
| 411 | T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS: ${{ steps.capture_helper_cache.outputs.cache-hit == 'true' }} |
| 412 | CSC_LINK: ${{ secrets.CSC_LINK }} |
| 413 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} |
| 414 | APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} |
| 415 | APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} |
| 416 | APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} |
| 417 | APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} |
| 418 | MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }} |
| 419 | T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }} |
| 420 | AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} |
| 421 | AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} |
| 422 | AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} |
| 423 | AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} |
| 424 | AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} |
| 425 | AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} |
| 426 | AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} |
| 427 | run: | |
| 428 | args=( |
| 429 | --platform "${{ inputs.platform }}" |
| 430 | --target "${{ inputs.target }}" |
| 431 | --arch "${{ inputs.arch }}" |
| 432 | --build-version "${{ inputs.version }}" |
| 433 | --skip-build |
| 434 | --verbose |
| 435 | ) |
| 436 | |
| 437 | has_all() { |
| 438 | for value in "$@"; do |
| 439 | if [[ -z "$value" ]]; then |
| 440 | return 1 |
| 441 | fi |
| 442 | done |
| 443 | return 0 |
| 444 | } |
| 445 | |
| 446 | if [[ "${{ inputs.platform }}" == "mac" ]]; then |
| 447 | if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then |
| 448 | if ! has_all "$APPLE_TEAM_ID" "$MACOS_PROVISIONING_PROFILE"; then |
| 449 | echo "macOS signing is configured, but APPLE_TEAM_ID or MACOS_PROVISIONING_PROFILE is missing." >&2 |
| 450 | exit 1 |
| 451 | fi |
| 452 | |
| 453 | key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" |
| 454 | printf '%s' "$APPLE_API_KEY" > "$key_path" |
| 455 | export APPLE_API_KEY="$key_path" |
| 456 | |
| 457 | profile_path="$RUNNER_TEMP/t3code.provisionprofile" |
| 458 | printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path" |
| 459 | security cms -D -i "$profile_path" >/dev/null |
| 460 | export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID" |
| 461 | export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_path" |
| 462 | |
| 463 | echo "macOS signing enabled." |
| 464 | args+=(--signed) |
| 465 | else |
| 466 | echo "macOS signing disabled (missing one or more Apple signing secrets)." |
| 467 | fi |
| 468 | elif [[ "${{ inputs.platform }}" == "win" ]]; then |
| 469 | # Embed the Linux CLI archive built by the same-arch Linux job as |
| 470 | # the WSL runtime. Required for a working WSL backend on Windows. |
| 471 | args+=(--wsl-runtime "$GITHUB_WORKSPACE"/wsl-runtime/t3-*-linux-${{ inputs.arch }}.tar.gz) |
| 472 | if has_all \ |
| 473 | "$AZURE_TENANT_ID" \ |
| 474 | "$AZURE_CLIENT_ID" \ |
| 475 | "$AZURE_CLIENT_SECRET" \ |
| 476 | "$AZURE_TRUSTED_SIGNING_ENDPOINT" \ |
| 477 | "$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \ |
| 478 | "$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \ |
| 479 | "$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then |
| 480 | echo "Windows signing enabled (Azure Trusted Signing)." |
| 481 | args+=(--signed) |
| 482 | else |
| 483 | echo "Windows signing disabled (missing one or more Azure Trusted Signing secrets)." |
| 484 | fi |
| 485 | else |
| 486 | echo "Signing disabled for ${{ inputs.platform }}." |
| 487 | fi |
| 488 | |
| 489 | vp run dist:desktop:artifact "${args[@]}" |
| 490 | |
| 491 | # The single-executable is built with a Node that supports --build-sea |
| 492 | # (25.7+); the repo itself stays on the engines.node version. It always |
| 493 | # injects into the runner's own Node: tsdown's cross-target download path |
| 494 | # runs `tar` on a drive-letter path on Windows, which GNU tar reads as a |
| 495 | # remote host, and a cross-built macOS binary cannot be smoke-tested. |
| 496 | - name: Build CLI single-executable |
| 497 | if: inputs.cli_archive |
| 498 | shell: bash |
| 499 | env: |
| 500 | # The exact version, not a major: vp downloads it from nodejs.org/dist on |
| 501 | # the runner, and only exact versions have a dist directory. Keep in |
| 502 | # step with SEA_NODE_VERSION in apps/server/vite.config.ts. |
| 503 | VP_NODE_VERSION: "26.8.2" |
| 504 | run: node apps/server/scripts/cli.ts build-exe --verbose |
| 505 | |
| 506 | - name: Import macOS signing certificate for the CLI archive |
| 507 | if: inputs.cli_archive && inputs.platform == 'mac' |
| 508 | shell: bash |
| 509 | env: |
| 510 | CSC_LINK: ${{ secrets.CSC_LINK }} |
| 511 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} |
| 512 | run: | |
| 513 | set -euo pipefail |
| 514 | if [[ -z "$CSC_LINK" || -z "$CSC_KEY_PASSWORD" ]]; then |
| 515 | echo "macOS CLI signing disabled (missing CSC_LINK); the archive is signed ad hoc." |
| 516 | exit 0 |
| 517 | fi |
| 518 | keychain="$RUNNER_TEMP/t3-cli-signing.keychain-db" |
| 519 | keychain_password="$(openssl rand -hex 16)" |
| 520 | cert_path="$RUNNER_TEMP/t3-cli-signing.p12" |
| 521 | printf '%s' "$CSC_LINK" | base64 --decode > "$cert_path" |
| 522 | security create-keychain -p "$keychain_password" "$keychain" |
| 523 | security set-keychain-settings -lut 21600 "$keychain" |
| 524 | security unlock-keychain -p "$keychain_password" "$keychain" |
| 525 | security import "$cert_path" -k "$keychain" -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign |
| 526 | security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null |
| 527 | security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') |
| 528 | identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application: [^"]*\)".*/\1/p' | head -n 1)" |
| 529 | if [[ -z "$identity" ]]; then |
| 530 | echo "No Developer ID Application identity found in CSC_LINK." >&2 |
| 531 | exit 1 |
| 532 | fi |
| 533 | echo "::add-mask::$keychain_password" |
| 534 | echo "T3CODE_CLI_MAC_SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" |
| 535 | echo "macOS CLI signing enabled." |
| 536 | |
| 537 | - name: Stage resource monitor for the CLI archive |
| 538 | if: inputs.cli_archive |
| 539 | shell: bash |
| 540 | run: | |
| 541 | set -euo pipefail |
| 542 | binary_name="t3-resource-monitor" |
| 543 | if [[ "${{ inputs.platform }}" == "win" ]]; then |
| 544 | binary_name="${binary_name}.exe" |
| 545 | fi |
| 546 | target_dir="$RUNNER_TEMP/cli-resource-monitor/${{ inputs.resource_key }}" |
| 547 | mkdir -p "$target_dir" |
| 548 | cp "native/resource-monitor/target/${{ inputs.rust_target }}/release/${binary_name}" "$target_dir/$binary_name" |
| 549 | |
| 550 | - name: Build CLI archive |
| 551 | if: inputs.cli_archive |
| 552 | shell: bash |
| 553 | env: |
| 554 | APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} |
| 555 | APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} |
| 556 | APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} |
| 557 | AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} |
| 558 | AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} |
| 559 | AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} |
| 560 | AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} |
| 561 | AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} |
| 562 | AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} |
| 563 | run: | |
| 564 | set -euo pipefail |
| 565 | if [[ "${{ inputs.platform }}" == "mac" && -n "${APPLE_API_KEY:-}" ]]; then |
| 566 | key_path="$RUNNER_TEMP/AuthKey_cli_${APPLE_API_KEY_ID}.p8" |
| 567 | printf '%s' "$APPLE_API_KEY" > "$key_path" |
| 568 | export APPLE_API_KEY="$key_path" |
| 569 | fi |
| 570 | node scripts/build-cli-archive.ts \ |
| 571 | --platform "${{ inputs.platform }}" \ |
| 572 | --arch "${{ inputs.arch }}" \ |
| 573 | --version "${{ inputs.version }}" \ |
| 574 | --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \ |
| 575 | --output-dir release-cli |
| 576 | |
| 577 | - name: Smoke-test CLI archive |
| 578 | if: inputs.cli_archive |
| 579 | shell: bash |
| 580 | run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ inputs.version }}" |
| 581 | |
| 582 | - name: Upload CLI archive |
| 583 | if: inputs.cli_archive |
| 584 | uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 585 | with: |
| 586 | name: cli-${{ inputs.platform }}-${{ inputs.arch }} |
| 587 | path: release-cli/* |
| 588 | if-no-files-found: error |
| 589 | |
| 590 | - name: Collect release assets |
| 591 | shell: bash |
| 592 | run: | |
| 593 | set -euo pipefail |
| 594 | mkdir -p release-publish |
| 595 | |
| 596 | shopt -s nullglob |
| 597 | patterns=( |
| 598 | "release/*.dmg" |
| 599 | "release/*.zip" |
| 600 | "release/*.AppImage" |
| 601 | "release/*.deb" |
| 602 | "release/*.exe" |
| 603 | ) |
| 604 | # Preview builds have no publish config, so electron-builder writes |
| 605 | # no feed manifest for them, but it still emits blockmaps beside the |
| 606 | # installers. Neither belongs on a release no updater may follow. |
| 607 | if [[ "${{ inputs.release_channel }}" != "preview" ]]; then |
| 608 | patterns+=("release/*.blockmap" "release/*.yml") |
| 609 | fi |
| 610 | for pattern in "${patterns[@]}"; do |
| 611 | for file in $pattern; do |
| 612 | cp "$file" release-publish/ |
| 613 | done |
| 614 | done |
| 615 | |
| 616 | if [[ "${{ inputs.platform }}" == "mac" && "${{ inputs.arch }}" != "arm64" ]]; then |
| 617 | shopt -s nullglob |
| 618 | for manifest in release-publish/*-mac.yml; do |
| 619 | mv "$manifest" "${manifest%.yml}-${{ inputs.arch }}.yml" |
| 620 | done |
| 621 | fi |
| 622 | |
| 623 | # Windows updater metadata is channel-specific (for example |
| 624 | # "latest.yml" or "nightly.yml") and carries no arch, so the x64 and |
| 625 | # arm64 jobs would upload the same name. Suffix each per-arch copy; |
| 626 | # the release job merges them back into one manifest per channel. |
| 627 | # builder-debug.yml is electron-builder's config dump, not a feed. |
| 628 | if [[ "${{ inputs.platform }}" == "win" ]]; then |
| 629 | for manifest in release-publish/*.yml; do |
| 630 | [[ "$manifest" == */builder-debug.yml ]] && continue |
| 631 | mv "$manifest" "${manifest%.yml}-win-${{ inputs.arch }}.yml" |
| 632 | done |
| 633 | fi |
| 634 | |
| 635 | - name: Collect resource monitor |
| 636 | shell: bash |
| 637 | run: | |
| 638 | set -euo pipefail |
| 639 | binary_name="t3-resource-monitor" |
| 640 | if [[ "${{ inputs.platform }}" == "win" ]]; then |
| 641 | binary_name="${binary_name}.exe" |
| 642 | fi |
| 643 | source_path="native/resource-monitor/target/${{ inputs.rust_target }}/release/${binary_name}" |
| 644 | target_dir="resource-monitor-publish/${{ inputs.resource_key }}" |
| 645 | mkdir -p "$target_dir" |
| 646 | cp "$source_path" "$target_dir/$binary_name" |
| 647 | |
| 648 | - name: Upload build artifacts |
| 649 | uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 650 | with: |
| 651 | name: desktop-${{ inputs.platform }}-${{ inputs.arch }} |
| 652 | path: release-publish/* |
| 653 | if-no-files-found: error |
| 654 | |
| 655 | - name: Upload resource monitor |
| 656 | uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 |
| 657 | with: |
| 658 | name: resource-monitor-${{ inputs.resource_key }} |
| 659 | path: resource-monitor-publish/${{ inputs.resource_key }}/* |
| 660 | if-no-files-found: error |