Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,23 @@ jobs:
run: |
test -f apps/desktop/dist-electron/preload.js
grep -nE "desktopBridge|getWsUrl|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js

release_smoke:
name: Release Smoke
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: package.json

- name: Exercise release-only workflow steps
run: node scripts/release-smoke.ts
40 changes: 12 additions & 28 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ jobs:
needs: [preflight, build, publish_cli]
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.preflight.outputs.ref }}

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: package.json

- name: Download all desktop artifacts
uses: actions/download-artifact@v4
with:
Expand Down Expand Up @@ -312,41 +322,15 @@ jobs:
name: Update version strings
env:
RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
run: |
node --input-type=module -e '
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";

const files = [
"apps/server/package.json",
"apps/desktop/package.json",
"apps/web/package.json",
"packages/contracts/package.json",
];

let changed = false;
for (const file of files) {
const packageJson = JSON.parse(readFileSync(file, "utf8"));
if (packageJson.version !== process.env.RELEASE_VERSION) {
packageJson.version = process.env.RELEASE_VERSION;
writeFileSync(file, `${JSON.stringify(packageJson, null, 2)}\n`);
changed = true;
}
}

if (!changed) {
console.log("All package.json versions already match release version.");
}

appendFileSync(process.env.GITHUB_OUTPUT, `changed=${changed}\n`);
'
run: node scripts/update-release-package-versions.ts "$RELEASE_VERSION" --github-output

- name: Format package.json files
if: steps.update_versions.outputs.changed == 'true'
run: bunx oxfmt apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json

- name: Refresh lockfile
if: steps.update_versions.outputs.changed == 'true'
run: bun install
run: bun install --lockfile-only --ignore-scripts

- name: Commit and push version bump
if: steps.update_versions.outputs.changed == 'true'
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"dist:desktop:dmg:x64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch x64",
"dist:desktop:linux": "node scripts/build-desktop-artifact.ts --platform linux --target AppImage --arch x64",
"dist:desktop:win": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64",
"release:smoke": "node scripts/release-smoke.ts",
"clean": "rm -rf node_modules apps/*/node_modules packages/*/node_modules apps/*/dist apps/*/dist-electron packages/*/dist .turbo apps/*/.turbo packages/*/.turbo",
"sync:vscode-icons": "node scripts/sync-vscode-icons.mjs"
},
Expand Down
113 changes: 113 additions & 0 deletions scripts/release-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { execFileSync } from "node:child_process";
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");

const workspaceFiles = [
"package.json",
"bun.lock",
"apps/server/package.json",
"apps/desktop/package.json",
"apps/web/package.json",
"apps/marketing/package.json",
"packages/contracts/package.json",
"packages/shared/package.json",
"scripts/package.json",
] as const;

function copyWorkspaceManifestFixture(targetRoot: string): void {
for (const relativePath of workspaceFiles) {
const sourcePath = resolve(repoRoot, relativePath);
const destinationPath = resolve(targetRoot, relativePath);
mkdirSync(dirname(destinationPath), { recursive: true });
cpSync(sourcePath, destinationPath);
}
}

function writeMacManifestFixtures(targetRoot: string): { arm64Path: string; x64Path: string } {
const assetDirectory = resolve(targetRoot, "release-assets");
mkdirSync(assetDirectory, { recursive: true });

const arm64Path = resolve(assetDirectory, "latest-mac.yml");
const x64Path = resolve(assetDirectory, "latest-mac-x64.yml");

writeFileSync(
arm64Path,
`version: 9.9.9-smoke.0
files:
- url: T3-Code-9.9.9-smoke.0-arm64.zip
sha512: arm64zip
size: 125621344
- url: T3-Code-9.9.9-smoke.0-arm64.dmg
sha512: arm64dmg
size: 131754935
path: T3-Code-9.9.9-smoke.0-arm64.zip
sha512: arm64zip
releaseDate: '2026-03-08T10:32:14.587Z'
`,
);

writeFileSync(
x64Path,
`version: 9.9.9-smoke.0
files:
- url: T3-Code-9.9.9-smoke.0-x64.zip
sha512: x64zip
size: 132000112
- url: T3-Code-9.9.9-smoke.0-x64.dmg
sha512: x64dmg
size: 138148807
path: T3-Code-9.9.9-smoke.0-x64.zip
sha512: x64zip
releaseDate: '2026-03-08T10:36:07.540Z'
`,
);

return { arm64Path, x64Path };
}

