diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts
index d3a8af2320c4f..d33215a92dcc8 100644
--- a/apps/server/src/cloud/bootService.test.ts
+++ b/apps/server/src/cloud/bootService.test.ts
@@ -151,6 +151,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* (
cliVersion = "1.2.3",
serviceBaseDir = baseDir,
insideService = false,
+ hostExecPath = "/usr/bin/t3",
) =>
Effect.gen(function* () {
const paths = pinnedRuntimePaths(path, serviceBaseDir, cliVersion, platform);
@@ -161,7 +162,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* (
baseDir: serviceBaseDir,
logsDir: path.join(serviceBaseDir, "userdata", "logs"),
cliVersion,
- host: { execPath: "/usr/bin/t3" },
+ host: { execPath: hostExecPath },
});
}).pipe(
Effect.provideService(ProcessRunner.ProcessRunner, runner),
@@ -169,7 +170,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* (
Layer.mergeAll(
Layer.succeed(HostProcess.Platform, platform),
Layer.succeed(HostProcess.UserId, 501),
- Layer.succeed(HostProcess.ExecutablePath, "/usr/bin/t3"),
+ Layer.succeed(HostProcess.ExecutablePath, hostExecPath),
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make(() => Effect.die("no release download expected")),
@@ -817,6 +818,29 @@ it.layer(NodeServices.layer)("boot service install", (it) => {
}),
);
+ // regression: #17532
+ it.effect("puts the runtime an update installs on the launch agent PATH", () =>
+ Effect.gen(function* () {
+ const { makeService, fs, baseDir, runtime } = yield* makeHarness("darwin");
+ const path = yield* Path.Path;
+ const previous = runtime.versionDir;
+ const installed = pinnedRuntimePaths(path, baseDir, "1.2.4", "darwin").versionDir;
+ // The 1.2.3 CLI, or the 1.2.3 service's own PATH, installs 1.2.4.
+ const updated = yield* makeService(
+ `/usr/bin:${previous}:/bin`,
+ "1.2.4",
+ undefined,
+ false,
+ runtime.entryPath,
+ );
+ const plan = yield* updated.install();
+
+ expect(yield* fs.readFileString(plan.unitPath)).toContain(
+ ` PATH\n /usr/bin:${installed}:/bin:/opt/homebrew/bin:/usr/local/bin:/usr/sbin:/sbin`,
+ );
+ }),
+ );
+
it.effect("drops PATH directories that cannot be represented in a launch agent plist", () =>
Effect.gen(function* () {
const { service, fs } = yield* makeHarness(
diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts
index c1be1fc2c5ef4..eb28613a46aed 100644
--- a/apps/server/src/cloud/bootService.ts
+++ b/apps/server/src/cloud/bootService.ts
@@ -17,8 +17,10 @@ import { CLI_RELEASE_BASE_URL_ENV } from "@t3tools/shared/cliRelease";
import * as ProcessRunner from "../processRunner.ts";
import {
ensurePinnedRuntimeInstalled,
+ launcherOwnsVersionsDir,
pinnedRuntimeCommand,
pinnedRuntimePaths,
+ pinnedRuntimeVersionsDir,
PinnedRuntimeInstallError,
} from "./pinnedRuntime.ts";
import {
@@ -565,6 +567,8 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: {
const path = yield* Path.Path;
const runner = yield* ProcessRunner.ProcessRunner;
const host = input.host ?? { execPath: hostExecPath };
+ const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion, platform);
+ const versionsDir = pinnedRuntimeVersionsDir(path, input.baseDir);
const xmlSafeInstallerDirectories = installerPath.split(":").filter(
(directory) =>
directory.length > 0 &&
@@ -573,17 +577,23 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: {
return code >= 0x20 || code === 0x09 || code === 0x0a || code === 0x0d;
}),
);
+ // During an update, the running CLI and a PATH inherited from the service name the runtime
+ // being replaced. The service and its agent shells should find the runtime it starts.
const environmentPath = Array.from(
- new Set([
- ...xmlSafeInstallerDirectories,
- path.dirname(host.execPath),
- "/opt/homebrew/bin",
- "/usr/local/bin",
- "/usr/bin",
- "/bin",
- "/usr/sbin",
- "/sbin",
- ]),
+ new Set(
+ [
+ ...xmlSafeInstallerDirectories,
+ path.dirname(host.execPath),
+ "/opt/homebrew/bin",
+ "/usr/local/bin",
+ "/usr/bin",
+ "/bin",
+ "/usr/sbin",
+ "/sbin",
+ ].map((directory) =>
+ launcherOwnsVersionsDir(path, versionsDir, directory) ? runtimePaths.versionDir : directory,
+ ),
+ ),
).join(":");
const detectedManager = selectBootServiceManager({
@@ -597,7 +607,6 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: {
const logPath = path.join(input.logsDir, BOOT_SERVICE_LOG_FILE);
const statePath = path.join(input.baseDir, "runtime", SERVICE_STATE_FILE);
const restartPendingPath = path.join(input.baseDir, "runtime", SERVICE_RESTART_PENDING_FILE);
- const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion, platform);
const writeDurably = (filePath: string, contents: string) =>
Effect.scoped(
Effect.gen(function* () {