fix(server): isolate paseo.json parse failures so they don't block fetch_workspaces
A malformed paseo.json (e.g. git-conflict markers) in any registered
workspace caused readPaseoConfig to throw, which propagated through
getScriptConfigs -> buildWorkspaceScriptPayloads ->
Session.describeWorkspaceRecord and failed the entire
fetch_workspaces_request with code=fetch_workspaces_failed. The client
showed no workspaces at all.
Reshape: separate loading (mechanism) from policy (per-caller).
- readPaseoConfig now returns a discriminated result
({ ok: true; config } | { ok: false; configPath; error }). Pure load,
no logging, no opinions.
- Workspace projection owns its own policy via readPaseoConfigForProjection
in script-status-projection.ts: warn via pino with configPath +
workspaceDirectory + err, treat workspace as having no scripts, keep the
list alive.
- spawnWorkspaceScript throws with cause so the existing
handleStartWorkspaceScriptRequest catch logs it once with full context.
- The setup/teardown/terminal wrappers retain their pre-existing silent
degradation on missing/broken config.
- getScriptConfigs takes a parsed PaseoConfig | null instead of repoRoot
so the projection can pass the result through without double-reading.
Closes https://github.com/getpaseo/paseo/issues/598
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
isServiceScript,
|
||||
isPaseoOwnedWorktreeCwd,
|
||||
listPaseoWorktrees,
|
||||
readPaseoConfig,
|
||||
resolveWorktreeRuntimeEnv,
|
||||
type WorktreeSetupCommandProgressEvent,
|
||||
runWorktreeSetupCommands,
|
||||
@@ -18,6 +19,12 @@ import {
|
||||
type CreateWorktreeOptions,
|
||||
type WorktreeConfig,
|
||||
} from "./worktree";
|
||||
import type { PaseoConfig } from "./paseo-config-schema.js";
|
||||
|
||||
function loadConfigForTest(repoRoot: string): PaseoConfig | null {
|
||||
const result = readPaseoConfig(repoRoot);
|
||||
return result.ok ? result.config : null;
|
||||
}
|
||||
import { getPaseoWorktreeMetadataPath } from "./worktree-metadata.js";
|
||||
import { execSync } from "child_process";
|
||||
import {
|
||||
@@ -707,7 +714,7 @@ describe.skipIf(process.platform === "win32")("createWorktree", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const scriptConfigs = getScriptConfigs(repoDir);
|
||||
const scriptConfigs = getScriptConfigs(loadConfigForTest(repoDir));
|
||||
const typecheck = scriptConfigs.get("typecheck");
|
||||
|
||||
expect(typecheck).toEqual({
|
||||
@@ -731,7 +738,7 @@ describe.skipIf(process.platform === "win32")("createWorktree", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const scriptConfigs = getScriptConfigs(repoDir);
|
||||
const scriptConfigs = getScriptConfigs(loadConfigForTest(repoDir));
|
||||
const server = scriptConfigs.get("server");
|
||||
|
||||
expect(server).toEqual({
|
||||
@@ -771,7 +778,7 @@ describe.skipIf(process.platform === "win32")("createWorktree", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getScriptConfigs(repoDir)).toEqual(
|
||||
expect(getScriptConfigs(loadConfigForTest(repoDir))).toEqual(
|
||||
new Map([
|
||||
["valid", { command: "npm run valid" }],
|
||||
["invalidType", { command: "npm run worker" }],
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createHash } from "node:crypto";
|
||||
import * as pty from "node-pty";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { buildStringCommandShellInvocation } from "./string-command-shell.js";
|
||||
import { readPaseoConfigJson } from "./paseo-config-file.js";
|
||||
import { readPaseoConfigJson, resolvePaseoConfigPath } from "./paseo-config-file.js";
|
||||
export {
|
||||
PaseoConfigRawSchema,
|
||||
PaseoLifecycleCommandRawSchema,
|
||||
@@ -197,31 +197,35 @@ export class UnknownBranchError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export function readPaseoConfig(repoRoot: string): PaseoConfig | null {
|
||||
export type ReadPaseoConfigResult =
|
||||
| { ok: true; config: PaseoConfig | null }
|
||||
| { ok: false; configPath: string; error: unknown };
|
||||
|
||||
export function readPaseoConfig(repoRoot: string): ReadPaseoConfigResult {
|
||||
try {
|
||||
const json = readPaseoConfigJson(repoRoot);
|
||||
if (json === null) {
|
||||
return null;
|
||||
return { ok: true, config: null };
|
||||
}
|
||||
return PaseoConfigSchema.parse(json);
|
||||
} catch {
|
||||
throw new Error(`Failed to parse paseo.json`);
|
||||
return { ok: true, config: PaseoConfigSchema.parse(json) };
|
||||
} catch (error) {
|
||||
return { ok: false, configPath: resolvePaseoConfigPath(repoRoot), error };
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorktreeSetupCommands(repoRoot: string): string[] {
|
||||
const config = readPaseoConfig(repoRoot);
|
||||
return config?.worktree?.setup ?? [];
|
||||
const result = readPaseoConfig(repoRoot);
|
||||
return result.ok ? (result.config?.worktree?.setup ?? []) : [];
|
||||
}
|
||||
|
||||
export function getWorktreeTeardownCommands(repoRoot: string): string[] {
|
||||
const config = readPaseoConfig(repoRoot);
|
||||
return config?.worktree?.teardown ?? [];
|
||||
const result = readPaseoConfig(repoRoot);
|
||||
return result.ok ? (result.config?.worktree?.teardown ?? []) : [];
|
||||
}
|
||||
|
||||
export function getWorktreeTerminalSpecs(repoRoot: string): WorktreeTerminalConfig[] {
|
||||
const config = readPaseoConfig(repoRoot);
|
||||
const terminals = config?.worktree?.terminals;
|
||||
const result = readPaseoConfig(repoRoot);
|
||||
const terminals = result.ok ? result.config?.worktree?.terminals : undefined;
|
||||
if (!Array.isArray(terminals) || terminals.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -254,8 +258,7 @@ export function getWorktreeTerminalSpecs(repoRoot: string): WorktreeTerminalConf
|
||||
return specs;
|
||||
}
|
||||
|
||||
export function getScriptConfigs(repoRoot: string): Map<string, ScriptConfig> {
|
||||
const config = readPaseoConfig(repoRoot);
|
||||
export function getScriptConfigs(config: PaseoConfig | null): Map<string, ScriptConfig> {
|
||||
const scripts = config?.scripts;
|
||||
if (!scripts || typeof scripts !== "object") {
|
||||
return new Map();
|
||||
|
||||
Reference in New Issue
Block a user