function assertContains(haystack: string, needle: string, message: string): void {
if (!haystack.includes(needle)) {
throw new Error(message);
}
}

const tempRoot = mkdtempSync(join(tmpdir(), "t3-release-smoke-"));

try {
copyWorkspaceManifestFixture(tempRoot);

execFileSync(
process.execPath,
[resolve(repoRoot, "scripts/update-release-package-versions.ts"), "9.9.9-smoke.0", "--root", tempRoot],
{
cwd: repoRoot,
stdio: "inherit",
},
);

execFileSync("bun", ["install", "--lockfile-only", "--ignore-scripts"], {
cwd: tempRoot,
stdio: "inherit",
});

const lockfile = readFileSync(resolve(tempRoot, "bun.lock"), "utf8");
assertContains(lockfile, `"version": "9.9.9-smoke.0"`, "Expected bun.lock to contain the smoke version.");

const { arm64Path, x64Path } = writeMacManifestFixtures(tempRoot);
execFileSync(process.execPath, [resolve(repoRoot, "scripts/merge-mac-update-manifests.ts"), arm64Path, x64Path], {
cwd: repoRoot,
stdio: "inherit",
});

const mergedManifest = readFileSync(arm64Path, "utf8");
assertContains(mergedManifest, "T3-Code-9.9.9-smoke.0-arm64.zip", "Merged manifest is missing the arm64 asset.");
assertContains(mergedManifest, "T3-Code-9.9.9-smoke.0-x64.zip", "Merged manifest is missing the x64 asset.");

console.log("Release smoke checks passed.");
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
111 changes: 111 additions & 0 deletions scripts/update-release-package-versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

export const releasePackageFiles = [
"apps/server/package.json",
"apps/desktop/package.json",
"apps/web/package.json",
"packages/contracts/package.json",
] as const;

interface UpdateReleasePackageVersionsOptions {
readonly rootDir?: string;
}

interface MutablePackageJson {
version?: string;
[key: string]: unknown;
}

export function updateReleasePackageVersions(
version: string,
options: UpdateReleasePackageVersionsOptions = {},
): { changed: boolean } {
const rootDir = resolve(options.rootDir ?? process.cwd());
let changed = false;

for (const relativePath of releasePackageFiles) {
const filePath = resolve(rootDir, relativePath);
const packageJson = JSON.parse(readFileSync(filePath, "utf8")) as MutablePackageJson;
if (packageJson.version === version) {
continue;
}

packageJson.version = version;
writeFileSync(filePath, `${JSON.stringify(packageJson, null, 2)}\n`);
changed = true;
}

return { changed };
}

function parseArgs(argv: ReadonlyArray<string>): {
version: string;
rootDir: string | undefined;
writeGithubOutput: boolean;
} {
let version: string | undefined;
let rootDir: string | undefined;
let writeGithubOutput = false;

for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === undefined) {
continue;
}

if (argument === "--github-output") {
writeGithubOutput = true;
continue;
}

if (argument === "--root") {
rootDir = argv[index + 1];
if (!rootDir) {
throw new Error("Missing value for --root.");
}
index += 1;
continue;
}

if (argument.startsWith("--")) {
throw new Error(`Unknown argument: ${argument}`);
}

if (version !== undefined) {
throw new Error("Only one release version can be provided.");
}
version = argument;
}

if (!version) {
throw new Error(
"Usage: node scripts/update-release-package-versions.ts <version> [--root <path>] [--github-output]",
);
}

return { version, rootDir, writeGithubOutput };
}

const isMain = process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url);

if (isMain) {
const { version, rootDir, writeGithubOutput } = parseArgs(process.argv.slice(2));
const { changed } = updateReleasePackageVersions(
version,
rootDir === undefined ? {} : { rootDir },
);

if (!changed) {
console.log("All package.json versions already match release version.");
}

if (writeGithubOutput) {
const githubOutputPath = process.env.GITHUB_OUTPUT;
if (!githubOutputPath) {
throw new Error("GITHUB_OUTPUT is required when --github-output is set.");
}
appendFileSync(githubOutputPath, `changed=${changed}\n`);
}
}