1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeChildProcess from "node:child_process";
3import * as NodeFS from "node:fs";
4import * as NodeOS from "node:os";
5import * as NodePath from "node:path";
6import * as NodeURL from "node:url";
7import * as Console from "effect/Console";
8import * as Effect from "effect/Effect";
9
10const repoRoot = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), "..");
11
12const workspaceFiles = [
13 "package.json",
14 "pnpm-lock.yaml",
15 "pnpm-workspace.yaml",
16 "apps/server/package.json",
17 "apps/desktop/package.json",
18 "apps/web/package.json",
19 "apps/mobile/package.json",
20 "apps/mobile/modules/t3-source-text/package.json",
21 "apps/mobile/modules/t3-review-diff/package.json",
22 "apps/mobile/modules/t3-terminal/package.json",
23 "apps/marketing/package.json",
24 "infra/relay/package.json",
25 "oxlint-plugin-t3code/package.json",
26 "packages/client-runtime/package.json",
27 "packages/contracts/package.json",
28 "packages/shared/package.json",
29 "packages/ssh/package.json",
30 "packages/tailscale/package.json",
31 "packages/provider-core/package.json",
32 "packages/provider-testing/package.json",
33 "packages/provider-pi/package.json",
34 "packages/provider-acp/package.json",
35 "packages/provider-acp-registry/package.json",
36 "packages/provider-cursor/package.json",
37 "packages/provider-grok/package.json",
38 "packages/provider-muse/package.json",
39 "packages/provider-opencode/package.json",
40 "packages/source-control-azure-devops/package.json",
41 "packages/source-control-bitbucket/package.json",
42 "packages/source-control-core/package.json",
43 "packages/source-control-forgejo/package.json",
44 "packages/source-control-gitcafe/package.json",
45 "packages/source-control-github/package.json",
46 "packages/source-control-gitlab/package.json",
47 "packages/source-control-testing/package.json",
48 "packages/effect-acp/package.json",
49 "packages/effect-codex-app-server/package.json",
50 "scripts/package.json",
51] as const;
52
53function copyWorkspaceManifestFixture(targetRoot: string): void {
54 for (const relativePath of workspaceFiles) {
55 const sourcePath = NodePath.resolve(repoRoot, relativePath);
56 const destinationPath = NodePath.resolve(targetRoot, relativePath);
57 NodeFS.mkdirSync(NodePath.dirname(destinationPath), { recursive: true });
58 NodeFS.cpSync(sourcePath, destinationPath);
59 }
60
61 const mobileDependencies = NodePath.resolve(repoRoot, "apps/mobile/deps");
62 if (NodeFS.existsSync(mobileDependencies)) {
63 NodeFS.cpSync(mobileDependencies, NodePath.resolve(targetRoot, "apps/mobile/deps"), {
64 recursive: true,
65 });
66 }
67
68 const patchesDirectory = NodePath.resolve(repoRoot, "patches");
69 if (NodeFS.existsSync(patchesDirectory)) {
70 NodeFS.cpSync(patchesDirectory, NodePath.resolve(targetRoot, "patches"), { recursive: true });
71 }
72}
73
74function writeMacManifestFixtures(targetRoot: string): { arm64Path: string; x64Path: string } {
75 const assetDirectory = NodePath.resolve(targetRoot, "release-assets");
76 NodeFS.mkdirSync(assetDirectory, { recursive: true });
77
78 const arm64Path = NodePath.resolve(assetDirectory, "latest-mac.yml");
79 const x64Path = NodePath.resolve(assetDirectory, "latest-mac-x64.yml");
80
81 NodeFS.writeFileSync(
82 arm64Path,
83 `version: 9.9.9-smoke.0
84files:
85 - url: T3-Code-9.9.9-smoke.0-arm64.zip
86 sha512: arm64zip
87 size: 125621344
88 - url: T3-Code-9.9.9-smoke.0-arm64.dmg
89 sha512: arm64dmg
90 size: 131754935
91path: T3-Code-9.9.9-smoke.0-arm64.zip
92sha512: arm64zip
93releaseDate: '2026-03-08T10:32:14.587Z'
94`,
95 );
96
97 NodeFS.writeFileSync(
98 x64Path,
99 `version: 9.9.9-smoke.0
100files:
101 - url: T3-Code-9.9.9-smoke.0-x64.zip
102 sha512: x64zip
103 size: 132000112
104 - url: T3-Code-9.9.9-smoke.0-x64.dmg
105 sha512: x64dmg
106 size: 138148807
107path: T3-Code-9.9.9-smoke.0-x64.zip
108sha512: x64zip
109releaseDate: '2026-03-08T10:36:07.540Z'
110`,
111 );
112
113 return { arm64Path, x64Path };
114}
115
116function writeWindowsManifestFixtures(
117 targetRoot: string,
118 channel: string,
119): { arm64Path: string; x64Path: string } {
120 const assetDirectory = NodePath.resolve(targetRoot, "release-assets");
121 NodeFS.mkdirSync(assetDirectory, { recursive: true });
122
123 const arm64Path = NodePath.resolve(assetDirectory, `${channel}-win-arm64.yml`);
124 const x64Path = NodePath.resolve(assetDirectory, `${channel}-win-x64.yml`);
125
126 NodeFS.writeFileSync(
127 arm64Path,
128 `version: 9.9.9-smoke.0
129files:
130 - url: T3-Code-9.9.9-smoke.0-arm64.exe
131 sha512: arm64exe
132 size: 126621344
133 - url: T3-Code-9.9.9-smoke.0-arm64.exe.blockmap
134 sha512: arm64blockmap
135 size: 152344
136path: T3-Code-9.9.9-smoke.0-arm64.exe
137sha512: arm64exe
138releaseDate: '2026-03-08T10:32:14.587Z'
139`,
140 );
141
142 NodeFS.writeFileSync(
143 x64Path,
144 `version: 9.9.9-smoke.0
145files:
146 - url: T3-Code-9.9.9-smoke.0-x64.exe
147 sha512: x64exe
148 size: 132000112
149 - url: T3-Code-9.9.9-smoke.0-x64.exe.blockmap
150 sha512: x64blockmap
151 size: 160112
152path: T3-Code-9.9.9-smoke.0-x64.exe
153sha512: x64exe
154releaseDate: '2026-03-08T10:36:07.540Z'
155`,
156 );
157
158 return { arm64Path, x64Path };
159}
160
161function writeWindowsBuilderDebugFixtures(targetRoot: string): {
162 arm64Path: string;
163 x64Path: string;
164} {
165 const assetDirectory = NodePath.resolve(targetRoot, "release-assets");
166 NodeFS.mkdirSync(assetDirectory, { recursive: true });
167
168 const arm64Path = NodePath.resolve(assetDirectory, "builder-debug-win-arm64.yml");
169 const x64Path = NodePath.resolve(assetDirectory, "builder-debug-win-x64.yml");
170 const debugFixture = `arm64:
171 firstOrDefaultFilePatterns:
172 - '**/*'
173nsis:
174 script: |-
175 !include "example.nsh"
176`;
177
178 NodeFS.writeFileSync(arm64Path, debugFixture);
179 NodeFS.writeFileSync(x64Path, debugFixture);
180
181 return { arm64Path, x64Path };
182}
183function assertContains(haystack: string, needle: string, message: string): void {
184 if (!haystack.includes(needle)) {
185 throw new Error(message);
186 }
187}
188
189function assertExists(path: string, message: string): void {
190 if (!NodeFS.existsSync(path)) {
191 throw new Error(message);
192 }
193}
194
195function assertPackageVersion(path: string, version: string): void {
196 const packageJson = JSON.parse(NodeFS.readFileSync(path, "utf8")) as {
197 readonly version?: unknown;
198 };
199
200 if (packageJson.version !== version) {
201 throw new Error(`Expected ${path} to have version ${version}.`);
202 }
203}
204
205function assertMissing(path: string, message: string): void {
206 if (NodeFS.existsSync(path)) {
207 throw new Error(message);
208 }
209}
210
211const tempRoot = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-release-smoke-"));
212
213try {
214 NodeChildProcess.execFileSync(
215 process.execPath,
216 ["--test", NodePath.resolve(repoRoot, ".github/scripts/relay-state-output.test.cjs")],
217 { stdio: "inherit" },
218 );
219
220 copyWorkspaceManifestFixture(tempRoot);
221
222 NodeChildProcess.execFileSync(
223 process.execPath,
224 [
225 NodePath.resolve(repoRoot, "scripts/update-release-package-versions.ts"),
226 "9.9.9-smoke.0",
227 "--root",
228 tempRoot,
229 ],
230 {
231 cwd: repoRoot,
232 stdio: "inherit",
233 },
234 );
235
236 NodeFS.rmSync(NodePath.resolve(tempRoot, "pnpm-lock.yaml"), { force: true });
237
238 NodeChildProcess.execFileSync("vp", ["install", "--lockfile-only", "--ignore-scripts"], {
239 cwd: tempRoot,
240 stdio: "inherit",
241 });
242
243 const lockfile = NodeFS.readFileSync(NodePath.resolve(tempRoot, "pnpm-lock.yaml"), "utf8");
244 assertContains(lockfile, "lockfileVersion:", "Expected pnpm-lock.yaml to be regenerated.");
245
246 for (const relativePath of [
247 "apps/server/package.json",
248 "apps/desktop/package.json",
249 "apps/web/package.json",
250 "packages/contracts/package.json",
251 ]) {
252 assertPackageVersion(NodePath.resolve(tempRoot, relativePath), "9.9.9-smoke.0");
253 }
254
255 const nightlyReleaseMetadata = NodeChildProcess.execFileSync(
256 process.execPath,
257 [
258 NodePath.resolve(repoRoot, "scripts/resolve-nightly-release.ts"),
259 "--date",
260 "20260413",
261 "--run-number",
262 "321",
263 "--sha",
264 "abcdef1234567890",
265 "--root",
266 tempRoot,
267 ],
268 {
269 cwd: repoRoot,
270 encoding: "utf8",
271 },
272 );
273 assertContains(
274 nightlyReleaseMetadata,
275 "version=9.9.10-nightly.20260413.321",
276 "Expected nightly metadata to contain the derived nightly version.",
277 );
278 assertContains(
279 nightlyReleaseMetadata,
280 "tag=v9.9.10-nightly.20260413.321",
281 "Expected nightly metadata to contain the derived nightly tag.",
282 );
283 assertContains(
284 nightlyReleaseMetadata,
285 "name=T3 Code Nightly 9.9.10-nightly.20260413.321 (abcdef123456)",
286 "Expected nightly metadata to include the short commit SHA in the release name.",
287 );
288
289 const { arm64Path, x64Path } = writeMacManifestFixtures(tempRoot);
290 NodeChildProcess.execFileSync(
291 process.execPath,
292 [
293 NodePath.resolve(repoRoot, "scripts/merge-update-manifests.ts"),
294 "--platform",
295 "mac",
296 arm64Path,
297 x64Path,
298 ],
299 {
300 cwd: repoRoot,
301 stdio: "inherit",
302 },
303 );
304
305 const mergedManifest = NodeFS.readFileSync(arm64Path, "utf8");
306 assertContains(
307 mergedManifest,
308 "T3-Code-9.9.9-smoke.0-arm64.zip",
309 "Merged manifest is missing the arm64 asset.",
310 );
311 assertContains(
312 mergedManifest,
313 "T3-Code-9.9.9-smoke.0-x64.zip",
314 "Merged manifest is missing the x64 asset.",
315 );
316
317 const { arm64Path: winArm64Path, x64Path: winX64Path } = writeWindowsManifestFixtures(
318 tempRoot,
319 "latest",
320 );
321 const mergedWindowsManifestPath = NodePath.resolve(tempRoot, "release-assets/latest.yml");
322 const { arm64Path: nightlyWinArm64Path, x64Path: nightlyWinX64Path } =
323 writeWindowsManifestFixtures(tempRoot, "nightly");
324 const mergedNightlyWindowsManifestPath = NodePath.resolve(tempRoot, "release-assets/nightly.yml");
325 const { arm64Path: previewWinArm64Path, x64Path: previewWinX64Path } =
326 writeWindowsManifestFixtures(tempRoot, "preview");
327 const mergedPreviewWindowsManifestPath = NodePath.resolve(tempRoot, "release-assets/preview.yml");
328 const { arm64Path: winDebugArm64Path, x64Path: winDebugX64Path } =
329 writeWindowsBuilderDebugFixtures(tempRoot);
330 NodeChildProcess.execFileSync(
331 "bash",
332 [
333 "-lc",
334 `
335 release_assets_dir=${JSON.stringify(NodePath.resolve(tempRoot, "release-assets"))}
336 shopt -s nullglob
337 found_windows_manifest=false
338 for x64_manifest in "$release_assets_dir"/*-win-x64.yml; do
339 if [[ "$(basename "$x64_manifest")" == builder-debug-* ]]; then
340 continue
341 fi
342
343 arm64_manifest="\${x64_manifest/-x64.yml/-arm64.yml}"
344 output_manifest="\${x64_manifest/-win-x64.yml/.yml}"
345 if [[ ! -f "$arm64_manifest" ]]; then
346 echo "Missing matching arm64 Windows manifest for $x64_manifest" >&2
347 exit 1
348 fi
349
350 found_windows_manifest=true
351 ${JSON.stringify(process.execPath)} ${JSON.stringify(NodePath.resolve(repoRoot, "scripts/merge-update-manifests.ts"))} --platform win \
352 "$arm64_manifest" \
353 "$x64_manifest" \
354 "$output_manifest"
355 rm -f "$arm64_manifest" "$x64_manifest"
356 done
357
358 if [[ "$found_windows_manifest" != true ]]; then
359 echo "No Windows updater manifests found to merge." >&2
360 exit 1
361 fi
362 `,
363 ],
364 {
365 cwd: repoRoot,
366 stdio: "inherit",
367 },
368 );
369
370 const mergedWindowsManifest = NodeFS.readFileSync(mergedWindowsManifestPath, "utf8");
371 assertContains(
372 mergedWindowsManifest,
373 "T3-Code-9.9.9-smoke.0-arm64.exe",
374 "Merged Windows manifest is missing the arm64 asset.",
375 );
376 assertContains(
377 mergedWindowsManifest,
378 "T3-Code-9.9.9-smoke.0-x64.exe",
379 "Merged Windows manifest is missing the x64 asset.",
380 );
381 const mergedNightlyWindowsManifest = NodeFS.readFileSync(
382 mergedNightlyWindowsManifestPath,
383 "utf8",
384 );
385 assertContains(
386 mergedNightlyWindowsManifest,
387 "T3-Code-9.9.9-smoke.0-arm64.exe",
388 "Merged nightly Windows manifest is missing the arm64 asset.",
389 );
390 assertContains(
391 mergedNightlyWindowsManifest,
392 "T3-Code-9.9.9-smoke.0-x64.exe",
393 "Merged nightly Windows manifest is missing the x64 asset.",
394 );
395 const mergedPreviewWindowsManifest = NodeFS.readFileSync(
396 mergedPreviewWindowsManifestPath,
397 "utf8",
398 );
399 assertContains(
400 mergedPreviewWindowsManifest,
401 "T3-Code-9.9.9-smoke.0-arm64.exe",
402 "Merged preview Windows manifest is missing the arm64 asset.",
403 );
404 assertContains(
405 mergedPreviewWindowsManifest,
406 "T3-Code-9.9.9-smoke.0-x64.exe",
407 "Merged preview Windows manifest is missing the x64 asset.",
408 );
409 assertMissing(
410 winArm64Path,
411 "Windows release smoke unexpectedly kept the arm64 updater manifest.",
412 );
413 assertMissing(winX64Path, "Windows release smoke unexpectedly kept the x64 updater manifest.");
414 assertMissing(
415 nightlyWinArm64Path,
416 "Windows release smoke unexpectedly kept the nightly arm64 updater manifest.",
417 );
418 assertMissing(
419 nightlyWinX64Path,
420 "Windows release smoke unexpectedly kept the nightly x64 updater manifest.",
421 );
422 assertMissing(
423 previewWinArm64Path,
424 "Windows release smoke unexpectedly kept the preview arm64 updater manifest.",
425 );
426 assertMissing(
427 previewWinX64Path,
428 "Windows release smoke unexpectedly kept the preview x64 updater manifest.",
429 );
430 assertExists(
431 winDebugArm64Path,
432 "Windows release smoke unexpectedly removed the arm64 builder debug fixture.",
433 );
434 assertExists(
435 winDebugX64Path,
436 "Windows release smoke unexpectedly removed the x64 builder debug fixture.",
437 );
438
439 Effect.runSync(Console.log("Release smoke checks passed."));
440} finally {
441 NodeFS.rmSync(tempRoot, { recursive: true, force: true });
442}