mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
fix(service): make managed installs start reliably
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
8925d3f38a
commit
c9559a5d90
10 files changed
+88
-45
No files matched your search
@@ -93,7 +93,15 @@ describe("managed install commands", () => {
|
||||
if (file === "curl" && !args.includes("--output")) return { stdout: JSON.stringify({ sha }), stderr: "" };
|
||||
if (file === "curl") { fs.writeFileSync(args[args.indexOf("--output") + 1], "archive"); return { stdout: "", stderr: "" }; }
|
||||
if (file === "tar") { const checkout = args[args.indexOf("-C") + 1]; fs.mkdirSync(path.join(checkout, "cli"), { recursive: true }); fs.writeFileSync(path.join(checkout, "cli", "package.json"), JSON.stringify({ version: "0.3.1" })); return { stdout: "", stderr: "" }; }
|
||||
if (file === "corepack" || file === "bash") return { stdout: "", stderr: "" };
|
||||
if (file === "corepack") {
|
||||
if (args.includes("pack")) {
|
||||
const destination = args[args.indexOf("--pack-destination") + 1];
|
||||
const packageName = args.includes("packages/db") ? "paperclipai-db" : "paperclipai-server";
|
||||
fs.writeFileSync(path.join(destination, `${packageName}-0.3.1.tgz`), "package");
|
||||
}
|
||||
return { stdout: "", stderr: "" };
|
||||
}
|
||||
if (file === "bash") return { stdout: "", stderr: "" };
|
||||
if (file === "npm" && args[0] === "pack") { fs.writeFileSync(path.join(args[args.indexOf("--pack-destination") + 1], "paperclipai-0.3.1.tgz"), "package"); return { stdout: "", stderr: "" }; }
|
||||
if (file === "npm" && args[0] === "install") { const prefix = args[args.indexOf("--prefix") + 1]; const packageRoot = path.join(prefix, "node_modules", "paperclipai"); fs.mkdirSync(path.join(packageRoot, "dist"), { recursive: true }); fs.writeFileSync(path.join(packageRoot, "package.json"), JSON.stringify({ version: "0.3.1" })); fs.writeFileSync(path.join(packageRoot, "dist", "index.js"), "#!/usr/bin/env node\n"); return { stdout: "", stderr: "" }; }
|
||||
if (file === process.execPath) return { stdout: "0.3.1\n", stderr: "" };
|
||||
@@ -105,7 +113,8 @@ describe("managed install commands", () => {
|
||||
expect(manifest).toMatchObject({ source: "git", repo: "HenkDz/paperclip", ref: "master", sha });
|
||||
expect(manifest?.payloadPath).toContain(path.join("git", sha.slice(0, 12)));
|
||||
expect(runCommand.mock.calls.filter(([command, args]) => command === "curl" && args.includes("--output"))).toHaveLength(1);
|
||||
expect(runCommand.mock.calls.filter(([command]) => command === "corepack")).toHaveLength(1);
|
||||
expect(runCommand.mock.calls.filter(([command, args]) => command === "corepack" && args[1] === "install")).toHaveLength(1);
|
||||
expect(runCommand.mock.calls.filter(([command, args]) => command === "corepack" && args.includes("pack"))).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("installs through the shim, reports provenance, and uninstalls without deleting user data", async () => {
|
||||
|
||||
@@ -12,15 +12,19 @@ const config = {
|
||||
} as PaperclipConfig;
|
||||
|
||||
let previousPaperclipHome: string | undefined;
|
||||
let previousServiceManaged: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
previousPaperclipHome = process.env.PAPERCLIP_HOME;
|
||||
previousServiceManaged = process.env.PAPERCLIP_SERVICE_MANAGED;
|
||||
process.env.PAPERCLIP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-service-restart-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (previousPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME;
|
||||
else process.env.PAPERCLIP_HOME = previousPaperclipHome;
|
||||
if (previousServiceManaged === undefined) delete process.env.PAPERCLIP_SERVICE_MANAGED;
|
||||
else process.env.PAPERCLIP_SERVICE_MANAGED = previousServiceManaged;
|
||||
});
|
||||
|
||||
function managerFixture(active = true) {
|
||||
@@ -52,6 +56,15 @@ function managerFixture(active = true) {
|
||||
}
|
||||
|
||||
describe("service health doctor checks", () => {
|
||||
it("skips live service checks during the managed unit's own activation", async () => {
|
||||
process.env.PAPERCLIP_SERVICE_MANAGED = "1";
|
||||
const detect = vi.fn();
|
||||
const probe = vi.fn();
|
||||
await expect(serviceHealthChecks(config, { detect, probe })).resolves.toEqual([]);
|
||||
expect(detect).not.toHaveBeenCalled();
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips exact version matching unless a restart version is explicit", () => {
|
||||
expect(resolveRestartExpectedVersion(null)).toBeNull();
|
||||
expect(resolveRestartExpectedVersion(undefined)).toBeNull();
|
||||
|
||||
@@ -40,6 +40,8 @@ export async function serviceHealthChecks(
|
||||
config: PaperclipConfig,
|
||||
dependencies: Partial<ServiceCheckDependencies> = {},
|
||||
): Promise<CheckResult[]> {
|
||||
if (process.env.PAPERCLIP_SERVICE_MANAGED === "1") return [];
|
||||
|
||||
const deps: ServiceCheckDependencies = {
|
||||
detect: (instanceId) => detectServiceManager({ instanceId }),
|
||||
probe: probeHealth,
|
||||
|
||||
@@ -185,11 +185,18 @@ export async function installGitPayload(repo: string, sha: string, runCommand: C
|
||||
await runCommand("tar", ["-xzf", archivePath, "--strip-components=1", "-C", checkoutPath], { maxBuffer: 4 * 1024 * 1024 });
|
||||
await runCommand("corepack", ["pnpm", "install", "--frozen-lockfile"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
|
||||
await runCommand("bash", ["scripts/build-npm.sh", "--skip-checks"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
|
||||
await runCommand("corepack", ["pnpm", "--filter", "@paperclipai/db", "build"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
|
||||
await runCommand("corepack", ["pnpm", "--filter", "@paperclipai/server", "build"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
|
||||
const metadata = JSON.parse(fs.readFileSync(path.join(checkoutPath, "cli", "package.json"), "utf8")) as { version: string };
|
||||
await runCommand("corepack", ["pnpm", "--dir", "packages/db", "pack", "--pack-destination", stagingRoot], { cwd: checkoutPath, maxBuffer: 16 * 1024 * 1024 });
|
||||
await runCommand("corepack", ["pnpm", "--dir", "server", "pack", "--pack-destination", stagingRoot], { cwd: checkoutPath, env: { ...process.env, PAPERCLIP_RELEASE_REUSE_UI_DIST: "1" }, maxBuffer: 32 * 1024 * 1024 });
|
||||
await runCommand("npm", ["pack", "--pack-destination", stagingRoot], { cwd: path.join(checkoutPath, "cli"), maxBuffer: 16 * 1024 * 1024 });
|
||||
const tarball = fs.readdirSync(stagingRoot).find((entry) => entry.endsWith(".tgz"));
|
||||
if (!tarball) throw new Error("npm pack did not produce a paperclipai tarball.");
|
||||
await runCommand("npm", ["install", "--prefix", stagedPayload, path.join(stagingRoot, tarball), "--no-audit", "--no-fund"], { cwd: stagingRoot, maxBuffer: 32 * 1024 * 1024 });
|
||||
const tarballs = fs.readdirSync(stagingRoot).filter((entry) => entry.endsWith(".tgz"));
|
||||
const cliTarball = tarballs.find((entry) => /^paperclipai-(?!db-|server-)/.test(entry));
|
||||
const dbTarball = tarballs.find((entry) => entry.startsWith("paperclipai-db-"));
|
||||
const serverTarball = tarballs.find((entry) => entry.startsWith("paperclipai-server-"));
|
||||
if (!cliTarball || !dbTarball || !serverTarball) throw new Error("Git install packaging did not produce paperclipai, @paperclipai/db, and @paperclipai/server tarballs.");
|
||||
await runCommand("npm", ["install", "--prefix", stagedPayload, path.join(stagingRoot, cliTarball), path.join(stagingRoot, dbTarball), path.join(stagingRoot, serverTarball), "--no-audit", "--no-fund"], { cwd: stagingRoot, maxBuffer: 32 * 1024 * 1024 });
|
||||
await smokePayload(stagedPayload, metadata.version, runCommand);
|
||||
fs.renameSync(stagedPayload, payloadPath);
|
||||
return { payloadPath, reused: false, version: metadata.version };
|
||||
|
||||
@@ -79,7 +79,6 @@
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"embedded-postgres@18.1.0-beta.16": "patches/embedded-postgres@18.1.0-beta.16.patch",
|
||||
"acpx@0.12.0": "patches/acpx@0.12.0.patch"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { ensureLinuxSharedLibraryAliases } from "./embedded-postgres-native.js";
|
||||
import { ensureLinuxSharedLibraryAliases, mergeEmbeddedPostgresSpawnEnv } from "./embedded-postgres-native.js";
|
||||
|
||||
describe("embedded Postgres native runtime", () => {
|
||||
const tempDirs: string[] = [];
|
||||
@@ -41,3 +41,23 @@ describe("embedded Postgres native runtime", () => {
|
||||
expect(fs.readlinkSync(path.join(tempDir, "libicuuc.so.60"))).toBe("libicuuc.so.60.2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedded Postgres spawn environment", () => {
|
||||
it("preserves the process environment for initdb and forces a portable locale", () => {
|
||||
const previousPath = process.env.PATH;
|
||||
process.env.PATH = "/paperclip/bin";
|
||||
try {
|
||||
expect(mergeEmbeddedPostgresSpawnEnv("/native/bin/initdb", { env: { LC_MESSAGES: "en_US.UTF-8" } })).toEqual({
|
||||
env: expect.objectContaining({ PATH: "/paperclip/bin", LC_MESSAGES: "C" }),
|
||||
});
|
||||
} finally {
|
||||
if (previousPath === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = previousPath;
|
||||
}
|
||||
});
|
||||
|
||||
it("does not alter unrelated child processes", () => {
|
||||
const options = { env: { ONLY: "value" } };
|
||||
expect(mergeEmbeddedPostgresSpawnEnv("node", options)).toBe(options);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,32 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import childProcess from "node:child_process";
|
||||
import { createRequire, syncBuiltinESMExports } from "node:module";
|
||||
import path from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let embeddedPostgresSpawnPatchInstalled = false;
|
||||
|
||||
export function mergeEmbeddedPostgresSpawnEnv(
|
||||
command: string,
|
||||
options: { env?: NodeJS.ProcessEnv } | undefined,
|
||||
): { env?: NodeJS.ProcessEnv } | undefined {
|
||||
const executable = path.basename(command).replace(/\.exe$/i, "");
|
||||
if (executable !== "initdb" && executable !== "postgres") return options;
|
||||
return { ...options, env: { ...process.env, ...options?.env, LC_MESSAGES: "C" } };
|
||||
}
|
||||
|
||||
function installEmbeddedPostgresSpawnPatch(): void {
|
||||
if (embeddedPostgresSpawnPatchInstalled) return;
|
||||
const originalSpawn = childProcess.spawn;
|
||||
childProcess.spawn = ((command: string, args?: readonly string[], options?: object) =>
|
||||
originalSpawn(
|
||||
command,
|
||||
args,
|
||||
mergeEmbeddedPostgresSpawnEnv(command, options) as never,
|
||||
)) as typeof childProcess.spawn;
|
||||
syncBuiltinESMExports();
|
||||
embeddedPostgresSpawnPatchInstalled = true;
|
||||
}
|
||||
|
||||
function resolveNativePackageName(): string | null {
|
||||
if (process.platform !== "linux") return null;
|
||||
@@ -72,6 +96,7 @@ export async function ensureLinuxSharedLibraryAliases(libDir: string): Promise<s
|
||||
}
|
||||
|
||||
export async function prepareEmbeddedPostgresNativeRuntime(): Promise<void> {
|
||||
installEmbeddedPostgresSpawnPatch();
|
||||
const nativePackageName = resolveNativePackageName();
|
||||
const packageRoot = resolveEmbeddedPostgresPackageRoot();
|
||||
if (!nativePackageName || !packageRoot) return;
|
||||
|
||||
@@ -33,6 +33,7 @@ export {
|
||||
} from "./embedded-postgres-error.js";
|
||||
export {
|
||||
ensureLinuxSharedLibraryAliases,
|
||||
mergeEmbeddedPostgresSpawnEnv,
|
||||
prepareEmbeddedPostgresNativeRuntime,
|
||||
} from "./embedded-postgres-native.js";
|
||||
export { issueRelations } from "./schema/issue_relations.js";
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -23,7 +23,7 @@
|
||||
* for a particular string, we need to force that string into the right locale.
|
||||
* @see https://github.com/leinelissen/embedded-postgres/issues/15
|
||||
*/
|
||||
-const LC_MESSAGES_LOCALE = 'en_US.UTF-8';
|
||||
+const LC_MESSAGES_LOCALE = 'C';
|
||||
// The default configuration options for the class
|
||||
const defaults = {
|
||||
databaseDir: path.join(process.cwd(), 'data', 'db'),
|
||||
@@ -133,7 +133,7 @@
|
||||
`--pwfile=${passwordFile}`,
|
||||
`--lc-messages=${LC_MESSAGES_LOCALE}`,
|
||||
...this.options.initdbFlags,
|
||||
- ], Object.assign(Object.assign({}, permissionIds), { env: { LC_MESSAGES: LC_MESSAGES_LOCALE } }));
|
||||
+ ], Object.assign(Object.assign({}, permissionIds), { env: Object.assign(Object.assign({}, globalThis.process.env), { LC_MESSAGES: LC_MESSAGES_LOCALE }) }));
|
||||
// Connect to stderr, as that is where the messages get sent
|
||||
(_a = process.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
|
||||
// Parse the data as a string and log it
|
||||
@@ -177,7 +177,7 @@
|
||||
'-p',
|
||||
this.options.port.toString(),
|
||||
...this.options.postgresFlags,
|
||||
- ], Object.assign(Object.assign({}, permissionIds), { env: { LC_MESSAGES: LC_MESSAGES_LOCALE } }));
|
||||
+ ], Object.assign(Object.assign({}, permissionIds), { env: Object.assign(Object.assign({}, globalThis.process.env), { LC_MESSAGES: LC_MESSAGES_LOCALE }) }));
|
||||
// Connect to stderr, as that is where the messages get sent
|
||||
(_a = this.process.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
|
||||
// Parse the data as a string and log it
|
||||
Generated
+4
-7
@@ -23,9 +23,6 @@ patchedDependencies:
|
||||
acpx@0.12.0:
|
||||
hash: tb5cdbd7kiiblhylbkroxfdcha
|
||||
path: patches/acpx@0.12.0.patch
|
||||
embedded-postgres@18.1.0-beta.16:
|
||||
hash: 55uhvnotpqyiy37rn3pqpukhei
|
||||
path: patches/embedded-postgres@18.1.0-beta.16.patch
|
||||
|
||||
importers:
|
||||
|
||||
@@ -105,7 +102,7 @@ importers:
|
||||
version: 0.45.2(@electric-sql/pglite@0.3.15)(kysely@0.29.3)(pg@8.18.0)(postgres@3.4.9)(sqlite3@5.1.7)
|
||||
embedded-postgres:
|
||||
specifier: ^18.1.0-beta.16
|
||||
version: 18.1.0-beta.16(patch_hash=55uhvnotpqyiy37rn3pqpukhei)
|
||||
version: 18.1.0-beta.16
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -340,7 +337,7 @@ importers:
|
||||
version: 0.45.2(@electric-sql/pglite@0.3.15)(kysely@0.29.3)(pg@8.18.0)(postgres@3.4.9)(sqlite3@5.1.7)
|
||||
embedded-postgres:
|
||||
specifier: ^18.1.0-beta.16
|
||||
version: 18.1.0-beta.16(patch_hash=55uhvnotpqyiy37rn3pqpukhei)
|
||||
version: 18.1.0-beta.16
|
||||
postgres:
|
||||
specifier: ^3.4.9
|
||||
version: 3.4.9
|
||||
@@ -796,7 +793,7 @@ importers:
|
||||
version: 0.45.2(@electric-sql/pglite@0.3.15)(kysely@0.29.3)(pg@8.18.0)(postgres@3.4.9)(sqlite3@5.1.7)
|
||||
embedded-postgres:
|
||||
specifier: ^18.1.0-beta.16
|
||||
version: 18.1.0-beta.16(patch_hash=55uhvnotpqyiy37rn3pqpukhei)
|
||||
version: 18.1.0-beta.16
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.2.1
|
||||
@@ -13888,7 +13885,7 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.389: {}
|
||||
|
||||
embedded-postgres@18.1.0-beta.16(patch_hash=55uhvnotpqyiy37rn3pqpukhei):
|
||||
embedded-postgres@18.1.0-beta.16:
|
||||
dependencies:
|
||||
async-exit-hook: 2.0.1
|
||||
pg: 8.18.0
|
||||
|
||||
Reference in new issue
Block a user