authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-25 02:47:06-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-25 02:47:06-07:00
logc6b8bb82575c972ce858ddf3bdbf5386f2fb0f5e
tree883ffc1eb7e17861561321c8a2185d8ab25f22fe
parent06de9e90ab0832727968c58ce72282b94a95ae17
signature Signed by PGP key B5690EEEBB952194

feat(desktop): build macOS previews from a PR label (#8182)


3 files changed, 220 insertions(+), 10 deletions(-)

.github/workflows/desktop-macos-preview.yml created+165
...@@ -0,0 +1,165 @@
1name: Desktop macOS Preview
2
3on:
4 pull_request:
5 types: [labeled, synchronize, reopened]
6
7permissions:
8 contents: read
9 pull-requests: write
10
11concurrency:
12 group: desktop-macos-preview-${{ github.event.pull_request.number }}
13 cancel-in-progress: true
14
15jobs:
16 build:
17 name: Build macOS Apple Silicon preview
18 if: >-
19 github.event.pull_request.head.repo.full_name == github.repository &&
20 contains(github.event.pull_request.labels.*.name, 'preview:mac') &&
21 (github.event.action != 'labeled' || github.event.label.name == 'preview:mac')
22 runs-on: blacksmith-12vcpu-macos-26
23 timeout-minutes: 30
24 steps:
25 - name: Checkout
26 uses: actions/checkout@v6
27 with:
28 ref: ${{ github.event.pull_request.head.sha }}
29 sparse-checkout: |
30 /*
31 !/.repos/
32 sparse-checkout-cone-mode: false
33
34 - name: Setup Vite+
35 uses: voidzero-dev/setup-vp@v1
36 with:
37 node-version-file: package.json
38 cache: true
39 run-install: false
40
41 - name: Install desktop dependencies
42 run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts...
43
44 - name: Cache resource monitor
45 id: resource_monitor_cache
46 uses: actions/cache@v6
47 with:
48 path: native/resource-monitor/target/aarch64-apple-darwin/release/t3-resource-monitor
49 key: resource-monitor-aarch64-apple-darwin-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }}
50
51 - name: Setup Rust
52 if: steps.resource_monitor_cache.outputs.cache-hit != 'true'
53 uses: dtolnay/rust-toolchain@stable
54 with:
55 targets: aarch64-apple-darwin
56
57 - id: version
58 name: Set preview version and public configuration
59 shell: bash
60 env:
61 PR_NUMBER: ${{ github.event.pull_request.number }}
62 run: |
63 set -euo pipefail
64
65 base_version="$(node -p "require('./apps/desktop/package.json').version")"
66 preview_version="${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}"
67 node scripts/update-release-package-versions.ts "$preview_version"
68 cp .env.example .env
69
70 echo "version=$preview_version" >> "$GITHUB_OUTPUT"
71
72 - id: build
73 name: Build unsigned macOS DMG
74 shell: bash
75 env:
76 T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }}
77 PREVIEW_VERSION: ${{ steps.version.outputs.version }}
78 run: |
79 set -euo pipefail
80
81 vp run dist:desktop:artifact \
82 --platform mac \
83 --target dmg \
84 --arch arm64 \
85 --build-version "$PREVIEW_VERSION" \
86 --verbose
87
88 shopt -s nullglob
89 dmg_files=(release/*.dmg)
90 if (( ${#dmg_files[@]} != 1 )); then
91 printf 'Expected one DMG, found %s.\n' "${#dmg_files[@]}" >&2
92 exit 1
93 fi
94 printf 'dmg_name=%s\n' "$(basename "${dmg_files[0]}")" >> "$GITHUB_OUTPUT"
95
96 - id: upload
97 name: Upload macOS DMG
98 uses: actions/upload-artifact@v7
99 with:
100 path: release/*.dmg
101 if-no-files-found: error
102 archive: false
103 overwrite: true
104 retention-days: 7
105
106 - name: Comment download link
107 uses: actions/github-script@v8
108 env:
109 ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
110 DMG_NAME: ${{ steps.build.outputs.dmg_name }}
111 HEAD_SHA: ${{ github.event.pull_request.head.sha }}
112 PREVIEW_VERSION: ${{ steps.version.outputs.version }}
113 with:
114 script: |
115 const { data: pullRequest } = await github.rest.pulls.get({
116 owner: context.repo.owner,
117 repo: context.repo.repo,
118 pull_number: context.payload.pull_request.number,
119 });
120 if (pullRequest.head.sha !== process.env.HEAD_SHA) {
121 core.info("Skipping the outdated macOS preview comment.");
122 return;
123 }
124
125 const marker = "<!-- desktop-macos-preview -->";
126 const body = [
127 marker,
128 "### macOS preview",
129 "",
130 `[Download Apple Silicon DMG](${process.env.ARTIFACT_URL})`,
131 "",
132 `Version: ${process.env.PREVIEW_VERSION}`,
133 `Commit: ${process.env.HEAD_SHA.slice(0, 7)}`,
134 "",
135 "Unsigned build. Clear quarantine before opening:",
136 "```sh",
137 `xattr -d com.apple.quarantine ~/Downloads/${process.env.DMG_NAME}`,
138 "```",
139 "",
140 "The download requires GitHub access and expires after 7 days.",
141 ].join("\n");
142
143 const { data: comments } = await github.rest.issues.listComments({
144 owner: context.repo.owner,
145 repo: context.repo.repo,
146 issue_number: context.payload.pull_request.number,
147 per_page: 100,
148 });
149 const existing = comments.find((comment) => comment.body?.includes(marker));
150
151 if (existing) {
152 await github.rest.issues.updateComment({
153 owner: context.repo.owner,
154 repo: context.repo.repo,
155 comment_id: existing.id,
156 body,
157 });
158 } else {
159 await github.rest.issues.createComment({
160 owner: context.repo.owner,
161 repo: context.repo.repo,
162 issue_number: context.payload.pull_request.number,
163 body,
164 });
165 }
scripts/build-desktop-artifact.test.ts+39
...@@ -223,6 +223,45 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -223,6 +223,45 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
223 }),223 }),
224 );224 );
225225
226 it.effect("omits update feeds for pull request preview builds", () =>
227 Effect.gen(function* () {
228 const preview = yield* createBuildConfig(
229 "mac",
230 "dmg",
231 "0.0.33-pr.8182.1",
232 false,
233 false,
234 undefined,
235 undefined,
236 );
237 const release = yield* createBuildConfig(
238 "mac",
239 "dmg",
240 "0.0.33",
241 false,
242 false,
243 undefined,
244 undefined,
245 );
246
247 assert.notProperty(preview, "publish");
248 assert.deepStrictEqual(release.publish, [
249 {
250 provider: "github",
251 owner: "pingdotgg",
252 repo: "t3code",
253 releaseType: "release",
254 },
255 ]);
256 }).pipe(
257 Effect.provide(
258 ConfigProvider.layer(
259 ConfigProvider.fromEnv({ env: { GITHUB_REPOSITORY: "pingdotgg/t3code" } }),
260 ),
261 ),
262 ),
263 );
264
226 it("omits bundled workspace packages from staged desktop dependencies", () => {265 it("omits bundled workspace packages from staged desktop dependencies", () => {
227 assert.deepStrictEqual(266 assert.deepStrictEqual(
228 resolveDesktopRuntimeDependencies(267 resolveDesktopRuntimeDependencies(
scripts/build-desktop-artifact.ts+16-10
...@@ -2017,6 +2017,10 @@ export function resolveDesktopUpdateChannel(version: string): "latest" | "nightl...@@ -2017,6 +2017,10 @@ export function resolveDesktopUpdateChannel(version: string): "latest" | "nightl
2017 return /-nightly\.\d{8}\.\d+$/.test(version) ? "nightly" : "latest";2017 return /-nightly\.\d{8}\.\d+$/.test(version) ? "nightly" : "latest";
2018}2018}
20192019
2020function isDesktopPreviewVersion(version: string): boolean {
2021 return /-pr\./.test(version);
2022}
2023
2020export function resolveDesktopWebAssetBrand(version: string): WebAssetBrand {2024export function resolveDesktopWebAssetBrand(version: string): WebAssetBrand {
2021 return resolveWebAssetBrandForChannel(resolveDesktopUpdateChannel(version));2025 return resolveWebAssetBrandForChannel(resolveDesktopUpdateChannel(version));
2022}2026}
...@@ -2093,16 +2097,18 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (...@@ -2093,16 +2097,18 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
2093 ],2097 ],
2094 };2098 };
2095 const updateChannel = resolveDesktopUpdateChannel(version);2099 const updateChannel = resolveDesktopUpdateChannel(version);
2096 const publishConfig = yield* resolveGitHubPublishConfig(updateChannel);2100 if (!isDesktopPreviewVersion(version)) {
2097 if (publishConfig) {2101 const publishConfig = yield* resolveGitHubPublishConfig(updateChannel);
2098 buildConfig.publish = [publishConfig];2102 if (publishConfig) {
2099 } else if (mockUpdates) {2103 buildConfig.publish = [publishConfig];
2100 buildConfig.publish = [2104 } else if (mockUpdates) {
2101 {2105 buildConfig.publish = [
2102 provider: "generic",2106 {
2103 url: resolveMockUpdateServerUrl(mockUpdateServerPort),2107 provider: "generic",
2104 },2108 url: resolveMockUpdateServerUrl(mockUpdateServerPort),
2105 ];2109 },
2110 ];
2111 }
2106 }2112 }
21072113
2108 if (platform === "mac") {2114 if (platform === "mac") {