Skip to content
Source
Sign inSign up

release-smoke.ts

450 lines13.8 KB

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