authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-30 22:01:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-30 22:01:33-07:00
logc57a04b722f2172e3be2c8f73c936d10b4582431
tree9b7785bdee58a951c97529873769f6d491ec3466
parent8630e1ac7a94cdae6f3014161db65385931cf3a2
signature Signed by PGP key B5690EEEBB952194

perf(release): start Windows builds without waiting for the Linux job (#14027)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

4 files changed, 85 insertions(+), 22 deletions(-)

.github/workflows/desktop-macos-preview-publish.yml+4
...@@ -284,6 +284,10 @@ jobs:...@@ -284,6 +284,10 @@ jobs:
284 concurrency:284 concurrency:
285 group: desktop-macos-preview-${{ needs.resolve.outputs.pr_number }}-build285 group: desktop-macos-preview-${{ needs.resolve.outputs.pr_number }}-build
286 cancel-in-progress: true286 cancel-in-progress: true
287 # release-desktop.yml asks for actions: read (its Windows builds list artifacts).
288 permissions:
289 actions: read
290 contents: read
287 uses: ./.github/workflows/release-desktop.yml291 uses: ./.github/workflows/release-desktop.yml
288 secrets:292 secrets:
289 CSC_LINK: ${{ secrets.CSC_LINK }}293 CSC_LINK: ${{ secrets.CSC_LINK }}
.github/workflows/release-desktop.yml+66-10
...@@ -93,13 +93,17 @@ on:...@@ -93,13 +93,17 @@ on:
93 type: string93 type: string
9494
95permissions:95permissions:
96 # Windows builds read this run's artifact list to wait for the Linux CLI archive.
97 actions: read
96 contents: read98 contents: read
9799
98jobs:100jobs:
99 build:101 build:
100 name: Build ${{ inputs.label }}102 name: Build ${{ inputs.label }}
101 runs-on: ${{ inputs.runner }}103 runs-on: ${{ inputs.runner }}
102 timeout-minutes: 30104 # 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 }}
103 env:107 env:
104 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ inputs.clerk_publishable_key }}108 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ inputs.clerk_publishable_key }}
105 T3CODE_CLERK_JWT_TEMPLATE: ${{ inputs.clerk_jwt_template }}109 T3CODE_CLERK_JWT_TEMPLATE: ${{ inputs.clerk_jwt_template }}
...@@ -204,15 +208,6 @@ jobs:...@@ -204,15 +208,6 @@ jobs:
204 name: js-bundle208 name: js-bundle
205 path: apps209 path: apps
206210
207 # The WSL backend runs the Linux CLI archive inside the distro, so the
208 # Windows desktop embeds the same-arch archive the release attaches.
209 - name: Download Linux CLI archive for WSL
210 if: inputs.platform == 'win'
211 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
212 with:
213 name: cli-linux-${{ inputs.arch }}
214 path: wsl-runtime
215
216 # Only the resource monitor build needs these libraries. With a cached211 # Only the resource monitor build needs these libraries. With a cached
217 # monitor, the desktop build's preflight skips its MSVC check, and node-pty212 # monitor, the desktop build's preflight skips its MSVC check, and node-pty
218 # ships prebuilt Windows binaries, so the 30-60s install is skipped too.213 # ships prebuilt Windows binaries, so the 30-60s install is skipped too.
...@@ -314,6 +309,67 @@ jobs:...@@ -314,6 +309,67 @@ jobs:
314 Select-Object -Unique309 Select-Object -Unique
315 "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV310 "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV
316311
312 # The WSL backend runs the Linux CLI archive inside the distro, so the
313 # Windows desktop embeds the same-arch archive the release attaches.
314 # This job starts alongside that Linux job instead of after it, and the
315 # Linux job uploads the archive partway through, so the wait is placed
316 # after all other setup. It stops early if the Linux job ended without
317 # uploading the archive.
318 - name: Wait for Linux CLI archive
319 if: inputs.platform == 'win'
320 shell: pwsh
321 timeout-minutes: 40
322 env:
323 GITHUB_TOKEN: ${{ github.token }}
324 ARTIFACT: cli-linux-${{ inputs.arch }}
325 LINUX_JOB: Build Linux ${{ inputs.arch }}
326 run: |
327 $headers = @{ Authorization = "Bearer $env:GITHUB_TOKEN"; Accept = "application/vnd.github+json" }
328 $run = "$env:GITHUB_API_URL/repos/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID"
329 function Test-Archive {
330 (Invoke-RestMethod -Headers $headers -Uri "$run/artifacts?name=$env:ARTIFACT").total_count -gt 0
331 }
332 $missingSince = $null
333 while ($true) {
334 try {
335 if (Test-Archive) {
336 Write-Host "$env:ARTIFACT is ready."
337 exit 0
338 }
339 $jobs = Invoke-RestMethod -Headers $headers -Uri "$run/attempts/$env:GITHUB_RUN_ATTEMPT/jobs?per_page=100"
340 $linux = $jobs.jobs | Where-Object { $_.name.EndsWith($env:LINUX_JOB) } | Select-Object -First 1
341 if (-not $linux) {
342 # Both jobs start together, so a Linux job still missing after a
343 # few minutes means a renamed or skipped job, not a slow one.
344 if (-not $missingSince) {
345 $missingSince = Get-Date
346 } elseif (((Get-Date) - $missingSince).TotalMinutes -ge 5) {
347 Write-Host "::error::No job named '$env:LINUX_JOB' in this run, so $env:ARTIFACT will not arrive."
348 exit 1
349 }
350 } elseif ($linux.conclusion) {
351 # The upload may have landed between the two requests above.
352 if (Test-Archive) {
353 Write-Host "$env:ARTIFACT is ready."
354 exit 0
355 }
356 Write-Host "::error::$env:LINUX_JOB ended ($($linux.conclusion)) without uploading $env:ARTIFACT."
357 exit 1
358 }
359 } catch {
360 Write-Warning "Could not read this run's artifacts or jobs: $($_.Exception.Message)"
361 }
362 Write-Host "Waiting for $env:ARTIFACT..."
363 Start-Sleep -Seconds 10
364 }
365
366 - name: Download Linux CLI archive for WSL
367 if: inputs.platform == 'win'
368 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
369 with:
370 name: cli-linux-${{ inputs.arch }}
371 path: wsl-runtime
372
317 - name: Build desktop artifact373 - name: Build desktop artifact
318 shell: bash374 shell: bash
319 env:375 env:
.github/workflows/release.yml+14-11
...@@ -37,6 +37,9 @@ concurrency:...@@ -37,6 +37,9 @@ concurrency:
37 queue: max37 queue: max
3838
39permissions:39permissions:
40 # The Windows desktop jobs read this run's artifact list to wait for the
41 # Linux CLI archive they embed (see release-desktop.yml).
42 actions: read
40 contents: read43 contents: read
41 id-token: none44 id-token: none
4245
...@@ -531,11 +534,12 @@ jobs:...@@ -531,11 +534,12 @@ jobs:
531 retention-days: 1534 retention-days: 1
532535
533 # One job per platform and architecture (see release-desktop.yml), each on536 # One job per platform and architecture (see release-desktop.yml), each on
534 # hardware of its own architecture, and each gated only on what it consumes:537 # hardware of its own architecture, and each gated only on the JS bundle.
535 # every platform needs the JS bundle, and the Windows jobs also need the538 # The Windows jobs also embed the same-arch Linux CLI archive as the WSL
536 # same-arch Linux job, whose CLI archive they embed as the WSL runtime. Every539 # runtime; they wait for that artifact partway through instead of for the
537 # job builds the desktop app; all but macOS x64 also build the CLI archive540 # whole Linux job. Every job builds the desktop app; all but macOS x64 also
538 # for their platform, so a target either ships fully or not at all.541 # build the CLI archive for their platform, so a target either ships fully or
542 # not at all.
539 desktop_mac_arm64:543 desktop_mac_arm64:
540 name: Desktop macOS arm64544 name: Desktop macOS arm64
541 needs: [preflight, relay_public_config, build_bundle]545 needs: [preflight, relay_public_config, build_bundle]
...@@ -633,13 +637,12 @@ jobs:...@@ -633,13 +637,12 @@ jobs:
633 resource_key: linux-arm64637 resource_key: linux-arm64
634 cli_archive: true638 cli_archive: true
635639
636 # The Windows jobs embed the same-arch Linux CLI archive as the WSL runtime.640 # The Windows jobs start with the other platforms. Their checkout, install,
637 # `!cancelled()` (not `!failure()`) still lets them start when that Linux job641 # and toolchain setup take longer than the same-arch Linux job needs to
638 # failed; the download step inside then fails this single platform if the642 # upload its CLI archive, so the wait inside is usually instant.
639 # archive is missing.
640 desktop_win_x64:643 desktop_win_x64:
641 name: Desktop Windows x64644 name: Desktop Windows x64
642 needs: [preflight, relay_public_config, build_bundle, desktop_linux_x64]645 needs: [preflight, relay_public_config, build_bundle]
643 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}646 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}
644 uses: ./.github/workflows/release-desktop.yml647 uses: ./.github/workflows/release-desktop.yml
645 secrets: inherit648 secrets: inherit
...@@ -662,7 +665,7 @@ jobs:...@@ -662,7 +665,7 @@ jobs:
662665
663 desktop_win_arm64:666 desktop_win_arm64:
664 name: Desktop Windows arm64667 name: Desktop Windows arm64
665 needs: [preflight, relay_public_config, build_bundle, desktop_linux_arm64]668 needs: [preflight, relay_public_config, build_bundle]
666 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}669 if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build_bundle.result == 'success' }}
667 uses: ./.github/workflows/release-desktop.yml670 uses: ./.github/workflows/release-desktop.yml
668 secrets: inherit671 secrets: inherit
docs/operations/release.md+1-1
...@@ -26,7 +26,7 @@ This document covers the unified release workflow for stable and nightly desktop...@@ -26,7 +26,7 @@ This document covers the unified release workflow for stable and nightly desktop
26- Runs lint, typecheck, and tests alongside artifact builds. Publishing waits for every check.26- Runs lint, typecheck, and tests alongside artifact builds. Publishing waits for every check.
27- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients.27- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients.
28- 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.28- 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.
29- 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):29- 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 embed the same-arch Linux CLI archive as the WSL runtime and wait for that artifact partway through, not for the whole Linux job:
30 - macOS `arm64` DMG30 - macOS `arm64` DMG
31 - macOS `x64` DMG31 - macOS `x64` DMG
32 - Linux `x64` and `arm64` AppImage and `.deb`, from one electron-builder run. The `.deb` updates in the app through electron-updater, which installs it with `dpkg`.32 - Linux `x64` and `arm64` AppImage and `.deb`, from one electron-builder run. The `.deb` updates in the app through electron-updater, which installs it with `dpkg`.