From cf3196c1126925b87a8ea8976611922acbec897f Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 8 Mar 2026 19:59:33 -0700 Subject: [PATCH 1/2] 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 --- .github/workflows/release.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e8cf0fdfc..d72fcbaf88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: From 5483245a031ab33c18caa0d7fbd3c913d304e440 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 8 Mar 2026 20:25:21 -0700 Subject: [PATCH 2/2] Fix release finalize lockfile refresh --- .github/workflows/ci.yml | 20 ++++ .github/workflows/release.yml | 30 +----- package.json | 1 + scripts/release-smoke.ts | 113 +++++++++++++++++++++ scripts/update-release-package-versions.ts | 111 ++++++++++++++++++++ 5 files changed, 247 insertions(+), 28 deletions(-) create mode 100644 scripts/release-smoke.ts create mode 100644 scripts/update-release-package-versions.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 787e07bce7..8035a57ed8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d72fcbaf88..1524675138 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -322,33 +322,7 @@ 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' @@ -356,7 +330,7 @@ jobs: - 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' diff --git a/package.json b/package.json index 2fa94b5dab..be86eeb93e 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts new file mode 100644 index 0000000000..9e7595e619 --- /dev/null +++ b/scripts/release-smoke.ts @@ -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 }); +} diff --git a/scripts/update-release-package-versions.ts b/scripts/update-release-package-versions.ts new file mode 100644 index 0000000000..7cdf13dd39 --- /dev/null +++ b/scripts/update-release-package-versions.ts @@ -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): { + 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 [--root ] [--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`); + } +}