1name: 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
9on:
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
95permissions:
96 # Windows builds read this run's artifact list to wait for the Linux CLI archive.
97 actions: read
98 contents: read
99
100jobs:
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