authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-30 21:28:25-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-30 21:28:25-07:00
log67b175a4c916ac0d5dbac809da22271db3f542d7
tree2925f5ff8b2e78551e810f132c90062fe28d29fa
parent0cf482b08bca0249ec94fbcbd0eb04fff28d9b3d
signature Signed by PGP key B5690EEEBB952194

perf(release): build and publish npm platform packages concurrently (#14028)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

4 files changed, 101 insertions(+), 15 deletions(-)

apps/server/scripts/cli.ts+6-2
......@@ -19,6 +19,7 @@ import {
1919 ServerCliDevelopmentIconTargetMissingError,
2020 ServerCliExecutableImportError,
2121} from "./cliErrors.ts";
22import { publishPlatformsThenLauncher } from "./publishOrder.ts";
2223
2324const RepoRoot = Effect.service(Path.Path).pipe(
2425 Effect.flatMap((path) => path.fromFileUrl(new URL("../../..", import.meta.url))),
......@@ -212,7 +213,7 @@ const publishCmd = Command.make(
212213 if (config.provenance) args.push("--provenance");
213214 if (config.dryRun) args.push("--dry-run");
214215
215 for (const tarball of [...platformTarballs, launcherTarball]) {
216 const publish = Effect.fn("publish")(function* (tarball: string) {
216217 const spawnCommand = yield* resolveSpawnCommand("npm", [...args, tarball]);
217218 yield* Effect.log(`[cli] npm ${args.join(" ")} ${path.basename(tarball)}`);
218219 yield* runCommand(
......@@ -223,7 +224,10 @@ const publishCmd = Command.make(
223224 shell: spawnCommand.shell,
224225 }),
225226 );
226 }
227 });
228
229 // Each publish takes about 17s, so the platform packages go at once.
230 yield* publishPlatformsThenLauncher({ platformTarballs, launcherTarball, publish });
227231 }),
228232).pipe(
229233 Command.withDescription(
apps/server/scripts/publishOrder.test.ts created+58
......@@ -0,0 +1,58 @@
1import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Exit from "effect/Exit";
4import * as Latch from "effect/Latch";
5
6import { publishPlatformsThenLauncher } from "./publishOrder.ts";
7
8const platformTarballs = ["a.tgz", "b.tgz", "c.tgz"];
9const launcherTarball = "t3.tgz";
10
11// A fake publish that records when each upload starts and ends. Platform
12// uploads only end once every platform upload has started, so they have to
13// run at the same time. `failing` fails as soon as it starts.
14const fakePublish = Effect.fn("fakePublish")(function* (failing?: string) {
15 const events: Array<string> = [];
16 const allStarted = yield* Latch.make();
17 let started = 0;
18 const publish = (tarball: string) =>
19 Effect.gen(function* () {
20 events.push(`start ${tarball}`);
21 if (tarball === launcherTarball) return;
22 started += 1;
23 if (started === platformTarballs.length) yield* allStarted.open;
24 if (tarball === failing) return yield* Effect.fail(`${tarball} failed`);
25 yield* allStarted.await;
26 events.push(`end ${tarball}`);
27 });
28 return { events, publish };
29});
30
31it.effect("publishes the platform packages at once and the launcher last", () =>
32 Effect.gen(function* () {
33 const { events, publish } = yield* fakePublish();
34
35 yield* publishPlatformsThenLauncher({ platformTarballs, launcherTarball, publish });
36
37 assert.deepStrictEqual(events.slice(0, 3).toSorted(), [
38 "start a.tgz",
39 "start b.tgz",
40 "start c.tgz",
41 ]);
42 assert.strictEqual(events.at(-1), `start ${launcherTarball}`);
43 }),
44);
45
46it.effect("finishes the other uploads and skips the launcher when one fails", () =>
47 Effect.gen(function* () {
48 const { events, publish } = yield* fakePublish("b.tgz");
49
50 const exit = yield* Effect.exit(
51 publishPlatformsThenLauncher({ platformTarballs, launcherTarball, publish }),
52 );
53
54 assert.deepStrictEqual(exit, Exit.fail("b.tgz failed"));
55 assert.includeMembers(events, ["end a.tgz", "end c.tgz"]);
56 assert.notInclude(events, `start ${launcherTarball}`);
57 }),
58);
apps/server/scripts/publishOrder.ts created+26
......@@ -0,0 +1,26 @@
1import * as Effect from "effect/Effect";
2import * as Result from "effect/Result";
3
4/**
5 * Publishes every platform package at once, then the launcher, which must
6 * never be installable before the executables it points to. When one platform
7 * fails, the other uploads still finish rather than being interrupted: npm
8 * never accepts the same version twice, and an interrupted upload may already
9 * be live. The launcher is then skipped and the first failure is returned.
10 */
11export const publishPlatformsThenLauncher = <E, R>(input: {
12 readonly platformTarballs: ReadonlyArray<string>;
13 readonly launcherTarball: string;
14 readonly publish: (tarball: string) => Effect.Effect<void, E, R>;
15}) =>
16 Effect.gen(function* () {
17 const results = yield* Effect.all(input.platformTarballs.map(input.publish), {
18 concurrency: "unbounded",
19 mode: "result",
20 });
21 const failed = results.find(Result.isFailure);
22 if (failed) {
23 return yield* Effect.fail(failed.failure);
24 }
25 yield* input.publish(input.launcherTarball);
26 });
scripts/build-npm-platform-packages.ts+11-13
......@@ -429,24 +429,22 @@ export const buildNpmPlatformPackages = Effect.fn("buildNpmPlatformPackages")(fu
429429 yield* fs.makeDirectory(path.join(input.outputDir, NPM_PLATFORM_PACKAGE_SCOPE), {
430430 recursive: true,
431431 });
432 const outputs: Array<NpmPackageOutput> = [];
433 for (const { key, archive } of archives) {
434 outputs.push(
435 yield* stagePlatformPackage({
436 key,
437 archive,
438 outputDir: input.outputDir,
439 version: input.version,
440 }),
441 );
442 }
443 outputs.push(
432 // Each archive stages in its own scratch dir, so all of them unpack and
433 // compress at once. Sequentially this took about 45s for five archives.
434 const platformOutputs = yield* Effect.forEach(
435 archives,
436 ({ key, archive }) =>
437 stagePlatformPackage({ key, archive, outputDir: input.outputDir, version: input.version }),
438 { concurrency: "unbounded" },
439 );
440 const outputs = [
441 ...platformOutputs,
444442 yield* stageLauncherPackage({
445443 outputDir: input.outputDir,
446444 version: input.version,
447445 platformKeys: archives.map((entry) => entry.key),
448446 }),
449 );
447 ];
450448
451449 for (const output of outputs) {
452450 yield* Effect.log(`[npm-packages] Wrote ${output.packageDir} and ${output.tarball}`);