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

ci(release): build the JS bundle once and run every platform and architecture in parallel (#11606)

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

4 files changed, 758 insertions(+), 785 deletions(-)

.github/workflows/release-desktop.yml created+528
......@@ -0,0 +1,528 @@
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 inputs:
12 label:
13 required: true
14 type: string
15 runner:
16 required: true
17 type: string
18 platform:
19 required: true
20 type: string
21 target:
22 required: true
23 type: string
24 arch:
25 required: true
26 type: string
27 rust_target:
28 required: true
29 type: string
30 resource_key:
31 required: true
32 type: string
33 # Whether the job also builds the self-contained CLI archive for its own
34 # platform/arch, on this runner, and smoke-tests it here. Every archive
35 # is built on hardware of its own architecture.
36 cli_archive:
37 required: false
38 default: false
39 type: boolean
40 version:
41 required: true
42 type: string
43 ref:
44 required: true
45 type: string
46 release_channel:
47 required: true
48 type: string
49 clerk_publishable_key:
50 required: true
51 type: string
52 clerk_jwt_template:
53 required: true
54 type: string
55 clerk_cli_oauth_client_id:
56 required: true
57 type: string
58 relay_url:
59 required: true
60 type: string
61
62permissions:
63 contents: read
64
65jobs:
66 build:
67 name: Build ${{ inputs.label }}
68 runs-on: ${{ inputs.runner }}
69 timeout-minutes: 30
70 env:
71 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ inputs.clerk_publishable_key }}
72 T3CODE_CLERK_JWT_TEMPLATE: ${{ inputs.clerk_jwt_template }}
73 T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ inputs.clerk_cli_oauth_client_id }}
74 T3CODE_RELAY_URL: ${{ inputs.relay_url }}
75 steps:
76 - name: Checkout
77 uses: actions/checkout@v6
78 with:
79 ref: ${{ inputs.ref }}
80 sparse-checkout: |
81 /*
82 !/.repos/
83 sparse-checkout-cone-mode: false
84
85 - name: Setup Vite+
86 uses: voidzero-dev/setup-vp@v1
87 with:
88 node-version-file: package.json
89 cache: ${{ inputs.platform != 'win' }}
90 run-install: false
91
92 - name: Resolve Windows package cache path
93 if: inputs.platform == 'win'
94 id: package_cache_path
95 shell: pwsh
96 run: '"path=$(vp pm cache dir)" >> $env:GITHUB_OUTPUT'
97
98 - name: Cache Windows packages
99 if: inputs.platform == 'win'
100 uses: actions/cache@v6
101 with:
102 path: ${{ steps.package_cache_path.outputs.path }}
103 key: windows-release-packages-v1-${{ inputs.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
104
105 # pnpm checks the lockfile and policy before reusing this result. A missing
106 # artifact leaves the cache empty, so installation runs the checks again.
107 - name: Download dependency verification
108 continue-on-error: true
109 uses: actions/download-artifact@v8
110 with:
111 name: release-dependency-verification
112 path: ${{ runner.temp }}/pnpm-metadata
113
114 - name: Install desktop dependencies
115 env:
116 pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
117 run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts...
118
119 - name: Cache resource monitor
120 id: resource_monitor_cache
121 uses: actions/cache@v6
122 with:
123 path: native/resource-monitor/target/${{ inputs.rust_target }}/release/t3-resource-monitor${{ inputs.platform == 'win' && '.exe' || '' }}
124 key: resource-monitor-${{ inputs.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }}
125
126 - name: Cache Linux capture helpers
127 if: inputs.platform == 'linux'
128 id: capture_helper_cache
129 uses: actions/cache@v6
130 with:
131 path: |
132 native/kde-snap-shot/target/${{ inputs.rust_target }}/release/t3-kde-snap-shot
133 native/hyprland-snap-shot/target/${{ inputs.rust_target }}/release/t3-hyprland-snap-shot
134 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/**') }}
135
136 - name: Setup Rust
137 if: steps.resource_monitor_cache.outputs.cache-hit != 'true' || (inputs.platform == 'linux' && steps.capture_helper_cache.outputs.cache-hit != 'true')
138 uses: dtolnay/rust-toolchain@stable
139 with:
140 targets: ${{ inputs.rust_target }}
141
142 - name: Download relay client tracing config
143 uses: actions/download-artifact@v8
144 with:
145 name: relay-client-tracing-config
146 path: ${{ runner.temp }}/relay-client-tracing
147
148 - name: Load relay client tracing config
149 shell: bash
150 run: |
151 config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env"
152 tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")"
153 echo "::add-mask::$tracing_token"
154 cat "$config_path" >> "$GITHUB_ENV"
155
156 - name: Align package versions to release version
157 run: node scripts/update-release-package-versions.ts "${{ inputs.version }}"
158
159 # The artifact root is `apps/` (upload-artifact keeps the least common
160 # ancestor of its paths), so extracting into `apps` restores
161 # apps/server/dist and apps/desktop/dist-electron at their build paths.
162 - name: Download JS bundle
163 uses: actions/download-artifact@v8
164 with:
165 name: js-bundle
166 path: apps
167
168 # The WSL backend runs the Linux CLI archive inside the distro, so the
169 # Windows desktop embeds the same-arch archive the release attaches.
170 - name: Download Linux CLI archive for WSL
171 if: inputs.platform == 'win'
172 uses: actions/download-artifact@v8
173 with:
174 name: cli-linux-${{ inputs.arch }}
175 path: wsl-runtime
176
177 - name: Install Spectre-mitigated MSVC libs
178 if: inputs.platform == 'win'
179 shell: pwsh
180 run: |
181 $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
182 $installPath = & $vswhere -products * -latest -property installationPath
183 $setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe"
184 $proc = Start-Process -FilePath $setupExe `
185 -ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", `
186 "Microsoft.VisualStudio.Component.VC.Runtimes.${{ inputs.arch == 'arm64' && 'ARM64' || 'x86.x64' }}.Spectre", "--quiet", "--norestart" `
187 -Wait -PassThru -NoNewWindow
188 if ($null -eq $proc -or $proc.ExitCode -ne 0) {
189 $code = if ($null -ne $proc) { $proc.ExitCode } else { 1 }
190 Write-Error "Visual Studio Installer failed with exit code $code"
191 exit $code
192 }
193
194 - uses: ./.github/actions/setup-apt-mirrors
195 if: inputs.platform == 'linux'
196
197 - name: Install Linux desktop build libraries
198 if: inputs.platform == 'linux'
199 shell: bash
200 run: |
201 sudo apt-get update
202 sudo apt-get install -y libsecret-1-dev pkg-config
203 if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then
204 sudo apt-get install -y imagemagick
205 fi
206
207 if command -v magick >/dev/null 2>&1; then
208 magick -version
209 else
210 convert -version
211 fi
212
213 - name: Prepare Azure Trusted Signing
214 if: inputs.platform == 'win'
215 shell: pwsh
216 env:
217 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
218 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
219 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
220 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
221 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
222 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
223 AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }}
224 run: |
225 $ErrorActionPreference = "Stop"
226
227 $requiredSecrets = @(
228 $env:AZURE_TENANT_ID,
229 $env:AZURE_CLIENT_ID,
230 $env:AZURE_CLIENT_SECRET,
231 $env:AZURE_TRUSTED_SIGNING_ENDPOINT,
232 $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME,
233 $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME,
234 $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME
235 )
236 if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) {
237 Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation."
238 exit 0
239 }
240
241 try {
242 Install-PackageProvider `
243 -Name NuGet `
244 -MinimumVersion 2.8.5.201 `
245 -Force `
246 -Scope CurrentUser `
247 -ErrorAction Stop
248 } catch {
249 Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)"
250 }
251
252 Install-Module `
253 -Name TrustedSigning `
254 -MinimumVersion 0.5.0 `
255 -Force `
256 -AllowClobber `
257 -Repository PSGallery `
258 -Scope CurrentUser `
259 -ErrorAction Stop
260
261 Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force
262 Get-Command Invoke-TrustedSigning -ErrorAction Stop
263
264 $moduleRoots = @(
265 [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"),
266 [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"),
267 [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"),
268 [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules")
269 )
270 $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) |
271 Where-Object { $_ -and (Test-Path $_) } |
272 Select-Object -Unique
273 "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV
274
275 - name: Build desktop artifact
276 shell: bash
277 env:
278 pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
279 T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }}
280 T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS: ${{ steps.capture_helper_cache.outputs.cache-hit == 'true' }}
281 CSC_LINK: ${{ secrets.CSC_LINK }}
282 CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
283 APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
284 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
285 APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
286 APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
287 MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}
288 T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }}
289 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
290 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
291 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
292 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
293 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
294 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
295 AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }}
296 run: |
297 args=(
298 --platform "${{ inputs.platform }}"
299 --target "${{ inputs.target }}"
300 --arch "${{ inputs.arch }}"
301 --build-version "${{ inputs.version }}"
302 --skip-build
303 --verbose
304 )
305
306 has_all() {
307 for value in "$@"; do
308 if [[ -z "$value" ]]; then
309 return 1
310 fi
311 done
312 return 0
313 }
314
315 if [[ "${{ inputs.platform }}" == "mac" ]]; then
316 if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then
317 if ! has_all "$APPLE_TEAM_ID" "$MACOS_PROVISIONING_PROFILE"; then
318 echo "macOS signing is configured, but APPLE_TEAM_ID or MACOS_PROVISIONING_PROFILE is missing." >&2
319 exit 1
320 fi
321
322 key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
323 printf '%s' "$APPLE_API_KEY" > "$key_path"
324 export APPLE_API_KEY="$key_path"
325
326 profile_path="$RUNNER_TEMP/t3code.provisionprofile"
327 printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path"
328 security cms -D -i "$profile_path" >/dev/null
329 export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID"
330 export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_path"
331
332 echo "macOS signing enabled."
333 args+=(--signed)
334 else
335 echo "macOS signing disabled (missing one or more Apple signing secrets)."
336 fi
337 elif [[ "${{ inputs.platform }}" == "win" ]]; then
338 # Embed the Linux CLI archive built by the same-arch Linux job as
339 # the WSL runtime. Required for a working WSL backend on Windows.
340 args+=(--wsl-runtime "$GITHUB_WORKSPACE"/wsl-runtime/t3-*-linux-${{ inputs.arch }}.tar.gz)
341 if has_all \
342 "$AZURE_TENANT_ID" \
343 "$AZURE_CLIENT_ID" \
344 "$AZURE_CLIENT_SECRET" \
345 "$AZURE_TRUSTED_SIGNING_ENDPOINT" \
346 "$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \
347 "$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \
348 "$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then
349 echo "Windows signing enabled (Azure Trusted Signing)."
350 args+=(--signed)
351 else
352 echo "Windows signing disabled (missing one or more Azure Trusted Signing secrets)."
353 fi
354 else
355 echo "Signing disabled for ${{ inputs.platform }}."
356 fi
357
358 vp run dist:desktop:artifact "${args[@]}"
359
360 # The single-executable is built with a Node that supports --build-sea
361 # (25.7+); the repo itself stays on the engines.node version. It always
362 # injects into the runner's own Node: tsdown's cross-target download path
363 # runs `tar` on a drive-letter path on Windows, which GNU tar reads as a
364 # remote host, and a cross-built macOS binary cannot be smoke-tested.
365 - name: Build CLI single-executable
366 if: inputs.cli_archive
367 shell: bash
368 env:
369 # The exact version, not a major: vp downloads it from nodejs.org/dist on
370 # the runner, and only exact versions have a dist directory. Keep in
371 # step with SEA_NODE_VERSION in apps/server/vite.config.ts.
372 VP_NODE_VERSION: "26.8.2"
373 run: node apps/server/scripts/cli.ts build-exe --verbose
374
375 - name: Import macOS signing certificate for the CLI archive
376 if: inputs.cli_archive && inputs.platform == 'mac'
377 shell: bash
378 env:
379 CSC_LINK: ${{ secrets.CSC_LINK }}
380 CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
381 run: |
382 set -euo pipefail
383 if [[ -z "$CSC_LINK" || -z "$CSC_KEY_PASSWORD" ]]; then
384 echo "macOS CLI signing disabled (missing CSC_LINK); the archive is signed ad hoc."
385 exit 0
386 fi
387 keychain="$RUNNER_TEMP/t3-cli-signing.keychain-db"
388 keychain_password="$(openssl rand -hex 16)"
389 cert_path="$RUNNER_TEMP/t3-cli-signing.p12"
390 printf '%s' "$CSC_LINK" | base64 --decode > "$cert_path"
391 security create-keychain -p "$keychain_password" "$keychain"
392 security set-keychain-settings -lut 21600 "$keychain"
393 security unlock-keychain -p "$keychain_password" "$keychain"
394 security import "$cert_path" -k "$keychain" -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign
395 security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null
396 security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"')
397 identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application: [^"]*\)".*/\1/p' | head -n 1)"
398 if [[ -z "$identity" ]]; then
399 echo "No Developer ID Application identity found in CSC_LINK." >&2
400 exit 1
401 fi
402 echo "::add-mask::$keychain_password"
403 echo "T3CODE_CLI_MAC_SIGN_IDENTITY=$identity" >> "$GITHUB_ENV"
404 echo "macOS CLI signing enabled."
405
406 - name: Stage resource monitor for the CLI archive
407 if: inputs.cli_archive
408 shell: bash
409 run: |
410 set -euo pipefail
411 binary_name="t3-resource-monitor"
412 if [[ "${{ inputs.platform }}" == "win" ]]; then
413 binary_name="${binary_name}.exe"
414 fi
415 target_dir="$RUNNER_TEMP/cli-resource-monitor/${{ inputs.resource_key }}"
416 mkdir -p "$target_dir"
417 cp "native/resource-monitor/target/${{ inputs.rust_target }}/release/${binary_name}" "$target_dir/$binary_name"
418
419 - name: Build CLI archive
420 if: inputs.cli_archive
421 shell: bash
422 env:
423 APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
424 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
425 APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
426 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
427 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
428 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
429 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
430 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
431 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
432 run: |
433 set -euo pipefail
434 if [[ "${{ inputs.platform }}" == "mac" && -n "${APPLE_API_KEY:-}" ]]; then
435 key_path="$RUNNER_TEMP/AuthKey_cli_${APPLE_API_KEY_ID}.p8"
436 printf '%s' "$APPLE_API_KEY" > "$key_path"
437 export APPLE_API_KEY="$key_path"
438 fi
439 node scripts/build-cli-archive.ts \
440 --platform "${{ inputs.platform }}" \
441 --arch "${{ inputs.arch }}" \
442 --version "${{ inputs.version }}" \
443 --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \
444 --output-dir release-cli
445
446 - name: Smoke-test CLI archive
447 if: inputs.cli_archive
448 shell: bash
449 run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ inputs.version }}"
450
451 - name: Upload CLI archive
452 if: inputs.cli_archive
453 uses: actions/upload-artifact@v7
454 with:
455 name: cli-${{ inputs.platform }}-${{ inputs.arch }}
456 path: release-cli/*
457 if-no-files-found: error
458
459 - name: Collect release assets
460 shell: bash
461 run: |
462 set -euo pipefail
463 mkdir -p release-publish
464
465 shopt -s nullglob
466 patterns=(
467 "release/*.dmg"
468 "release/*.zip"
469 "release/*.AppImage"
470 "release/*.exe"
471 )
472 # Preview builds have no publish config, so electron-builder writes
473 # no feed manifest for them, but it still emits blockmaps beside the
474 # installers. Neither belongs on a release no updater may follow.
475 if [[ "${{ inputs.release_channel }}" != "preview" ]]; then
476 patterns+=("release/*.blockmap" "release/*.yml")
477 fi
478 for pattern in "${patterns[@]}"; do
479 for file in $pattern; do
480 cp "$file" release-publish/
481 done
482 done
483
484 if [[ "${{ inputs.platform }}" == "mac" && "${{ inputs.arch }}" != "arm64" ]]; then
485 shopt -s nullglob
486 for manifest in release-publish/*-mac.yml; do
487 mv "$manifest" "${manifest%.yml}-${{ inputs.arch }}.yml"
488 done
489 fi
490
491 # Windows updater metadata is channel-specific (for example
492 # "latest.yml" or "nightly.yml") and carries no arch, so the x64 and
493 # arm64 jobs would upload the same name. Suffix each per-arch copy;
494 # the release job merges them back into one manifest per channel.
495 # builder-debug.yml is electron-builder's config dump, not a feed.
496 if [[ "${{ inputs.platform }}" == "win" ]]; then
497 for manifest in release-publish/*.yml; do
498 [[ "$manifest" == */builder-debug.yml ]] && continue
499 mv "$manifest" "${manifest%.yml}-win-${{ inputs.arch }}.yml"
500 done
501 fi
502
503 - name: Collect resource monitor
504 shell: bash
505 run: |
506 set -euo pipefail
507 binary_name="t3-resource-monitor"
508 if [[ "${{ inputs.platform }}" == "win" ]]; then
509 binary_name="${binary_name}.exe"
510 fi
511 source_path="native/resource-monitor/target/${{ inputs.rust_target }}/release/${binary_name}"
512 target_dir="resource-monitor-publish/${{ inputs.resource_key }}"
513 mkdir -p "$target_dir"
514 cp "$source_path" "$target_dir/$binary_name"
515
516 - name: Upload build artifacts
517 uses: actions/upload-artifact@v7
518 with:
519 name: desktop-${{ inputs.platform }}-${{ inputs.arch }}
520 path: release-publish/*
521 if-no-files-found: error
522
523 - name: Upload resource monitor
524 uses: actions/upload-artifact@v7
525 with:
526 name: resource-monitor-${{ inputs.resource_key }}
527 path: resource-monitor-publish/${{ inputs.resource_key }}/*
528 if-no-files-found: error
.github/workflows/release.yml+219-776
......@@ -348,36 +348,18 @@ jobs:
348348 echo "clerk_cli_oauth_client_id=$CLERK_CLI_OAUTH_CLIENT_ID" >> "$GITHUB_OUTPUT"
349349 echo "relay_url=https://$relay_domain" >> "$GITHUB_OUTPUT"
350350
351 # node-pty publishes no Linux prebuilt and the WSL backend runs under the
352 # distro's own (Linux) Node, which can't load the Windows/Electron binary. We
353 # build the Linux pty.node here, on Linux, and hand it to the Windows packaging
354 # job — the Windows artifact then ships a ready WSL backend binary with no
355 # cross-compiling and no first-launch compiler/node-gyp/network on the user's
356 # machine. node-pty is N-API, so one binary works across all WSL Node versions.
357 # The Linux CLI archive is built ahead of the desktop matrix because two
358 # consumers need it: the Linux desktop entry attaches it to the release, and
359 # the Windows desktop entry embeds it as the WSL runtime. Building it once
360 # here means the WSL backend runs the exact bytes a Linux user downloads.
361 build_linux_cli:
362 name: Build CLI archive (linux-${{ matrix.arch }})
351 # The platform-independent JS (server bundle, web client, Electron main) is
352 # built exactly once here and handed to every platform job as `js-bundle`.
353 # The relay/Clerk values are baked into the bundle, so they belong to this
354 # job rather than to the packaging jobs.
355 build_bundle:
356 name: Build JS bundle
363357 # Same gating as relay_public_config: only the release commit is needed, so
364358 # this runs alongside preflight. See the condition comment there.
365 needs: [resolve_commit, preflight, relay_public_config]
359 needs: [preflight, relay_public_config]
366360 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
367 runs-on: ${{ matrix.runner }}
361 runs-on: blacksmith-32vcpu-ubuntu-2404
368362 timeout-minutes: 30
369 strategy:
370 fail-fast: false
371 matrix:
372 include:
373 - arch: x64
374 runner: blacksmith-32vcpu-ubuntu-2404
375 rust_target: x86_64-unknown-linux-gnu
376 # node-pty has no Linux prebuild and compiles from source, so the
377 # arm64 archive is built on arm64 hardware rather than cross-built.
378 - arch: arm64
379 runner: ubuntu-24.04-arm
380 rust_target: aarch64-unknown-linux-gnu
381363 env:
382364 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
383365 T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
......@@ -398,358 +380,8 @@ jobs:
398380 with:
399381 node-version-file: package.json
400382 cache: true
401 run-install: |
402 args:
403 - --filter=t3...
404 - --filter=@t3tools/web...
405 - --filter=@t3tools/scripts...
406
407 - name: Cache resource monitor
408 id: resource_monitor_cache
409 uses: actions/cache@v6
410 with:
411 path: native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor
412 key: resource-monitor-${{ matrix.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }}
413
414 - name: Setup Rust
415 if: steps.resource_monitor_cache.outputs.cache-hit != 'true'
416 uses: dtolnay/rust-toolchain@stable
417 with:
418 targets: ${{ matrix.rust_target }}
419
420 - name: Build resource monitor
421 if: steps.resource_monitor_cache.outputs.cache-hit != 'true'
422 run: cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml --target ${{ matrix.rust_target }}
423
424 - name: Download relay client tracing config
425 uses: actions/download-artifact@v8
426 with:
427 name: relay-client-tracing-config
428 path: ${{ runner.temp }}/relay-client-tracing
429
430 - name: Load relay client tracing config
431 shell: bash
432 run: |
433 config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env"
434 tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")"
435 echo "::add-mask::$tracing_token"
436 cat "$config_path" >> "$GITHUB_ENV"
437
438 - name: Align package versions to release version
439 run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
440
441 # The t3 build task depends on @t3tools/web#build, so the web client is
442 # built as part of this step.
443 - name: Build CLI package
444 run: vp run --filter t3 build
445
446 - name: Build CLI single-executable
447 env:
448 # The exact version, not a major: vp downloads it from nodejs.org/dist on
449 # the runner, and only exact versions have a dist directory. Keep in
450 # step with SEA_NODE_VERSION in apps/server/vite.config.ts.
451 VP_NODE_VERSION: "26.8.2"
452 run: node apps/server/scripts/cli.ts build-exe --verbose
453
454 - name: Stage resource monitor for the CLI archive
455 run: |
456 set -euo pipefail
457 target_dir="$RUNNER_TEMP/cli-resource-monitor/linux-${{ matrix.arch }}"
458 mkdir -p "$target_dir"
459 cp native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor "$target_dir/"
460
461 - name: Build CLI archive
462 run: |
463 node scripts/build-cli-archive.ts \
464 --platform linux \
465 --arch ${{ matrix.arch }} \
466 --version "${{ needs.preflight.outputs.version }}" \
467 --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \
468 --output-dir release-cli
469
470 - name: Smoke-test CLI archive
471 run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ needs.preflight.outputs.version }}"
472
473 - name: Upload CLI archive
474 uses: actions/upload-artifact@v7
475 with:
476 name: cli-linux-${{ matrix.arch }}
477 path: release-cli/*
478 if-no-files-found: error
479
480 # Windows arm64 has no desktop build yet (the NSIS arm64 row is still off),
481 # but the CLI archive is built here on arm64 hardware so it is signed and
482 # smoke-tested on the architecture it targets, like every other archive.
483 build_windows_arm64_cli:
484 name: Build CLI archive (win32-arm64)
485 needs: [resolve_commit, preflight, relay_public_config]
486 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
487 runs-on: windows-11-arm
488 timeout-minutes: 30
489 env:
490 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
491 T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
492 T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
493 T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
494 steps:
495 - name: Checkout
496 uses: actions/checkout@v6
497 with:
498 ref: ${{ needs.preflight.outputs.ref }}
499 sparse-checkout: |
500 /*
501 !/.repos/
502 sparse-checkout-cone-mode: false
503
504 - name: Setup Vite+
505 uses: voidzero-dev/setup-vp@v1
506 with:
507 node-version-file: package.json
508 cache: false
509383 run-install: false
510384
511 - name: Download dependency verification
512 continue-on-error: true
513 uses: actions/download-artifact@v8
514 with:
515 name: release-dependency-verification
516 path: ${{ runner.temp }}/pnpm-metadata
517
518 - name: Install dependencies
519 env:
520 pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
521 run: vp install --filter=t3... --filter=@t3tools/web... --filter=@t3tools/scripts...
522
523 - name: Setup Rust
524 uses: dtolnay/rust-toolchain@stable
525 with:
526 targets: aarch64-pc-windows-msvc
527
528 - name: Build resource monitor
529 run: cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml --target aarch64-pc-windows-msvc
530
531 - name: Download relay client tracing config
532 uses: actions/download-artifact@v8
533 with:
534 name: relay-client-tracing-config
535 path: ${{ runner.temp }}/relay-client-tracing
536
537 - name: Load relay client tracing config
538 shell: bash
539 run: |
540 config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env"
541 tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")"
542 echo "::add-mask::$tracing_token"
543 cat "$config_path" >> "$GITHUB_ENV"
544
545 - name: Align package versions to release version
546 run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
547
548 # The t3 build task depends on @t3tools/web#build, so the web client is
549 # built as part of this step.
550 - name: Build CLI package
551 run: vp run --filter t3 build
552
553 - name: Build CLI single-executable
554 shell: bash
555 env:
556 VP_NODE_VERSION: "26.8.2"
557 run: node apps/server/scripts/cli.ts build-exe --verbose
558
559 - name: Prepare Azure Trusted Signing
560 shell: pwsh
561 env:
562 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
563 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
564 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
565 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
566 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
567 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
568 AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }}
569 run: |
570 $ErrorActionPreference = "Stop"
571
572 $requiredSecrets = @(
573 $env:AZURE_TENANT_ID,
574 $env:AZURE_CLIENT_ID,
575 $env:AZURE_CLIENT_SECRET,
576 $env:AZURE_TRUSTED_SIGNING_ENDPOINT,
577 $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME,
578 $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME,
579 $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME
580 )
581 if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) {
582 Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation."
583 exit 0
584 }
585
586 try {
587 Install-PackageProvider `
588 -Name NuGet `
589 -MinimumVersion 2.8.5.201 `
590 -Force `
591 -Scope CurrentUser `
592 -ErrorAction Stop
593 } catch {
594 Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)"
595 }
596
597 Install-Module `
598 -Name TrustedSigning `
599 -MinimumVersion 0.5.0 `
600 -Force `
601 -AllowClobber `
602 -Repository PSGallery `
603 -Scope CurrentUser `
604 -ErrorAction Stop
605
606 Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force
607 Get-Command Invoke-TrustedSigning -ErrorAction Stop
608
609 $moduleRoots = @(
610 [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"),
611 [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"),
612 [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"),
613 [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules")
614 )
615 $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) |
616 Where-Object { $_ -and (Test-Path $_) } |
617 Select-Object -Unique
618 "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV
619
620 - name: Stage resource monitor for the CLI archive
621 shell: bash
622 run: |
623 set -euo pipefail
624 target_dir="$RUNNER_TEMP/cli-resource-monitor/win32-arm64"
625 mkdir -p "$target_dir"
626 cp native/resource-monitor/target/aarch64-pc-windows-msvc/release/t3-resource-monitor.exe "$target_dir/"
627
628 - name: Build CLI archive
629 shell: bash
630 env:
631 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
632 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
633 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
634 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
635 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
636 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
637 run: |
638 node scripts/build-cli-archive.ts \
639 --platform win \
640 --arch arm64 \
641 --version "${{ needs.preflight.outputs.version }}" \
642 --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \
643 --output-dir release-cli
644
645 - name: Smoke-test CLI archive
646 shell: bash
647 run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ needs.preflight.outputs.version }}"
648
649 - name: Upload CLI archive
650 uses: actions/upload-artifact@v7
651 with:
652 name: cli-win-arm64
653 path: release-cli/*
654 if-no-files-found: error
655
656 build:
657 name: Build ${{ matrix.label }}
658 # build_linux_cli stays in `needs` so it runs first and its artifact is
659 # available to download, but only the Windows matrix entry consumes it (as
660 # the WSL runtime). The job is gated on preflight + relay WITHOUT requiring
661 # build_linux_cli, so a failed Linux archive doesn't skip the macOS builds.
662 # `!cancelled()` (not `!failure()`) lets the job run even when
663 # build_linux_cli failed; the Windows-only download step below then fails
664 # that single platform if the archive is missing.
665 needs: [preflight, relay_public_config, build_linux_cli]
666 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
667 runs-on: ${{ matrix.runner }}
668 timeout-minutes: 30
669 env:
670 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
671 T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
672 T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
673 T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
674 strategy:
675 fail-fast: false
676 matrix:
677 include:
678 # cli_archive: whether the job also builds the self-contained CLI
679 # archive for its own platform/arch, on this runner, and smoke-tests
680 # it here. Every archive is built on hardware of its own
681 # architecture: Linux and Windows arm64 have their own jobs below.
682 - label: macOS arm64
683 runner: blacksmith-12vcpu-macos-26
684 platform: mac
685 target: dmg
686 arch: arm64
687 rust_target: aarch64-apple-darwin
688 resource_key: darwin-arm64
689 cli_archive: true
690 # No CLI archive: Node single-executables are unsupported on x64
691 # macOS (the SEA docs list macOS as arm64 only) and the built binary
692 # segfaults on start. The x64 desktop app is Electron and unaffected.
693 - label: macOS x64
694 runner: blacksmith-12vcpu-macos-26
695 platform: mac
696 target: dmg
697 arch: x64
698 rust_target: x86_64-apple-darwin
699 resource_key: darwin-x64
700 cli_archive: false
701 # The Linux CLI archive is produced by build_linux_cli, not here.
702 - label: Linux x64
703 runner: blacksmith-32vcpu-ubuntu-2404
704 platform: linux
705 target: AppImage
706 arch: x64
707 rust_target: x86_64-unknown-linux-gnu
708 resource_key: linux-x64
709 cli_archive: false
710 - label: Windows x64
711 runner: blacksmith-32vcpu-windows-2025
712 platform: win
713 target: nsis
714 arch: x64
715 rust_target: x86_64-pc-windows-msvc
716 resource_key: win32-x64
717 cli_archive: true
718 # - label: Windows arm64
719 # runner: windows-11-arm
720 # platform: win
721 # target: nsis
722 # arch: arm64
723 steps:
724 - name: Checkout
725 uses: actions/checkout@v6
726 with:
727 ref: ${{ needs.preflight.outputs.ref }}
728 sparse-checkout: |
729 /*
730 !/.repos/
731 sparse-checkout-cone-mode: false
732
733 - name: Setup Vite+
734 uses: voidzero-dev/setup-vp@v1
735 with:
736 node-version-file: package.json
737 cache: ${{ matrix.platform != 'win' }}
738 run-install: false
739
740 - name: Resolve Windows package cache path
741 if: matrix.platform == 'win'
742 id: package_cache_path
743 shell: pwsh
744 run: '"path=$(vp pm cache dir)" >> $env:GITHUB_OUTPUT'
745
746 - name: Cache Windows packages
747 if: matrix.platform == 'win'
748 uses: actions/cache@v6
749 with:
750 path: ${{ steps.package_cache_path.outputs.path }}
751 key: windows-release-packages-v1-${{ matrix.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
752
753385 # pnpm checks the lockfile and policy before reusing this result. A missing
754386 # artifact leaves the cache empty, so installation runs the checks again.
755387 - name: Download dependency verification
......@@ -759,33 +391,10 @@ jobs:
759391 name: release-dependency-verification
760392 path: ${{ runner.temp }}/pnpm-metadata
761393
762 - name: Install desktop dependencies
394 - name: Install bundle dependencies
763395 env:
764396 pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
765 run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts...
766
767 - name: Cache resource monitor
768 id: resource_monitor_cache
769 uses: actions/cache@v6
770 with:
771 path: native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor${{ matrix.platform == 'win' && '.exe' || '' }}
772 key: resource-monitor-${{ matrix.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }}
773
774 - name: Cache Linux capture helpers
775 if: matrix.platform == 'linux'
776 id: capture_helper_cache
777 uses: actions/cache@v6
778 with:
779 path: |
780 native/kde-snap-shot/target/${{ matrix.rust_target }}/release/t3-kde-snap-shot
781 native/hyprland-snap-shot/target/${{ matrix.rust_target }}/release/t3-hyprland-snap-shot
782 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/**') }}
783
784 - name: Setup Rust
785 if: steps.resource_monitor_cache.outputs.cache-hit != 'true' || (matrix.platform == 'linux' && steps.capture_helper_cache.outputs.cache-hit != 'true')
786 uses: dtolnay/rust-toolchain@stable
787 with:
788 targets: ${{ matrix.rust_target }}
397 run: vp install --filter=t3... --filter=@t3tools/web... --filter=@t3tools/desktop... --filter=@t3tools/scripts...
789398
790399 - name: Download relay client tracing config
791400 uses: actions/download-artifact@v8
......@@ -804,373 +413,190 @@ jobs:
804413 - name: Align package versions to release version
805414 run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
806415
807 # The WSL backend runs the Linux CLI archive inside the distro, so the
808 # Windows desktop embeds the same archive the release attaches.
809 - name: Download Linux CLI archive for WSL
810 if: matrix.platform == 'win'
811 uses: actions/download-artifact@v8
812 with:
813 name: cli-linux-x64
814 path: wsl-runtime
815
816 - name: Install Spectre-mitigated MSVC libs
817 if: matrix.platform == 'win'
818 shell: pwsh
819 run: |
820 $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
821 $installPath = & $vswhere -products * -latest -property installationPath
822 $setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe"
823 $proc = Start-Process -FilePath $setupExe `
824 -ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", `
825 "Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre", "--quiet", "--norestart" `
826 -Wait -PassThru -NoNewWindow
827 if ($null -eq $proc -or $proc.ExitCode -ne 0) {
828 $code = if ($null -ne $proc) { $proc.ExitCode } else { 1 }
829 Write-Error "Visual Studio Installer failed with exit code $code"
830 exit $code
831 }
832
833416 - uses: ./.github/actions/setup-apt-mirrors
834 if: matrix.platform == 'linux'
835
836 - name: Install Linux desktop build libraries
837 if: matrix.platform == 'linux'
838 shell: bash
839 run: |
840 sudo apt-get update
841 sudo apt-get install -y libsecret-1-dev pkg-config
842 if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then
843 sudo apt-get install -y imagemagick
844 fi
845
846 if command -v magick >/dev/null 2>&1; then
847 magick -version
848 else
849 convert -version
850 fi
851
852 - name: Prepare Azure Trusted Signing
853 if: matrix.platform == 'win'
854 shell: pwsh
855 env:
856 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
857 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
858 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
859 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
860 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
861 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
862 AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }}
863 run: |
864 $ErrorActionPreference = "Stop"
865
866 $requiredSecrets = @(
867 $env:AZURE_TENANT_ID,
868 $env:AZURE_CLIENT_ID,
869 $env:AZURE_CLIENT_SECRET,
870 $env:AZURE_TRUSTED_SIGNING_ENDPOINT,
871 $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME,
872 $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME,
873 $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME
874 )
875 if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) {
876 Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation."
877 exit 0
878 }
879
880 try {
881 Install-PackageProvider `
882 -Name NuGet `
883 -MinimumVersion 2.8.5.201 `
884 -Force `
885 -Scope CurrentUser `
886 -ErrorAction Stop
887 } catch {
888 Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)"
889 }
890
891 Install-Module `
892 -Name TrustedSigning `
893 -MinimumVersion 0.5.0 `
894 -Force `
895 -AllowClobber `
896 -Repository PSGallery `
897 -Scope CurrentUser `
898 -ErrorAction Stop
899
900 Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force
901 Get-Command Invoke-TrustedSigning -ErrorAction Stop
902
903 $moduleRoots = @(
904 [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"),
905 [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"),
906 [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"),
907 [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules")
908 )
909 $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) |
910 Where-Object { $_ -and (Test-Path $_) } |
911 Select-Object -Unique
912 "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV
913
914 - name: Build desktop artifact
915 shell: bash
916 env:
917 pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
918 T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }}
919 T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS: ${{ steps.capture_helper_cache.outputs.cache-hit == 'true' }}
920 CSC_LINK: ${{ secrets.CSC_LINK }}
921 CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
922 APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
923 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
924 APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
925 APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
926 MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}
927 T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }}
928 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
929 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
930 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
931 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
932 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
933 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
934 AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }}
935 run: |
936 args=(
937 --platform "${{ matrix.platform }}"
938 --target "${{ matrix.target }}"
939 --arch "${{ matrix.arch }}"
940 --build-version "${{ needs.preflight.outputs.version }}"
941 --verbose
942 )
943
944 has_all() {
945 for value in "$@"; do
946 if [[ -z "$value" ]]; then
947 return 1
948 fi
949 done
950 return 0
951 }
952
953 if [[ "${{ matrix.platform }}" == "mac" ]]; then
954 if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then
955 if ! has_all "$APPLE_TEAM_ID" "$MACOS_PROVISIONING_PROFILE"; then
956 echo "macOS signing is configured, but APPLE_TEAM_ID or MACOS_PROVISIONING_PROFILE is missing." >&2
957 exit 1
958 fi
959
960 key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
961 printf '%s' "$APPLE_API_KEY" > "$key_path"
962 export APPLE_API_KEY="$key_path"
963
964 profile_path="$RUNNER_TEMP/t3code.provisionprofile"
965 printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path"
966 security cms -D -i "$profile_path" >/dev/null
967 export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID"
968 export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_path"
969
970 echo "macOS signing enabled."
971 args+=(--signed)
972 else
973 echo "macOS signing disabled (missing one or more Apple signing secrets)."
974 fi
975 elif [[ "${{ matrix.platform }}" == "win" ]]; then
976 # Embed the Linux CLI archive built by build_linux_cli as the WSL
977 # runtime. Required for a working WSL backend on Windows.
978 args+=(--wsl-runtime "$GITHUB_WORKSPACE"/wsl-runtime/t3-*-linux-x64.tar.gz)
979 if has_all \
980 "$AZURE_TENANT_ID" \
981 "$AZURE_CLIENT_ID" \
982 "$AZURE_CLIENT_SECRET" \
983 "$AZURE_TRUSTED_SIGNING_ENDPOINT" \
984 "$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \
985 "$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \
986 "$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then
987 echo "Windows signing enabled (Azure Trusted Signing)."
988 args+=(--signed)
989 else
990 echo "Windows signing disabled (missing one or more Azure Trusted Signing secrets)."
991 fi
992 else
993 echo "Signing disabled for ${{ matrix.platform }}."
994 fi
995
996 vp run dist:desktop:artifact "${args[@]}"
997
998 # The single-executable is built with a Node that supports --build-sea
999 # (25.7+); the repo itself stays on the engines.node version. It always
1000 # injects into the runner's own Node: tsdown's cross-target download path
1001 # runs `tar` on a drive-letter path on Windows, which GNU tar reads as a
1002 # remote host, and a cross-built macOS binary cannot be smoke-tested.
1003 - name: Build CLI single-executable
1004 if: matrix.cli_archive
1005 shell: bash
1006 env:
1007 # The exact version, not a major: vp downloads it from nodejs.org/dist on
1008 # the runner, and only exact versions have a dist directory. Keep in
1009 # step with SEA_NODE_VERSION in apps/server/vite.config.ts.
1010 VP_NODE_VERSION: "26.8.2"
1011 run: node apps/server/scripts/cli.ts build-exe --verbose
1012
1013 - name: Import macOS signing certificate for the CLI archive
1014 if: matrix.cli_archive && matrix.platform == 'mac'
1015 shell: bash
1016 env:
1017 CSC_LINK: ${{ secrets.CSC_LINK }}
1018 CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
1019 run: |
1020 set -euo pipefail
1021 if [[ -z "$CSC_LINK" || -z "$CSC_KEY_PASSWORD" ]]; then
1022 echo "macOS CLI signing disabled (missing CSC_LINK); the archive is signed ad hoc."
1023 exit 0
1024 fi
1025 keychain="$RUNNER_TEMP/t3-cli-signing.keychain-db"
1026 keychain_password="$(openssl rand -hex 16)"
1027 cert_path="$RUNNER_TEMP/t3-cli-signing.p12"
1028 printf '%s' "$CSC_LINK" | base64 --decode > "$cert_path"
1029 security create-keychain -p "$keychain_password" "$keychain"
1030 security set-keychain-settings -lut 21600 "$keychain"
1031 security unlock-keychain -p "$keychain_password" "$keychain"
1032 security import "$cert_path" -k "$keychain" -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign
1033 security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null
1034 security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"')
1035 identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application: [^"]*\)".*/\1/p' | head -n 1)"
1036 if [[ -z "$identity" ]]; then
1037 echo "No Developer ID Application identity found in CSC_LINK." >&2
1038 exit 1
1039 fi
1040 echo "::add-mask::$keychain_password"
1041 echo "T3CODE_CLI_MAC_SIGN_IDENTITY=$identity" >> "$GITHUB_ENV"
1042 echo "macOS CLI signing enabled."
1043
1044 - name: Stage resource monitor for the CLI archive
1045 if: matrix.cli_archive
1046 shell: bash
1047 run: |
1048 set -euo pipefail
1049 binary_name="t3-resource-monitor"
1050 if [[ "${{ matrix.platform }}" == "win" ]]; then
1051 binary_name="${binary_name}.exe"
1052 fi
1053 target_dir="$RUNNER_TEMP/cli-resource-monitor/${{ matrix.resource_key }}"
1054 mkdir -p "$target_dir"
1055 cp "native/resource-monitor/target/${{ matrix.rust_target }}/release/${binary_name}" "$target_dir/$binary_name"
1056417
1057 - name: Build CLI archive
1058 if: matrix.cli_archive
1059 shell: bash
1060 env:
1061 APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
1062 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
1063 APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
1064 AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
1065 AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
1066 AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
1067 AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
1068 AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
1069 AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
1070 run: |
1071 set -euo pipefail
1072 if [[ "${{ matrix.platform }}" == "mac" && -n "${APPLE_API_KEY:-}" ]]; then
1073 key_path="$RUNNER_TEMP/AuthKey_cli_${APPLE_API_KEY_ID}.p8"
1074 printf '%s' "$APPLE_API_KEY" > "$key_path"
1075 export APPLE_API_KEY="$key_path"
1076 fi
1077 node scripts/build-cli-archive.ts \
1078 --platform "${{ matrix.platform }}" \
1079 --arch "${{ matrix.arch }}" \
1080 --version "${{ needs.preflight.outputs.version }}" \
1081 --resource-monitor-dir "$RUNNER_TEMP/cli-resource-monitor" \
1082 --output-dir release-cli
1083
1084 - name: Smoke-test CLI archive
1085 if: matrix.cli_archive
1086 shell: bash
1087 run: node scripts/smoke-cli-archive.ts --archive release-cli/* --expect-version "${{ needs.preflight.outputs.version }}"
1088
1089 - name: Upload CLI archive
1090 if: matrix.cli_archive
1091 uses: actions/upload-artifact@v7
1092 with:
1093 name: cli-${{ matrix.platform }}-${{ matrix.arch }}
1094 path: release-cli/*
1095 if-no-files-found: error
1096
1097 - name: Collect release assets
1098 shell: bash
1099 run: |
1100 set -euo pipefail
1101 mkdir -p release-publish
1102
1103 shopt -s nullglob
1104 patterns=(
1105 "release/*.dmg"
1106 "release/*.zip"
1107 "release/*.AppImage"
1108 "release/*.exe"
1109 )
1110 # Preview builds have no publish config, so electron-builder writes
1111 # no feed manifest for them, but it still emits blockmaps beside the
1112 # installers. Neither belongs on a release no updater may follow.
1113 if [[ "${{ needs.preflight.outputs.release_channel }}" != "preview" ]]; then
1114 patterns+=("release/*.blockmap" "release/*.yml")
1115 fi
1116 for pattern in "${patterns[@]}"; do
1117 for file in $pattern; do
1118 cp "$file" release-publish/
1119 done
1120 done
1121
1122 if [[ "${{ matrix.platform }}" == "mac" && "${{ matrix.arch }}" != "arm64" ]]; then
1123 shopt -s nullglob
1124 for manifest in release-publish/*-mac.yml; do
1125 mv "$manifest" "${manifest%.yml}-${{ matrix.arch }}.yml"
1126 done
1127 fi
418 # @t3tools/desktop#build compiles the Linux browser secret helper on a
419 # Linux host before packing, and that needs libsecret headers.
420 - name: Install browser secret helper build libraries
421 run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
1128422
1129 # Enable if Windows arm64 builds are enabled.
1130 # Windows updater metadata is channel-specific (for example
1131 # "latest.yml" or "nightly.yml"). Suffix each per-arch copy so the
1132 # release job can merge matching arm64/x64 manifests back into one
1133 # canonical manifest per channel.
1134 # if [[ "${{ matrix.platform }}" == "win" ]]; then
1135 # shopt -s nullglob
1136 # for manifest in release-publish/*.yml; do
1137 # mv "$manifest" "${manifest%.yml}-win-${{ matrix.arch }}.yml"
1138 # done
1139 # fi
1140
1141 - name: Collect resource monitor
1142 shell: bash
1143 run: |
1144 set -euo pipefail
1145 binary_name="t3-resource-monitor"
1146 if [[ "${{ matrix.platform }}" == "win" ]]; then
1147 binary_name="${binary_name}.exe"
1148 fi
1149 source_path="native/resource-monitor/target/${{ matrix.rust_target }}/release/${binary_name}"
1150 target_dir="resource-monitor-publish/${{ matrix.resource_key }}"
1151 mkdir -p "$target_dir"
1152 cp "$source_path" "$target_dir/$binary_name"
423 # Runs t3#build (which depends on @t3tools/web#build) and
424 # @t3tools/desktop#build, so apps/server/dist holds the server bundle
425 # plus the web client and apps/desktop/dist-electron the Electron main.
426 - name: Build JS bundle
427 run: vp run build:desktop
1153428
1154 - name: Upload build artifacts
429 # Two paths under apps/ so the artifact root is apps/; consumers download
430 # into `apps` to restore both at their original locations.
431 - name: Upload JS bundle
1155432 uses: actions/upload-artifact@v7
1156433 with:
1157 name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
1158 path: release-publish/*
434 name: js-bundle
435 path: |
436 apps/server/dist
437 apps/desktop/dist-electron
1159438 if-no-files-found: error
439 retention-days: 1
1160440
1161 - name: Upload resource monitor
1162 uses: actions/upload-artifact@v7
1163 with:
1164 name: resource-monitor-${{ matrix.resource_key }}
1165 path: resource-monitor-publish/${{ matrix.resource_key }}/*
1166 if-no-files-found: error
441 # One job per platform and architecture (see release-desktop.yml), each on
442 # hardware of its own architecture, and each gated only on what it consumes:
443 # every platform needs the JS bundle, and the Windows jobs also need the
444 # same-arch Linux job, whose CLI archive they embed as the WSL runtime. Every
445 # job builds the desktop app; all but macOS x64 also build the CLI archive
446 # for their platform, so a target either ships fully or not at all.
447 desktop_mac_arm64:
448 name: Desktop macOS arm64
449 needs: [preflight, relay_public_config, build_bundle]
450 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}
451 uses: ./.github/workflows/release-desktop.yml
452 secrets: inherit
453 with:
454 version: ${{ needs.preflight.outputs.version }}
455 ref: ${{ needs.preflight.outputs.ref }}
456 release_channel: ${{ needs.preflight.outputs.release_channel }}
457 clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
458 clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
459 clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
460 relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
461 label: macOS arm64
462 runner: blacksmith-12vcpu-macos-26
463 platform: mac
464 target: dmg
465 arch: arm64
466 rust_target: aarch64-apple-darwin
467 resource_key: darwin-arm64
468 cli_archive: true
469
470 desktop_mac_x64:
471 name: Desktop macOS x64
472 needs: [preflight, relay_public_config, build_bundle]
473 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}
474 uses: ./.github/workflows/release-desktop.yml
475 secrets: inherit
476 with:
477 version: ${{ needs.preflight.outputs.version }}
478 ref: ${{ needs.preflight.outputs.ref }}
479 release_channel: ${{ needs.preflight.outputs.release_channel }}
480 clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
481 clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
482 clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
483 relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
484 label: macOS x64
485 runner: blacksmith-12vcpu-macos-26
486 platform: mac
487 target: dmg
488 arch: x64
489 rust_target: x86_64-apple-darwin
490 resource_key: darwin-x64
491 # No CLI archive: Node single-executables are unsupported on x64 macOS
492 # (the SEA docs list macOS as arm64 only) and the built binary segfaults
493 # on start. The x64 desktop app is Electron and unaffected.
494 cli_archive: false
495
496 desktop_linux_x64:
497 name: Desktop Linux x64
498 needs: [preflight, relay_public_config, build_bundle]
499 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}
500 uses: ./.github/workflows/release-desktop.yml
501 secrets: inherit
502 with:
503 version: ${{ needs.preflight.outputs.version }}
504 ref: ${{ needs.preflight.outputs.ref }}
505 release_channel: ${{ needs.preflight.outputs.release_channel }}
506 clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
507 clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
508 clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
509 relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
510 label: Linux x64
511 runner: blacksmith-32vcpu-ubuntu-2404
512 platform: linux
513 target: AppImage
514 arch: x64
515 rust_target: x86_64-unknown-linux-gnu
516 resource_key: linux-x64
517 cli_archive: true
518
519 # node-pty has no Linux prebuild and compiles from source, so the arm64 app
520 # and archive are built on arm64 hardware rather than cross-built.
521 desktop_linux_arm64:
522 name: Desktop Linux arm64
523 needs: [preflight, relay_public_config, build_bundle]
524 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}
525 uses: ./.github/workflows/release-desktop.yml
526 secrets: inherit
527 with:
528 version: ${{ needs.preflight.outputs.version }}
529 ref: ${{ needs.preflight.outputs.ref }}
530 release_channel: ${{ needs.preflight.outputs.release_channel }}
531 clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
532 clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
533 clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
534 relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
535 label: Linux arm64
536 runner: ubuntu-24.04-arm
537 platform: linux
538 target: AppImage
539 arch: arm64
540 rust_target: aarch64-unknown-linux-gnu
541 resource_key: linux-arm64
542 cli_archive: true
543
544 # The Windows jobs embed the same-arch Linux CLI archive as the WSL runtime.
545 # `!cancelled()` (not `!failure()`) still lets them start when that Linux job
546 # failed; the download step inside then fails this single platform if the
547 # archive is missing.
548 desktop_win_x64:
549 name: Desktop Windows x64
550 needs: [preflight, relay_public_config, build_bundle, desktop_linux_x64]
551 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}
552 uses: ./.github/workflows/release-desktop.yml
553 secrets: inherit
554 with:
555 version: ${{ needs.preflight.outputs.version }}
556 ref: ${{ needs.preflight.outputs.ref }}
557 release_channel: ${{ needs.preflight.outputs.release_channel }}
558 clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
559 clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
560 clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
561 relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
562 label: Windows x64
563 runner: blacksmith-32vcpu-windows-2025
564 platform: win
565 target: nsis
566 arch: x64
567 rust_target: x86_64-pc-windows-msvc
568 resource_key: win32-x64
569 cli_archive: true
570
571 desktop_win_arm64:
572 name: Desktop Windows arm64
573 needs: [preflight, relay_public_config, build_bundle, desktop_linux_arm64]
574 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}
575 uses: ./.github/workflows/release-desktop.yml
576 secrets: inherit
577 with:
578 version: ${{ needs.preflight.outputs.version }}
579 ref: ${{ needs.preflight.outputs.ref }}
580 release_channel: ${{ needs.preflight.outputs.release_channel }}
581 clerk_publishable_key: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
582 clerk_jwt_template: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
583 clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
584 relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
585 label: Windows arm64
586 runner: windows-11-arm
587 platform: win
588 target: nsis
589 arch: arm64
590 rust_target: aarch64-pc-windows-msvc
591 resource_key: win32-arm64
592 cli_archive: true
1167593
1168594 # Preview releases never reach npm: the archive on the GitHub Release is the
1169595 # only way to obtain one, so no dist-tag can ever resolve to a preview build.
1170596 publish_cli:
1171597 name: Publish CLI to npm
1172 needs: [preflight, relay_public_config, quality, build]
1173 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' }}
598 needs: [preflight, relay_public_config, quality, build_bundle]
599 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' }}
1174600 runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
1175601 timeout-minutes: 10
1176602 permissions:
......@@ -1199,30 +625,19 @@ jobs:
1199625 run-install: |
1200626 args:
1201627 - --filter=t3...
1202 - --filter=@t3tools/web...
1203628 - --filter=@t3tools/scripts...
1204629
1205 - name: Download relay client tracing config
1206 uses: actions/download-artifact@v8
1207 with:
1208 name: relay-client-tracing-config
1209 path: ${{ runner.temp }}/relay-client-tracing
1210
1211 - name: Load relay client tracing config
1212 shell: bash
1213 run: |
1214 config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env"
1215 tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")"
1216 echo "::add-mask::$tracing_token"
1217 cat "$config_path" >> "$GITHUB_ENV"
1218
1219630 - name: Align package versions to release version
1220631 run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
1221632
1222 # The t3 build task depends on @t3tools/web#build, so the web client is
1223 # built (once) as part of this step.
1224 - name: Build CLI package
1225 run: vp run --filter t3 build
633 # The artifact root is `apps/` (upload-artifact keeps the least common
634 # ancestor of its paths), so extracting into `apps` restores
635 # apps/server/dist and apps/desktop/dist-electron at their build paths.
636 - name: Download JS bundle
637 uses: actions/download-artifact@v8
638 with:
639 name: js-bundle
640 path: apps
1226641
1227642 - name: Download resource monitors
1228643 uses: actions/download-artifact@v8
......@@ -1247,8 +662,18 @@ jobs:
1247662
1248663 release:
1249664 name: Publish GitHub Release
1250 needs: [preflight, build, build_windows_arm64_cli, publish_cli]
1251 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' && needs.build_windows_arm64_cli.result == 'success' && (needs.publish_cli.result == 'success' || (needs.preflight.outputs.release_channel == 'preview' && needs.publish_cli.result == 'skipped')) }}
665 needs:
666 [
667 preflight,
668 desktop_mac_arm64,
669 desktop_mac_x64,
670 desktop_linux_x64,
671 desktop_linux_arm64,
672 desktop_win_x64,
673 desktop_win_arm64,
674 publish_cli,
675 ]
676 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')) }}
1252677 runs-on: blacksmith-8vcpu-ubuntu-2404
1253678 timeout-minutes: 30
1254679 permissions:
......@@ -1319,6 +744,9 @@ jobs:
1319744 exit 1
1320745 fi
1321746
747 # electron-updater reads one manifest per platform and channel and picks
748 # the file entry whose name carries the running arch, so the per-arch
749 # manifests the build jobs wrote are merged back into that one file.
1322750 - name: Merge macOS updater manifests
1323751 if: needs.preflight.outputs.release_channel != 'preview'
1324752 run: |
......@@ -1331,6 +759,21 @@ jobs:
1331759 fi
1332760 done
1333761
762 - name: Merge Windows updater manifests
763 if: needs.preflight.outputs.release_channel != 'preview'
764 run: |
765 shopt -s nullglob
766 for x64_manifest in release-assets/*-win-x64.yml; do
767 arm64_manifest="${x64_manifest%-x64.yml}-arm64.yml"
768 merged_manifest="${x64_manifest%-win-x64.yml}.yml"
769 if [[ -f "$arm64_manifest" ]]; then
770 node scripts/merge-update-manifests.ts --platform win "$x64_manifest" "$arm64_manifest" "$merged_manifest"
771 rm -f "$x64_manifest" "$arm64_manifest"
772 else
773 mv "$x64_manifest" "$merged_manifest"
774 fi
775 done
776
1334777 # Updater manifests and blockmaps are what electron-updater consumes.
1335778 # They are only listed for channels an updater is meant to follow.
1336779 - id: release_files
apps/marketing/src/pages/index.astro+1-1
......@@ -444,7 +444,7 @@ const mobileEndorsementRows = [
444444 return assets.find((a) => a.name.endsWith("-arm64.dmg"))?.browser_download_url ?? null;
445445 }
446446 if (platform.os === "linux") {
447 return assets.find((a) => a.name.endsWith(".AppImage"))?.browser_download_url ?? null;
447 return assets.find((a) => a.name.endsWith("-x86_64.AppImage"))?.browser_download_url ?? null;
448448 }
449449 return null;
450450 }
docs/operations/release.md+10-8
......@@ -23,18 +23,19 @@ This document covers the unified release workflow for stable and nightly desktop
2323 the commit to ship is not the latest nightly, such as a cherry-picked fix on a release branch.
2424- Runs lint, typecheck, and tests alongside artifact builds. Publishing waits for every check.
2525- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients.
26- Builds four artifacts in parallel for both channels:
26- 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.
27- Builds six desktop artifacts in parallel for both channels, each as its own job (`desktop_<platform>_<arch>`, 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):
2728 - macOS `arm64` DMG
2829 - macOS `x64` DMG
29 - Linux `x64` AppImage
30 - Windows `x64` NSIS installer
30 - Linux `x64` and `arm64` AppImage
31 - Windows `x64` and `arm64` NSIS installer
3132- Publishes one GitHub Release with all produced files.
3233 - Stable tags with a suffix after `X.Y.Z` (for example `1.2.3-alpha.1`) are published as GitHub prereleases.
3334 - Only plain stable `X.Y.Z` releases are marked as the repository's latest release.
3435 - Nightly runs are always GitHub prereleases and never marked latest.
3536 - 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.
3637- Includes Electron auto-update metadata (for example `latest*.yml`, `nightly*.yml`, and `*.blockmap`) in release assets.
37- Builds a self-contained CLI archive per platform (`t3-<version>-<platform>-<arch>.tar.gz`, `.zip` on Windows) on the same runners as the desktop artifacts and attaches them to the GitHub Release with a `SHA256SUMS` file, on every channel, 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.
38- Builds a self-contained CLI archive per platform (`t3-<version>-<platform>-<arch>.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.
3839 - 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`.
3940 - 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`.
4041 - 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
253254paths below `resources/server.asar.unpacked`. The Windows-native backend reads
254255the archive in place through Electron. Packaged Windows builds also ship
255256`resources/wsl-runtime.tar.gz` plus its SHA-256 sidecar: the Linux CLI archive
256(`t3-<version>-linux-x64.tar.gz`) built by the `build_linux_cli` job and handed
257to the Windows desktop build as `--wsl-runtime`, copied in verbatim so WSL runs
258the exact bytes a Linux user downloads. WSL verifies and extracts that archive
257(`t3-<version>-linux-<arch>.tar.gz`, the same arch as the Windows host) built
258by the Linux desktop job and handed to the Windows desktop build as
259`--wsl-runtime`, copied in verbatim so WSL runs the exact bytes a Linux user
260downloads. WSL verifies and extracts that archive
259261into `~/.t3/wsl-runtime/sha256-<archive-digest>` inside the selected distro,
260262then reuses it for later launches of the same update.
261263
......@@ -413,7 +415,7 @@ Checklist:
4134154. Verify workflow steps:
414416 - preflight passes
415417 - release quality checks pass
416 - all matrix builds pass
418 - `build_bundle` and all platform builds pass
417419 - `publish_cli` publishes the exact release version before the release job
418420 - release job uploads expected files
4194215. Smoke test downloaded artifacts.