authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-03-08 20:30:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-08 20:30:01-07:00
log9b9006fed571dbeb1c810493500a679a49a2ba38
tree5e257d555d7a33515c737015efe4c61aa3f95916
parent42234b8f7cbe4465370fa0511f3fe26df2a2ae2e
signature Signed by PGP key B5690EEEBB952194

Fix release finalize lockfile refresh (#608)

* Add checkout and Node setup to release updater publish job - Ensure the updater publish job checks out the target ref - Initialize Node using the repository's package.json version file before downloading artifacts * Fix release finalize lockfile refresh

5 files changed, 247 insertions(+), 28 deletions(-)

.github/workflows/ci.yml+20
...@@ -69,3 +69,23 @@ jobs:...@@ -69,3 +69,23 @@ jobs:
69 run: |69 run: |
70 test -f apps/desktop/dist-electron/preload.js70 test -f apps/desktop/dist-electron/preload.js
71 grep -nE "desktopBridge|getWsUrl|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js71 grep -nE "desktopBridge|getWsUrl|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js
72
73 release_smoke:
74 name: Release Smoke
75 runs-on: ubuntu-24.04
76 steps:
77 - name: Checkout
78 uses: actions/checkout@v4
79
80 - name: Setup Bun
81 uses: oven-sh/setup-bun@v2
82 with:
83 bun-version-file: package.json
84
85 - name: Setup Node
86 uses: actions/setup-node@v4
87 with:
88 node-version-file: package.json
89
90 - name: Exercise release-only workflow steps
91 run: node scripts/release-smoke.ts
.github/workflows/release.yml+2-28
...@@ -322,33 +322,7 @@ jobs:...@@ -322,33 +322,7 @@ jobs:
322 name: Update version strings322 name: Update version strings
323 env:323 env:
324 RELEASE_VERSION: ${{ needs.preflight.outputs.version }}324 RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
325 run: |325 run: node scripts/update-release-package-versions.ts "$RELEASE_VERSION" --github-output
326 node --input-type=module -e '
327 import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
328
329 const files = [
330 "apps/server/package.json",
331 "apps/desktop/package.json",
332 "apps/web/package.json",
333 "packages/contracts/package.json",
334 ];
335
336 let changed = false;
337 for (const file of files) {
338 const packageJson = JSON.parse(readFileSync(file, "utf8"));
339 if (packageJson.version !== process.env.RELEASE_VERSION) {
340 packageJson.version = process.env.RELEASE_VERSION;
341 writeFileSync(file, `${JSON.stringify(packageJson, null, 2)}\n`);
342 changed = true;
343 }
344 }
345
346 if (!changed) {
347 console.log("All package.json versions already match release version.");
348 }
349
350 appendFileSync(process.env.GITHUB_OUTPUT, `changed=${changed}\n`);
351 '
352326
353 - name: Format package.json files327 - name: Format package.json files
354 if: steps.update_versions.outputs.changed == 'true'328 if: steps.update_versions.outputs.changed == 'true'
...@@ -356,7 +330,7 @@ jobs:...@@ -356,7 +330,7 @@ jobs:
356330
357 - name: Refresh lockfile331 - name: Refresh lockfile
358 if: steps.update_versions.outputs.changed == 'true'332 if: steps.update_versions.outputs.changed == 'true'
359 run: bun install333 run: bun install --lockfile-only --ignore-scripts
360334
361 - name: Commit and push version bump335 - name: Commit and push version bump
362 if: steps.update_versions.outputs.changed == 'true'336 if: steps.update_versions.outputs.changed == 'true'
package.json+1
...@@ -45,6 +45,7 @@...@@ -45,6 +45,7 @@
45 "dist:desktop:dmg:x64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch x64",45 "dist:desktop:dmg:x64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch x64",
46 "dist:desktop:linux": "node scripts/build-desktop-artifact.ts --platform linux --target AppImage --arch x64",46 "dist:desktop:linux": "node scripts/build-desktop-artifact.ts --platform linux --target AppImage --arch x64",
47 "dist:desktop:win": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64",47 "dist:desktop:win": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64",
48 "release:smoke": "node scripts/release-smoke.ts",
48 "clean": "rm -rf node_modules apps/*/node_modules packages/*/node_modules apps/*/dist apps/*/dist-electron packages/*/dist .turbo apps/*/.turbo packages/*/.turbo",49 "clean": "rm -rf node_modules apps/*/node_modules packages/*/node_modules apps/*/dist apps/*/dist-electron packages/*/dist .turbo apps/*/.turbo packages/*/.turbo",
49 "sync:vscode-icons": "node scripts/sync-vscode-icons.mjs"50 "sync:vscode-icons": "node scripts/sync-vscode-icons.mjs"
50 },51 },
scripts/release-smoke.ts created+113
...@@ -0,0 +1,113 @@
1import { execFileSync } from "node:child_process";
2import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3import { tmpdir } from "node:os";
4import { dirname, join, resolve } from "node:path";
5import { fileURLToPath } from "node:url";
6
7const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
9const workspaceFiles = [
10 "package.json",
11 "bun.lock",
12 "apps/server/package.json",
13 "apps/desktop/package.json",
14 "apps/web/package.json",
15 "apps/marketing/package.json",
16 "packages/contracts/package.json",
17 "packages/shared/package.json",
18 "scripts/package.json",
19] as const;
20
21function copyWorkspaceManifestFixture(targetRoot: string): void {
22 for (const relativePath of workspaceFiles) {
23 const sourcePath = resolve(repoRoot, relativePath);
24 const destinationPath = resolve(targetRoot, relativePath);
25 mkdirSync(dirname(destinationPath), { recursive: true });
26 cpSync(sourcePath, destinationPath);
27 }
28}
29
30function writeMacManifestFixtures(targetRoot: string): { arm64Path: string; x64Path: string } {
31 const assetDirectory = resolve(targetRoot, "release-assets");
32 mkdirSync(assetDirectory, { recursive: true });
33
34 const arm64Path = resolve(assetDirectory, "latest-mac.yml");
35 const x64Path = resolve(assetDirectory, "latest-mac-x64.yml");
36
37 writeFileSync(
38 arm64Path,
39 `version: 9.9.9-smoke.0
40files:
41 - url: T3-Code-9.9.9-smoke.0-arm64.zip
42 sha512: arm64zip
43 size: 125621344
44 - url: T3-Code-9.9.9-smoke.0-arm64.dmg
45 sha512: arm64dmg
46 size: 131754935
47path: T3-Code-9.9.9-smoke.0-arm64.zip
48sha512: arm64zip
49releaseDate: '2026-03-08T10:32:14.587Z'
50`,
51 );
52
53 writeFileSync(
54 x64Path,
55 `version: 9.9.9-smoke.0
56files:
57 - url: T3-Code-9.9.9-smoke.0-x64.zip
58 sha512: x64zip
59 size: 132000112
60 - url: T3-Code-9.9.9-smoke.0-x64.dmg
61 sha512: x64dmg
62 size: 138148807
63path: T3-Code-9.9.9-smoke.0-x64.zip
64sha512: x64zip
65releaseDate: '2026-03-08T10:36:07.540Z'
66`,
67 );
68
69 return { arm64Path, x64Path };
70}
71
72function assertContains(haystack: string, needle: string, message: string): void {
73 if (!haystack.includes(needle)) {
74 throw new Error(message);
75 }
76}
77
78const tempRoot = mkdtempSync(join(tmpdir(), "t3-release-smoke-"));
79
80try {
81 copyWorkspaceManifestFixture(tempRoot);
82
83 execFileSync(
84 process.execPath,
85 [resolve(repoRoot, "scripts/update-release-package-versions.ts"), "9.9.9-smoke.0", "--root", tempRoot],
86 {
87 cwd: repoRoot,
88 stdio: "inherit",
89 },
90 );
91
92 execFileSync("bun", ["install", "--lockfile-only", "--ignore-scripts"], {
93 cwd: tempRoot,
94 stdio: "inherit",
95 });
96
97 const lockfile = readFileSync(resolve(tempRoot, "bun.lock"), "utf8");
98 assertContains(lockfile, `"version": "9.9.9-smoke.0"`, "Expected bun.lock to contain the smoke version.");
99
100 const { arm64Path, x64Path } = writeMacManifestFixtures(tempRoot);
101 execFileSync(process.execPath, [resolve(repoRoot, "scripts/merge-mac-update-manifests.ts"), arm64Path, x64Path], {
102 cwd: repoRoot,
103 stdio: "inherit",
104 });
105
106 const mergedManifest = readFileSync(arm64Path, "utf8");
107 assertContains(mergedManifest, "T3-Code-9.9.9-smoke.0-arm64.zip", "Merged manifest is missing the arm64 asset.");
108 assertContains(mergedManifest, "T3-Code-9.9.9-smoke.0-x64.zip", "Merged manifest is missing the x64 asset.");
109
110 console.log("Release smoke checks passed.");
111} finally {
112 rmSync(tempRoot, { recursive: true, force: true });
113}
scripts/update-release-package-versions.ts created+111
...@@ -0,0 +1,111 @@
1import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
2import { resolve } from "node:path";
3import { fileURLToPath } from "node:url";
4
5export const releasePackageFiles = [
6 "apps/server/package.json",
7 "apps/desktop/package.json",
8 "apps/web/package.json",
9 "packages/contracts/package.json",
10] as const;
11
12interface UpdateReleasePackageVersionsOptions {
13 readonly rootDir?: string;
14}
15
16interface MutablePackageJson {
17 version?: string;
18 [key: string]: unknown;
19}
20
21export function updateReleasePackageVersions(
22 version: string,
23 options: UpdateReleasePackageVersionsOptions = {},
24): { changed: boolean } {
25 const rootDir = resolve(options.rootDir ?? process.cwd());
26 let changed = false;
27
28 for (const relativePath of releasePackageFiles) {
29 const filePath = resolve(rootDir, relativePath);
30 const packageJson = JSON.parse(readFileSync(filePath, "utf8")) as MutablePackageJson;
31 if (packageJson.version === version) {
32 continue;
33 }
34
35 packageJson.version = version;
36 writeFileSync(filePath, `${JSON.stringify(packageJson, null, 2)}\n`);
37 changed = true;
38 }
39
40 return { changed };
41}
42
43function parseArgs(argv: ReadonlyArray<string>): {
44 version: string;
45 rootDir: string | undefined;
46 writeGithubOutput: boolean;
47} {
48 let version: string | undefined;
49 let rootDir: string | undefined;
50 let writeGithubOutput = false;
51
52 for (let index = 0; index < argv.length; index += 1) {
53 const argument = argv[index];
54 if (argument === undefined) {
55 continue;
56 }
57
58 if (argument === "--github-output") {
59 writeGithubOutput = true;
60 continue;
61 }
62
63 if (argument === "--root") {
64 rootDir = argv[index + 1];
65 if (!rootDir) {
66 throw new Error("Missing value for --root.");
67 }
68 index += 1;
69 continue;
70 }
71
72 if (argument.startsWith("--")) {
73 throw new Error(`Unknown argument: ${argument}`);
74 }
75
76 if (version !== undefined) {
77 throw new Error("Only one release version can be provided.");
78 }
79 version = argument;
80 }
81
82 if (!version) {
83 throw new Error(
84 "Usage: node scripts/update-release-package-versions.ts <version> [--root <path>] [--github-output]",
85 );
86 }
87
88 return { version, rootDir, writeGithubOutput };
89}
90
91const isMain = process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
92
93if (isMain) {
94 const { version, rootDir, writeGithubOutput } = parseArgs(process.argv.slice(2));
95 const { changed } = updateReleasePackageVersions(
96 version,
97 rootDir === undefined ? {} : { rootDir },
98 );
99
100 if (!changed) {
101 console.log("All package.json versions already match release version.");
102 }
103
104 if (writeGithubOutput) {
105 const githubOutputPath = process.env.GITHUB_OUTPUT;
106 if (!githubOutputPath) {
107 throw new Error("GITHUB_OUTPUT is required when --github-output is set.");
108 }
109 appendFileSync(githubOutputPath, `changed=${changed}\n`);
110 }
111}