authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 17:39:11-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 17:39:11-07:00
log2d5464afbc19058be8c125d5ee70481d721d14f4
tree91b304ce65f75835d7a128d0eefd848a68f70ef2
parentb6f72681da394369274121c4d1216f5d64a7a4bb
signature Signed by PGP key B5690EEEBB952194

fix(server): preserve native provider executable paths during updates (#9850)


2 files changed, 53 insertions(+), 6 deletions(-)

apps/server/src/provider/providerMaintenance.test.ts+50-4
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1// @effect-diagnostics nodeBuiltinImport:off1// @effect-diagnostics nodeBuiltinImport:off
2import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
3import * as NodeFS from "node:fs";3import * as NodeFS from "node:fs";
4import * as NodeChildProcess from "node:child_process";
4import * as NodeServices from "@effect/platform-node/NodeServices";5import * as NodeServices from "@effect/platform-node/NodeServices";
5import * as NodeOS from "node:os";6import * as NodeOS from "node:os";
6import * as NodePath from "node:path";7import * as NodePath from "node:path";
...@@ -8,6 +9,7 @@ import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3...@@ -8,6 +9,7 @@ import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3
8import { HostProcessPlatform } from "@t3tools/shared/hostProcess";9import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
9import * as Crypto from "effect/Crypto";10import * as Crypto from "effect/Crypto";
10import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
11import { HttpClient } from "effect/unstable/http";13import { HttpClient } from "effect/unstable/http";
12import {14import {
13 createProviderVersionAdvisory,15 createProviderVersionAdvisory,
...@@ -362,9 +364,9 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {...@@ -362,9 +364,9 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
362 provider: driver("nativePackageTool"),364 provider: driver("nativePackageTool"),
363 packageName: "@example/native-package-tool",365 packageName: "@example/native-package-tool",
364 update: {366 update: {
365 command: "native-package-tool update",367 command: `${nativePackageToolPath} update`,
366368
367 executable: "native-package-tool",369 executable: nativePackageToolPath,
368370
369 args: ["update"],371 args: ["update"],
370372
...@@ -399,9 +401,9 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {...@@ -399,9 +401,9 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
399 provider: driver("scopedPackageTool"),401 provider: driver("scopedPackageTool"),
400 packageName: "@example/scoped-package-tool",402 packageName: "@example/scoped-package-tool",
401 update: {403 update: {
402 command: "scoped-package-tool upgrade",404 command: `${scopedPackageToolPath} upgrade`,
403405
404 executable: "scoped-package-tool",406 executable: scopedPackageToolPath,
405407
406 args: ["upgrade"],408 args: ["upgrade"],
407409
...@@ -411,6 +413,50 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {...@@ -411,6 +413,50 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
411 }),413 }),
412 );414 );
413415
416 it.effect.skipIf(windowsHost)("runs an explicit native updater outside PATH", () =>
417 Effect.gen(function* () {
418 const fs = yield* FileSystem.FileSystem;
419 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-native-update-" });
420 const nativeBinDir = NodePath.join(tempDir, "with spaces", ".scoped-package-tool", "bin");
421 yield* fs.makeDirectory(nativeBinDir, { recursive: true });
422 const binaryPath = NodePath.join(nativeBinDir, "scoped-package-tool");
423 yield* fs.writeFileString(binaryPath, "#!/bin/sh\nprintf '%s' \"$1\"\n");
424 yield* fs.chmod(binaryPath, 0o755);
425
426 const capabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(
427 scopedPackageToolUpdate,
428 { binaryPath, env: { PATH: "" } },
429 );
430 const update = capabilities.update;
431 expect(update).not.toBeNull();
432 if (!update) return;
433 const result = NodeChildProcess.spawnSync(update.executable, update.args, {
434 env: { PATH: "" },
435 encoding: "utf8",
436 });
437 expect(result.error).toBeUndefined();
438 expect(result.status).toBe(0);
439 expect(result.stdout).toBe("upgrade");
440 }).pipe(Effect.scoped),
441 );
442
443 it.each([
444 "/Users/example/.local/bin/native-package-tool",
445 "C:\\Users\\Example User\\.local\\bin\\native-package-tool.exe",
446 ])("preserves a configured native executable path: %s", (binaryPath) => {
447 expect(nativePackageToolUpdate.resolve({ binaryPath }).update?.executable).toBe(binaryPath);
448 });
449
450 it("uses the resolved launcher when its symlink target identifies a native install", () => {
451 const launcher = "/custom tools/native-launcher";
452 const capabilities = nativePackageToolUpdate.resolve({
453 binaryPath: launcher,
454 resolvedCommandPath: launcher,
455 realCommandPath: "/Users/example/.local/bin/native-package-tool",
456 });
457 expect(capabilities.update?.executable).toBe(launcher);
458 });
459
414 it("switches native-package-tool to Homebrew updates when the binary resolves through Homebrew", () => {460 it("switches native-package-tool to Homebrew updates when the binary resolves through Homebrew", () => {
415 expect(461 expect(
416 nativePackageToolUpdate.resolve({462 nativePackageToolUpdate.resolve({
apps/server/src/provider/providerMaintenance.ts+3-2
...@@ -209,6 +209,7 @@ function makeHomebrewProviderMaintenanceCapabilities(...@@ -209,6 +209,7 @@ function makeHomebrewProviderMaintenanceCapabilities(
209209
210function makeNativeProviderMaintenanceCapabilities(210function makeNativeProviderMaintenanceCapabilities(
211 definition: PackageManagedProviderMaintenanceDefinition,211 definition: PackageManagedProviderMaintenanceDefinition,
212 commandPath: string,
212): ProviderMaintenanceCapabilities | null {213): ProviderMaintenanceCapabilities | null {
213 if (!definition.nativeUpdate) {214 if (!definition.nativeUpdate) {
214 return null;215 return null;
...@@ -217,7 +218,7 @@ function makeNativeProviderMaintenanceCapabilities(...@@ -217,7 +218,7 @@ function makeNativeProviderMaintenanceCapabilities(
217 return makeProviderMaintenanceCapabilities({218 return makeProviderMaintenanceCapabilities({
218 provider: definition.provider,219 provider: definition.provider,
219 packageName: definition.npmPackageName,220 packageName: definition.npmPackageName,
220 updateExecutable: definition.nativeUpdate.executable,221 updateExecutable: commandPath,
221 updateArgs: definition.nativeUpdate.args,222 updateArgs: definition.nativeUpdate.args,
222 updateLockKey: definition.nativeUpdate.lockKey,223 updateLockKey: definition.nativeUpdate.lockKey,
223 });224 });
...@@ -297,7 +298,7 @@ export function resolvePackageManagedProviderMaintenance(...@@ -297,7 +298,7 @@ export function resolvePackageManagedProviderMaintenance(
297 commandPaths.some((commandPath) => nativeUpdate.isCommandPath(commandPath))298 commandPaths.some((commandPath) => nativeUpdate.isCommandPath(commandPath))
298 ) {299 ) {
299 return (300 return (
300 makeNativeProviderMaintenanceCapabilities(definition) ??301 makeNativeProviderMaintenanceCapabilities(definition, resolvedCommandPath) ??
301 makeNpmGlobalProviderMaintenanceCapabilities(definition)302 makeNpmGlobalProviderMaintenanceCapabilities(definition)
302 );303 );
303 }304 }