authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-05 14:42:44-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 14:42:44-04:00
log311f05c8e333a9f7adfbbf5681b849888670d00a
tree655fd712e33ef380dcb2cbdde1503adfd12f85b9
parent60e1b73948debac845c3dc72aac35c9adbd4cd64
signature Signed by PGP key B5690EEEBB952194

fix(server): install pinned runtime when pnpm node lacks npm (#9923)


2 files changed, 102 insertions(+), 1 deletions(-)

apps/server/src/cloud/pinnedRuntime.test.ts+79
......@@ -5,6 +5,7 @@ import * as Effect from "effect/Effect";
55import * as FileSystem from "effect/FileSystem";
66import * as Fiber from "effect/Fiber";
77import * as Path from "effect/Path";
8import * as PlatformError from "effect/PlatformError";
89import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
910
1011import * as ProcessRunner from "../processRunner.ts";
......@@ -38,6 +39,84 @@ const successfulRunner = (fs: FileSystem.FileSystem, path: Path.Path) =>
3839 });
3940
4041it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => {
42 it.effect("installs through pnpm when its Node runtime has no npm executable", () =>
43 Effect.gen(function* () {
44 const fs = yield* FileSystem.FileSystem;
45 const path = yield* Path.Path;
46 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-pnpm-" });
47 const commands: Array<ProcessRunner.ProcessRunInput> = [];
48 const install = successfulRunner(fs, path);
49 const paths = yield* ensurePinnedRuntimeInstalled({
50 baseDir,
51 version: "1.2.3",
52 fs,
53 path,
54 runner: ProcessRunner.ProcessRunner.of({
55 run: (input) => {
56 commands.push(input);
57 return input.command === "npm"
58 ? Effect.fail(
59 new ProcessRunner.ProcessSpawnError({
60 command: "npm",
61 argumentCount: input.args.length,
62 cause: PlatformError.systemError({
63 _tag: "NotFound",
64 module: "ChildProcess",
65 method: "spawn",
66 }),
67 }),
68 )
69 : install.run(input);
70 },
71 }),
72 validate: (staging) =>
73 fs.exists(staging.entryPath).pipe(
74 Effect.flatMap((exists) => (exists ? Effect.void : Effect.die("missing runtime"))),
75 Effect.orDie,
76 ),
77 });
78 assert.deepEqual(
79 commands.map((command) => command.command),
80 ["npm", "pnpm"],
81 );
82 assert.deepEqual(commands[1]!.args, ["--package=npm@11", "dlx", "npm", ...commands[0]!.args]);
83 assert.equal(yield* fs.readFileString(paths.sentinelPath), "1.2.3\n");
84 }),
85 );
86
87 it.effect("does not try a different installer for npm permission failures", () =>
88 Effect.gen(function* () {
89 const fs = yield* FileSystem.FileSystem;
90 const path = yield* Path.Path;
91 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-permission-" });
92 const commands: string[] = [];
93 yield* ensurePinnedRuntimeInstalled({
94 baseDir,
95 version: "1.2.3",
96 fs,
97 path,
98 runner: ProcessRunner.ProcessRunner.of({
99 run: (input) => {
100 commands.push(input.command);
101 return Effect.fail(
102 new ProcessRunner.ProcessSpawnError({
103 command: input.command,
104 argumentCount: input.args.length,
105 cause: PlatformError.systemError({
106 _tag: "PermissionDenied",
107 module: "ChildProcess",
108 method: "spawn",
109 }),
110 }),
111 );
112 },
113 }),
114 validate: () => Effect.die("must not validate a failed install"),
115 }).pipe(Effect.flip);
116 assert.deepEqual(commands, ["npm"]);
117 }),
118 );
119
41120 it.effect("validates a staging tree before atomically publishing it", () =>
42121 Effect.gen(function* () {
43122 const fs = yield* FileSystem.FileSystem;
apps/server/src/cloud/pinnedRuntime.ts+23-1
......@@ -2,6 +2,7 @@ import * as Duration from "effect/Duration";
22import * as Effect from "effect/Effect";
33import * as FileSystem from "effect/FileSystem";
44import * as Path from "effect/Path";
5import * as PlatformError from "effect/PlatformError";
56import * as Schema from "effect/Schema";
67import * as Option from "effect/Option";
78import * as Semaphore from "effect/Semaphore";
......@@ -152,14 +153,35 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")(
152153
153154 return yield* Effect.gen(function* () {
154155 const installStep = "installing the pinned t3 runtime (this can take a few minutes)";
156 const installArgs = [
157 "install",
158 "--prefix",
159 stagingDir,
160 "--no-fund",
161 "--no-audit",
162 `t3@${input.version}`,
163 ];
155164 yield* runner
156165 .run({
157166 command: "npm",
158 args: ["install", "--prefix", stagingDir, "--no-fund", "--no-audit", `t3@${input.version}`],
167 args: installArgs,
159168 // Native dependencies may compile from source on slower machines.
160169 timeout: PINNED_RUNTIME_INSTALL_TIMEOUT,
161170 })
162171 .pipe(
172 Effect.catchTags({
173 ProcessSpawnError: (error) =>
174 error.cause instanceof PlatformError.PlatformError &&
175 error.cause.reason._tag === "NotFound"
176 ? // pnpm-managed Node installations do not include npm. Keep npm
177 // installation semantics for the pinned runtime and native builds.
178 runner.run({
179 command: "pnpm",
180 args: ["--package=npm@11", "dlx", "npm", ...installArgs],
181 timeout: PINNED_RUNTIME_INSTALL_TIMEOUT,
182 })
183 : Effect.fail(error),
184 }),
163185 Effect.mapError((cause) => new PinnedRuntimeInstallError({ step: installStep, cause })),
164186 Effect.filterOrFail(
165187 (result) => result.code === 0,