| 1 | // @effect-diagnostics nodeBuiltinImport:off |
| 2 | import * as NodeChildProcess from "node:child_process"; |
| 3 | import * as NodeFS from "node:fs"; |
| 4 | import * as NodeOS from "node:os"; |
| 5 | import * as NodePath from "node:path"; |
| 6 | import * as NodeURL from "node:url"; |
| 7 | import * as Console from "effect/Console"; |
| 8 | import * as Effect from "effect/Effect"; |
| 9 | |
| 10 | const repoRoot = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), ".."); |
| 11 | |
| 12 | const 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 | |
| 53 | function 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 | |
| 74 | function 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 |
| 84 | files: |
| 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 |
| 91 | path: T3-Code-9.9.9-smoke.0-arm64.zip |
| 92 | sha512: arm64zip |
| 93 | releaseDate: '2026-03-08T10:32:14.587Z' |
| 94 | `, |
| 95 | ); |
| 96 | |
| 97 | NodeFS.writeFileSync( |
| 98 | x64Path, |
| 99 | `version: 9.9.9-smoke.0 |
| 100 | files: |
| 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 |
| 107 | path: T3-Code-9.9.9-smoke.0-x64.zip |
| 108 | sha512: x64zip |
| 109 | releaseDate: '2026-03-08T10:36:07.540Z' |
| 110 | `, |
| 111 | ); |
| 112 | |
| 113 | return { arm64Path, x64Path }; |
| 114 | } |
| 115 | |
| 116 | function 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 |
| 129 | files: |
| 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 |
| 136 | path: T3-Code-9.9.9-smoke.0-arm64.exe |
| 137 | sha512: arm64exe |
| 138 | releaseDate: '2026-03-08T10:32:14.587Z' |
| 139 | `, |
| 140 | ); |
| 141 | |
| 142 | NodeFS.writeFileSync( |
| 143 | x64Path, |
| 144 | `version: 9.9.9-smoke.0 |
| 145 | files: |
| 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 |
| 152 | path: T3-Code-9.9.9-smoke.0-x64.exe |
| 153 | sha512: x64exe |
| 154 | releaseDate: '2026-03-08T10:36:07.540Z' |
| 155 | `, |
| 156 | ); |
| 157 | |
| 158 | return { arm64Path, x64Path }; |
| 159 | } |
| 160 | |
| 161 | function 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 | - '**/*' |
| 173 | nsis: |
| 174 | script: |- |
| 175 | !include "example.nsh" |
| 176 | `; |
| 177 | |
| 178 | NodeFS.writeFileSync(arm64Path, debugFixture); |
| 179 | NodeFS.writeFileSync(x64Path, debugFixture); |
| 180 | |
| 181 | return { arm64Path, x64Path }; |
| 182 | } |
| 183 | function assertContains(haystack: string, needle: string, message: string): void { |
| 184 | if (!haystack.includes(needle)) { |
| 185 | throw new Error(message); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | function assertExists(path: string, message: string): void { |
| 190 | if (!NodeFS.existsSync(path)) { |
| 191 | throw new Error(message); |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | function 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 | |
| 205 | function assertMissing(path: string, message: string): void { |
| 206 | if (NodeFS.existsSync(path)) { |
| 207 | throw new Error(message); |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | const tempRoot = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-release-smoke-")); |
| 212 | |
| 213 | try { |
| 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 | } |