authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-23 05:36:09-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-23 05:36:09-07:00
log25dcee00a6e12db2781a17b326e6e34de0d4ced7
treed12c2e7bcbe33ff826a1c232f0f747fea602ee6d
parentf70eeeeb06d6d96f292be7eef1ed8948103e68dc
signature Signed by PGP key B5690EEEBB952194

perf(ci): speed up release builds and Windows packaging (#7975)


6 files changed, 251 insertions(+), 120 deletions(-)

.github/workflows/release.yml+63-19
...@@ -100,9 +100,6 @@ jobs:...@@ -100,9 +100,6 @@ jobs:
100 cache: true100 cache: true
101 run-install: true101 run-install: true
102102
103 - name: Ensure Electron runtime is installed
104 run: vp run --filter @t3tools/desktop ensure:electron
105
106 - id: release_meta103 - id: release_meta
107 name: Resolve release version104 name: Resolve release version
108 shell: bash105 shell: bash
...@@ -157,6 +154,40 @@ jobs:...@@ -157,6 +154,40 @@ jobs:
157 fi154 fi
158 fi155 fi
159156
157 - id: previous_tag
158 name: Resolve previous release tag
159 run: |
160 node scripts/resolve-previous-release-tag.ts \
161 --channel "${{ steps.release_meta.outputs.release_channel }}" \
162 --current-tag "${{ steps.release_meta.outputs.tag }}" \
163 --github-output
164
165 quality:
166 name: Release quality checks
167 needs: [preflight]
168 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
169 runs-on: blacksmith-8vcpu-ubuntu-2404
170 timeout-minutes: 10
171 steps:
172 - name: Checkout
173 uses: actions/checkout@v6
174 with:
175 ref: ${{ needs.preflight.outputs.ref }}
176 sparse-checkout: |
177 /*
178 !/.repos/
179 sparse-checkout-cone-mode: false
180
181 - name: Setup Vite+
182 uses: voidzero-dev/setup-vp@v1
183 with:
184 node-version-file: package.json
185 cache: true
186 run-install: true
187
188 - name: Ensure Electron runtime is installed
189 run: vp run --filter @t3tools/desktop ensure:electron
190
160 - name: Check191 - name: Check
161 run: vp check192 run: vp check
162193
...@@ -166,14 +197,6 @@ jobs:...@@ -166,14 +197,6 @@ jobs:
166 - name: Test197 - name: Test
167 run: vp run test198 run: vp run test
168199
169 - id: previous_tag
170 name: Resolve previous release tag
171 run: |
172 node scripts/resolve-previous-release-tag.ts \
173 --channel "${{ steps.release_meta.outputs.release_channel }}" \
174 --current-tag "${{ steps.release_meta.outputs.tag }}" \
175 --github-output
176
177 relay_public_config:200 relay_public_config:
178 name: Resolve T3 Connect public config201 name: Resolve T3 Connect public config
179 needs: preflight202 needs: preflight
...@@ -385,14 +408,34 @@ jobs:...@@ -385,14 +408,34 @@ jobs:
385 uses: voidzero-dev/setup-vp@v1408 uses: voidzero-dev/setup-vp@v1
386 with:409 with:
387 node-version-file: package.json410 node-version-file: package.json
388 cache: true411 cache: ${{ matrix.platform != 'win' }}
389 run-install: |412 run-install: false
390 args:413
391 - --filter=@t3tools/desktop...414 - name: Resolve Windows package cache path
392 - --filter=t3...415 if: matrix.platform == 'win'
393 - --filter=@t3tools/scripts...416 id: package_cache_path
417 shell: pwsh
418 run: '"path=$(vp pm cache dir)" >> $env:GITHUB_OUTPUT'
419
420 - name: Cache Windows packages
421 if: matrix.platform == 'win'
422 uses: actions/cache@v6
423 with:
424 path: ${{ steps.package_cache_path.outputs.path }}
425 key: windows-release-packages-v1-${{ matrix.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
426
427 - name: Install desktop dependencies
428 run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts...
429
430 - name: Cache resource monitor
431 id: resource_monitor_cache
432 uses: actions/cache@v6
433 with:
434 path: native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor${{ matrix.platform == 'win' && '.exe' || '' }}
435 key: resource-monitor-${{ matrix.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }}
394436
395 - name: Setup Rust437 - name: Setup Rust
438 if: steps.resource_monitor_cache.outputs.cache-hit != 'true'
396 uses: dtolnay/rust-toolchain@stable439 uses: dtolnay/rust-toolchain@stable
397 with:440 with:
398 targets: ${{ matrix.rust_target }}441 targets: ${{ matrix.rust_target }}
...@@ -518,6 +561,7 @@ jobs:...@@ -518,6 +561,7 @@ jobs:
518 - name: Build desktop artifact561 - name: Build desktop artifact
519 shell: bash562 shell: bash
520 env:563 env:
564 T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }}
521 CSC_LINK: ${{ secrets.CSC_LINK }}565 CSC_LINK: ${{ secrets.CSC_LINK }}
522 CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}566 CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
523 APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}567 APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
...@@ -664,8 +708,8 @@ jobs:...@@ -664,8 +708,8 @@ jobs:
664708
665 publish_cli:709 publish_cli:
666 name: Publish CLI to npm710 name: Publish CLI to npm
667 needs: [preflight, relay_public_config, build]711 needs: [preflight, relay_public_config, quality, build]
668 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build.result == 'success' }}712 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' }}
669 runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404713 runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
670 timeout-minutes: 10714 timeout-minutes: 10
671 permissions:715 permissions:
docs/operations/release.md+2-1
...@@ -11,7 +11,7 @@ This document covers the unified release workflow for stable and nightly desktop...@@ -11,7 +11,7 @@ This document covers the unified release workflow for stable and nightly desktop
11 - push tag matching `v*.*.*` for stable releases11 - push tag matching `v*.*.*` for stable releases
12 - scheduled nightly check every three hours12 - scheduled nightly check every three hours
13 - manual `workflow_dispatch` for either channel13 - manual `workflow_dispatch` for either channel
14- Runs quality gates first: lint, typecheck, test.14- Runs lint, typecheck, and tests alongside artifact builds. Publishing waits for every check.
15- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients.15- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients.
16- Builds four artifacts in parallel for both channels:16- Builds four artifacts in parallel for both channels:
17 - macOS `arm64` DMG17 - macOS `arm64` DMG
...@@ -365,6 +365,7 @@ Checklist:...@@ -365,6 +365,7 @@ Checklist:
3654. Push tag.3654. Push tag.
3665. Verify workflow steps:3665. Verify workflow steps:
367 - preflight passes367 - preflight passes
368 - release quality checks pass
368 - all matrix builds pass369 - all matrix builds pass
369 - `publish_cli` publishes the exact release version before the release job370 - `publish_cli` publishes the exact release version before the release job
370 - release job uploads expected files371 - release job uploads expected files
pnpm-lock.yaml+8-77
...@@ -38,6 +38,14 @@ catalogs:...@@ -38,6 +38,14 @@ catalogs:
38 version: 0.2.238 version: 0.2.2
3939
40overrides:40overrides:
41 '@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-darwin-arm64': '-'
42 '@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-darwin-x64': '-'
43 '@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-linux-arm64': '-'
44 '@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-linux-arm64-musl': '-'
45 '@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-linux-x64': '-'
46 '@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-linux-x64-musl': '-'
47 '@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-win32-arm64': '-'
48 '@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-win32-x64': '-'
41 '@clerk/backend': 3.14.049 '@clerk/backend': 3.14.0
42 '@clerk/clerk-js': 6.29.250 '@clerk/clerk-js': 6.29.2
43 '@clerk/clerk-js>@base-org/account': '-'51 '@clerk/clerk-js>@base-org/account': '-'
...@@ -960,50 +968,6 @@ packages:...@@ -960,50 +968,6 @@ packages:
960 '@alchemy.run/node-utils@0.0.5':968 '@alchemy.run/node-utils@0.0.5':
961 resolution: {integrity: sha512-5agdhQxWBodxa5hDRyjnpx91RTU3g+qd5fxYB7uNDCaOzB0XC47UU/KHR6zf6jhz/NXf61gJS+vhYwn8NHeRoQ==}969 resolution: {integrity: sha512-5agdhQxWBodxa5hDRyjnpx91RTU3g+qd5fxYB7uNDCaOzB0XC47UU/KHR6zf6jhz/NXf61gJS+vhYwn8NHeRoQ==}
962970
963 '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.170':
964 resolution: {integrity: sha512-rwfgArIa5WI0QPNqFsRBgvtSI0mrtpynUm0oK6+l6/KX4hcgnYGEzciZR1bOeD9/7sSZlTdIgt+T9alKeZmXcg==}
965 cpu: [arm64]
966 os: [darwin]
967
968 '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.170':
969 resolution: {integrity: sha512-0e58h8UQMtsQxLGIv9r4foxfBFWKZ7NeDtoplLhuD7EwQonehomw1sBXCch77t/IfUS+q5vQ5zv+fOGmap5nLQ==}
970 cpu: [x64]
971 os: [darwin]
972
973 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.170':
974 resolution: {integrity: sha512-SRYfQcsXlOq+CD/FqkQBTSHbaD++w73GnnO+NUV9adLYrca3kfetRwWT1iguY1cNS0l34dCR3rlzCPq78vg1Jg==}
975 cpu: [arm64]
976 os: [linux]
977 libc: [musl]
978
979 '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.170':
980 resolution: {integrity: sha512-gLbaFqcGppFJQd4DLNV4IXoeahejT/p2/M8bSSvRDbla9GOsBr1AxV5XLRyBn1e7xFGozZIAIQr3+1chp7NJgQ==}
981 cpu: [arm64]
982 os: [linux]
983 libc: [glibc]
984
985 '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.170':
986 resolution: {integrity: sha512-m4+I0qBEk7cxRKS+pL+eoWXbXTFOAo83fQ0tQvap4z/mDMm06IWJtEPoYTaMBwsp32GJWLkHWKbZSBCHZnp2DQ==}
987 cpu: [x64]
988 os: [linux]
989 libc: [musl]
990
991 '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.170':
992 resolution: {integrity: sha512-Xl/m7TaSC3T5IDBdHrZQ9fCQYyDmPELN34CL+MoyPIf7uSmuZnjE9fUOqDh2Rv26JxWssi1M6X+BBvVuKd6Cpg==}
993 cpu: [x64]
994 os: [linux]
995 libc: [glibc]
996
997 '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.170':
998 resolution: {integrity: sha512-IG+8isJNNJKbnnhO7m+PGhfVCg+XoQ/MDxGde5eigFI0WsEfitjuWSWwx82bT9ghxI1aa6qNvI+UPgPcZuo5Fg==}
999 cpu: [arm64]
1000 os: [win32]
1001
1002 '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.170':
1003 resolution: {integrity: sha512-7cuqSKbHVItPGVwRbd3A0BEJwcNtc7Fhoh6qHN4C6yrmjSrvdYYx3MLvq/VI768/RoG7mAMDxb+j7WfEfoP9BA==}
1004 cpu: [x64]
1005 os: [win32]
1006
1007 '@anthropic-ai/claude-agent-sdk@0.3.170':971 '@anthropic-ai/claude-agent-sdk@0.3.170':
1008 resolution: {integrity: sha512-pAvhfk+iTodXZ6RF18Kz7BEUWFjL7EcR3tKuhUNdPpE1NAYCR3mSHGbafi72JsrNwKEDIs7FU31z3fqhwy8QzA==}972 resolution: {integrity: sha512-pAvhfk+iTodXZ6RF18Kz7BEUWFjL7EcR3tKuhUNdPpE1NAYCR3mSHGbafi72JsrNwKEDIs7FU31z3fqhwy8QzA==}
1009 engines: {node: '>=18.0.0'}973 engines: {node: '>=18.0.0'}
...@@ -10546,44 +10510,11 @@ snapshots:...@@ -10546,44 +10510,11 @@ snapshots:
1054610510
10547 '@alchemy.run/node-utils@0.0.5': {}10511 '@alchemy.run/node-utils@0.0.5': {}
1054810512
10549 '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.170':
10550 optional: true
10551
10552 '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.170':
10553 optional: true
10554
10555 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.170':
10556 optional: true
10557
10558 '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.170':
10559 optional: true
10560
10561 '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.170':
10562 optional: true
10563
10564 '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.170':
10565 optional: true
10566
10567 '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.170':
10568 optional: true
10569
10570 '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.170':
10571 optional: true
10572
10573 '@anthropic-ai/claude-agent-sdk@0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)':10513 '@anthropic-ai/claude-agent-sdk@0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)':
10574 dependencies:10514 dependencies:
10575 '@anthropic-ai/sdk': 0.93.0(zod@4.4.3)10515 '@anthropic-ai/sdk': 0.93.0(zod@4.4.3)
10576 '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)10516 '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)
10577 zod: 4.4.310517 zod: 4.4.3
10578 optionalDependencies:
10579 '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.170
10580 '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.170
10581 '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.170
10582 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.170
10583 '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.170
10584 '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.170
10585 '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.170
10586 '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.170
1058710518
10588 '@anthropic-ai/sdk@0.93.0(zod@4.4.3)':10519 '@anthropic-ai/sdk@0.93.0(zod@4.4.3)':
10589 dependencies:10520 dependencies:
pnpm-workspace.yaml+9
...@@ -78,6 +78,15 @@ minimumReleaseAgeExclude:...@@ -78,6 +78,15 @@ minimumReleaseAgeExclude:
78 - "@legendapp/list@3.3.5"78 - "@legendapp/list@3.3.5"
7979
80overrides:80overrides:
81 # The SDK always receives the user's Claude executable, so its bundled binaries are unused.
82 "@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-darwin-arm64": "-"
83 "@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-darwin-x64": "-"
84 "@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-linux-arm64": "-"
85 "@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "-"
86 "@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-linux-x64": "-"
87 "@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-linux-x64-musl": "-"
88 "@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-win32-arm64": "-"
89 "@anthropic-ai/claude-agent-sdk>@anthropic-ai/claude-agent-sdk-win32-x64": "-"
81 "@clerk/backend": "catalog:"90 "@clerk/backend": "catalog:"
82 "@clerk/clerk-js": "catalog:"91 "@clerk/clerk-js": "catalog:"
83 "@clerk/clerk-js>@base-org/account": "-"92 "@clerk/clerk-js>@base-org/account": "-"
scripts/build-desktop-artifact.test.ts+119-1
...@@ -40,6 +40,7 @@ import {...@@ -40,6 +40,7 @@ import {
40 resolveDesktopUpdateChannel,40 resolveDesktopUpdateChannel,
41 resolveDesktopWebAssetBrand,41 resolveDesktopWebAssetBrand,
42 resolveResourceMonitorRustTargets,42 resolveResourceMonitorRustTargets,
43 resolveWindowsServerAsarIgnoreGlobs,
43 resourceMonitorExecutableName,44 resourceMonitorExecutableName,
44 resolveGitHubPublishConfig,45 resolveGitHubPublishConfig,
45 resolveMockUpdateServerPort,46 resolveMockUpdateServerPort,
...@@ -47,6 +48,7 @@ import {...@@ -47,6 +48,7 @@ import {
47 resolvePackageManagerUserAgent,48 resolvePackageManagerUserAgent,
48 stageLinuxIconSize,49 stageLinuxIconSize,
49 stageDesktopDmgBackground,50 stageDesktopDmgBackground,
51 stageResourceMonitor,
50 STAGE_INSTALL_ARGS,52 STAGE_INSTALL_ARGS,
51 ancestorNodeModulesPaths,53 ancestorNodeModulesPaths,
52 copyDirectoryPreservingSymlinks,54 copyDirectoryPreservingSymlinks,
...@@ -118,7 +120,7 @@ const makeWindowsPayloadFixture = Effect.fn("test.makeWindowsPayloadFixture")(fu...@@ -118,7 +120,7 @@ const makeWindowsPayloadFixture = Effect.fn("test.makeWindowsPayloadFixture")(fu
118 yield* fs.writeFileString(nativePath, "native-binary");120 yield* fs.writeFileString(nativePath, "native-binary");
119121
120 const generatedAsarPath = path.join(tempDir, WINDOWS_SERVER_ASAR_RESOURCE);122 const generatedAsarPath = path.join(tempDir, WINDOWS_SERVER_ASAR_RESOURCE);
121 yield* packWindowsServerAsar({ sourceDir, asarPath: generatedAsarPath });123 yield* packWindowsServerAsar({ sourceDir, asarPath: generatedAsarPath, arch: "x64" });
122124
123 const stageDistDir = path.join(tempDir, "dist");125 const stageDistDir = path.join(tempDir, "dist");
124 const packagedAppDir = path.join(stageDistDir, "win-unpacked");126 const packagedAppDir = path.join(stageDistDir, "win-unpacked");
...@@ -488,6 +490,121 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -488,6 +490,121 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
488 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),490 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
489 );491 );
490492
493 it("excludes node-pty binaries for the other Windows architecture", () => {
494 assert.deepStrictEqual(resolveWindowsServerAsarIgnoreGlobs("x64"), [
495 ...WINDOWS_SERVER_ASAR_IGNORE_GLOBS,
496 "**/node_modules/node-pty/prebuilds/win32-arm64",
497 "**/node_modules/node-pty/prebuilds/win32-arm64/**",
498 "**/node_modules/node-pty/third_party/conpty/*/win10-arm64",
499 "**/node_modules/node-pty/third_party/conpty/*/win10-arm64/**",
500 ]);
501 assert.deepStrictEqual(resolveWindowsServerAsarIgnoreGlobs("arm64"), [
502 ...WINDOWS_SERVER_ASAR_IGNORE_GLOBS,
503 "**/node_modules/node-pty/prebuilds/win32-x64",
504 "**/node_modules/node-pty/prebuilds/win32-x64/**",
505 "**/node_modules/node-pty/third_party/conpty/*/win10-x64",
506 "**/node_modules/node-pty/third_party/conpty/*/win10-x64/**",
507 ]);
508 });
509
510 it.effect(
511 "keeps target and WSL native files while excluding the other Windows architecture",
512 () =>
513 Effect.scoped(
514 Effect.gen(function* () {
515 const fs = yield* FileSystem.FileSystem;
516 const path = yield* Path.Path;
517 const tempDir = yield* fs.makeTempDirectoryScoped({
518 prefix: "t3-windows-architecture-test-",
519 });
520 const sourceDir = path.join(tempDir, "server");
521 const nativeFiles = [
522 "node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe",
523 "node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe",
524 "node_modules/node-pty/prebuilds/linux-x64/pty.node",
525 "node_modules/node-pty/third_party/conpty/1.0.0/win10-x64/OpenConsole.exe",
526 "node_modules/node-pty/third_party/conpty/1.0.0/win10-arm64/OpenConsole.exe",
527 ];
528
529 for (const nativeFile of nativeFiles) {
530 const nativePath = path.join(sourceDir, nativeFile);
531 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });
532 yield* fs.writeFileString(nativePath, "native");
533 }
534
535 const asarPath = path.join(tempDir, "server.asar");
536 yield* packWindowsServerAsar({ sourceDir, asarPath, arch: "x64" });
537 const unpackedRoot = `${asarPath}.unpacked`;
538
539 assert.isTrue(
540 yield* fs.exists(
541 path.join(
542 unpackedRoot,
543 "node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe",
544 ),
545 ),
546 );
547 assert.isTrue(
548 yield* fs.exists(
549 path.join(unpackedRoot, "node_modules/node-pty/prebuilds/linux-x64/pty.node"),
550 ),
551 );
552 assert.isFalse(
553 yield* fs.exists(
554 path.join(unpackedRoot, "node_modules/node-pty/prebuilds/win32-arm64"),
555 ),
556 );
557 assert.isFalse(
558 yield* fs.exists(
559 path.join(unpackedRoot, "node_modules/node-pty/third_party/conpty/1.0.0/win10-arm64"),
560 ),
561 );
562 }),
563 ),
564 );
565
566 it.effect("stages a cached resource monitor without invoking Cargo", () =>
567 Effect.scoped(
568 Effect.gen(function* () {
569 const fs = yield* FileSystem.FileSystem;
570 const path = yield* Path.Path;
571 const repoRoot = yield* fs.makeTempDirectoryScoped({
572 prefix: "t3-resource-monitor-cache-test-",
573 });
574 const binaryPath = path.join(
575 repoRoot,
576 "native/resource-monitor/target/x86_64-unknown-linux-gnu/release/t3-resource-monitor",
577 );
578 const stageResourcesDir = path.join(repoRoot, "stage");
579 yield* fs.makeDirectory(path.dirname(binaryPath), { recursive: true });
580 yield* fs.writeFileString(binaryPath, "cached monitor");
581
582 yield* stageResourceMonitor({
583 repoRoot,
584 stageResourcesDir,
585 platform: "linux",
586 arch: "x64",
587 verbose: false,
588 }).pipe(
589 Effect.provide(
590 ConfigProvider.layer(
591 ConfigProvider.fromEnv({
592 env: { T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: "true" },
593 }),
594 ),
595 ),
596 );
597
598 assert.equal(
599 yield* fs.readFileString(
600 path.join(stageResourcesDir, "resource-monitor/t3-resource-monitor"),
601 ),
602 "cached monitor",
603 );
604 }),
605 ),
606 );
607
491 it.effect("validates every ASAR-unpacked native in the packaged Windows payload", () =>608 it.effect("validates every ASAR-unpacked native in the packaged Windows payload", () =>
492 Effect.scoped(609 Effect.scoped(
493 Effect.gen(function* () {610 Effect.gen(function* () {
...@@ -504,6 +621,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -504,6 +621,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
504 yield* packWindowsServerAsar({621 yield* packWindowsServerAsar({
505 sourceDir: fixture.sourceDir,622 sourceDir: fixture.sourceDir,
506 asarPath: secondAsarPath,623 asarPath: secondAsarPath,
624 arch: "x64",
507 });625 });
508 const [firstAsar, secondAsar] = yield* Effect.all([626 const [firstAsar, secondAsar] = yield* Effect.all([
509 fs.readFile(fixture.generatedAsarPath),627 fs.readFile(fixture.generatedAsarPath),
scripts/build-desktop-artifact.ts+50-22
...@@ -819,6 +819,21 @@ export const WINDOWS_SERVER_ASAR_IGNORE_GLOBS = [...@@ -819,6 +819,21 @@ export const WINDOWS_SERVER_ASAR_IGNORE_GLOBS = [
819 "**/node_modules/.bin",819 "**/node_modules/.bin",
820 "**/node_modules/.bin/**",820 "**/node_modules/.bin/**",
821] as const;821] as const;
822
823export function resolveWindowsServerAsarIgnoreGlobs(arch: typeof BuildArch.Type) {
824 const unusedArch = arch === "arm64" ? "x64" : "arm64";
825 const unusedPrebuild = `**/node_modules/node-pty/prebuilds/win32-${unusedArch}`;
826 const unusedConpty = `**/node_modules/node-pty/third_party/conpty/*/win10-${unusedArch}`;
827
828 return [
829 ...WINDOWS_SERVER_ASAR_IGNORE_GLOBS,
830 unusedPrebuild,
831 `${unusedPrebuild}/**`,
832 unusedConpty,
833 `${unusedConpty}/**`,
834 ];
835}
836
822export const WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT = 80;837export const WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT = 80;
823export const WINDOWS_SERVER_RESOURCE_SOURCE_DIR = "apps/desktop/prod-resources/windows-server";838export const WINDOWS_SERVER_RESOURCE_SOURCE_DIR = "apps/desktop/prod-resources/windows-server";
824export const WINDOWS_SERVER_EXTRA_RESOURCES = [839export const WINDOWS_SERVER_EXTRA_RESOURCES = [
...@@ -1643,7 +1658,7 @@ const verifyPackagedBundleIsSelfContained = Effect.fn("verifyPackagedBundleIsSel...@@ -1643,7 +1658,7 @@ const verifyPackagedBundleIsSelfContained = Effect.fn("verifyPackagedBundleIsSel
1643 },1658 },
1644);1659);
16451660
1646const stageResourceMonitor = Effect.fn("stageResourceMonitor")(function* (input: {1661export const stageResourceMonitor = Effect.fn("stageResourceMonitor")(function* (input: {
1647 readonly repoRoot: string;1662 readonly repoRoot: string;
1648 readonly stageResourcesDir: string;1663 readonly stageResourcesDir: string;
1649 readonly platform: typeof BuildPlatform.Type;1664 readonly platform: typeof BuildPlatform.Type;
...@@ -1655,28 +1670,33 @@ const stageResourceMonitor = Effect.fn("stageResourceMonitor")(function* (input:...@@ -1655,28 +1670,33 @@ const stageResourceMonitor = Effect.fn("stageResourceMonitor")(function* (input:
1655 const manifestPath = path.join(input.repoRoot, "native/resource-monitor/Cargo.toml");1670 const manifestPath = path.join(input.repoRoot, "native/resource-monitor/Cargo.toml");
1656 const executableName = resourceMonitorExecutableName(input.platform);1671 const executableName = resourceMonitorExecutableName(input.platform);
1657 const rustTargets = resolveResourceMonitorRustTargets(input.platform, input.arch);1672 const rustTargets = resolveResourceMonitorRustTargets(input.platform, input.arch);
1673 const reuseResourceMonitor = yield* Config.boolean("T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR").pipe(
1674 Config.withDefault(false),
1675 );
1658 const builtBinaries: string[] = [];1676 const builtBinaries: string[] = [];
16591677
1660 for (const rustTarget of rustTargets) {1678 for (const rustTarget of rustTargets) {
1661 const spawnCommand = yield* resolveSpawnCommand("cargo", [1679 if (!reuseResourceMonitor) {
1662 "build",1680 const spawnCommand = yield* resolveSpawnCommand("cargo", [
1663 "--locked",1681 "build",
1664 "--release",1682 "--locked",
1665 "--manifest-path",1683 "--release",
1666 manifestPath,1684 "--manifest-path",
1667 "--target",1685 manifestPath,
1668 rustTarget,1686 "--target",
1669 ]);1687 rustTarget,
1670 yield* runCommand(1688 ]);
1671 ChildProcess.make(spawnCommand.command, spawnCommand.args, {1689 yield* runCommand(
1672 cwd: input.repoRoot,1690 ChildProcess.make(spawnCommand.command, spawnCommand.args, {
1673 shell: spawnCommand.shell,1691 cwd: input.repoRoot,
1674 }),1692 shell: spawnCommand.shell,
1675 {1693 }),
1676 label: `cargo build resource monitor (${rustTarget})`,1694 {
1677 verbose: input.verbose,1695 label: `cargo build resource monitor (${rustTarget})`,
1678 },1696 verbose: input.verbose,
1679 );1697 },
1698 );
1699 }
16801700
1681 const binaryPath = path.join(1701 const binaryPath = path.join(
1682 input.repoRoot,1702 input.repoRoot,
...@@ -1693,6 +1713,9 @@ const stageResourceMonitor = Effect.fn("stageResourceMonitor")(function* (input:...@@ -1693,6 +1713,9 @@ const stageResourceMonitor = Effect.fn("stageResourceMonitor")(function* (input:
1693 arch: input.arch,1713 arch: input.arch,
1694 });1714 });
1695 }1715 }
1716 if (reuseResourceMonitor) {
1717 yield* Effect.log(`[desktop-artifact] Reusing cached resource monitor (${rustTarget}).`);
1718 }
1696 builtBinaries.push(binaryPath);1719 builtBinaries.push(binaryPath);
1697 }1720 }
16981721
...@@ -2253,6 +2276,7 @@ const stageWslNodePtyPrebuild = Effect.fn("stageWslNodePtyPrebuild")(function* (...@@ -2253,6 +2276,7 @@ const stageWslNodePtyPrebuild = Effect.fn("stageWslNodePtyPrebuild")(function* (
2253export const packWindowsServerAsar = Effect.fn("packWindowsServerAsar")(function* (input: {2276export const packWindowsServerAsar = Effect.fn("packWindowsServerAsar")(function* (input: {
2254 readonly sourceDir: string;2277 readonly sourceDir: string;
2255 readonly asarPath: string;2278 readonly asarPath: string;
2279 readonly arch: typeof BuildArch.Type;
2256}) {2280}) {
2257 const fs = yield* FileSystem.FileSystem;2281 const fs = yield* FileSystem.FileSystem;
2258 yield* Effect.tryPromise({2282 yield* Effect.tryPromise({
...@@ -2260,7 +2284,7 @@ export const packWindowsServerAsar = Effect.fn("packWindowsServerAsar")(function...@@ -2260,7 +2284,7 @@ export const packWindowsServerAsar = Effect.fn("packWindowsServerAsar")(function
2260 createPackageWithOptions(input.sourceDir, input.asarPath, {2284 createPackageWithOptions(input.sourceDir, input.asarPath, {
2261 dot: true,2285 dot: true,
2262 unpack: WINDOWS_SERVER_ASAR_UNPACK_GLOB,2286 unpack: WINDOWS_SERVER_ASAR_UNPACK_GLOB,
2263 globOptions: { ignore: [...WINDOWS_SERVER_ASAR_IGNORE_GLOBS] },2287 globOptions: { ignore: resolveWindowsServerAsarIgnoreGlobs(input.arch) },
2264 }),2288 }),
2265 catch: (cause) => new WindowsServerSidecarPackError({ asarPath: input.asarPath, cause }),2289 catch: (cause) => new WindowsServerSidecarPackError({ asarPath: input.asarPath, cause }),
2266 });2290 });
...@@ -2354,7 +2378,11 @@ export const stageWindowsServerSidecar = Effect.fn("stageWindowsServerSidecar")(...@@ -2354,7 +2378,11 @@ export const stageWindowsServerSidecar = Effect.fn("stageWindowsServerSidecar")(
23542378
2355 yield* Effect.log("[desktop-artifact] Packing server.asar...");2379 yield* Effect.log("[desktop-artifact] Packing server.asar...");
2356 yield* fs.makeDirectory(path.dirname(input.asarPath), { recursive: true });2380 yield* fs.makeDirectory(path.dirname(input.asarPath), { recursive: true });
2357 yield* packWindowsServerAsar({ sourceDir: serverStageDir, asarPath: input.asarPath });2381 yield* packWindowsServerAsar({
2382 sourceDir: serverStageDir,
2383 asarPath: input.asarPath,
2384 arch: input.arch,
2385 });
2358 const packedStat = yield* fs.stat(input.asarPath);2386 const packedStat = yield* fs.stat(input.asarPath);
2359 yield* Effect.log(2387 yield* Effect.log(
2360 `[desktop-artifact] Packed server.asar (${String(packedStat.size)} bytes) + unpacked natives.`,2388 `[desktop-artifact] Packed server.asar (${String(packedStat.size)} bytes) + unpacked natives.`,