History

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

maria committed 2 months ago

Commit 311f05c1 parent 60e1b732 files · +102 −1Browse files

Unified · Split

2 files
  1. apps/server/src/cloud/pinnedRuntime.test.ts +79 −0
  2. apps/server/src/cloud/pinnedRuntime.ts +23 −1
apps/server/src/cloud/pinnedRuntime.test.ts+79−0
@@ -5,6 +5,7 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines
55 import * as FileSystem from "effect/FileSystem";
66 import * as Fiber from "effect/Fiber";
77 import * as Path from "effect/Path";
8+import * as PlatformError from "effect/PlatformError";
89 import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
910
1011 import * as ProcessRunner from "../processRunner.ts";
@@ -38,6 +39,84 @@ const successfulRunner = (fs: FileSystem.FileSystem, path: Path.Path) => · 27 unmodified lines
3839 });
3940
4041 it.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"; · 1 unmodified line
22 import * as Effect from "effect/Effect";
33 import * as FileSystem from "effect/FileSystem";
44 import * as Path from "effect/Path";
5+import * as PlatformError from "effect/PlatformError";
56 import * as Schema from "effect/Schema";
67 import * as Option from "effect/Option";
78 import * as Semaphore from "effect/Semaphore";
@@ -152,14 +153,35 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( · 144 unmodified lines
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,

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t