fix(migrate): validate source refs and expansions

This commit is contained in:
Mohamed Boudra
2026-07-19 05:02:59 +02:00
parent 234f0d9b75
commit 3fed2f2cd9
4 changed files with 55 additions and 2 deletions

View File

@@ -118,6 +118,41 @@ test("does not adopt an existing worktree on a different branch", () => {
});
});
test("does not recreate a missing workspace from a tag", () => {
const repo = createRepository("tag-only-workspace", null);
execFileSync("git", ["commit", "--allow-empty", "-m", "initial"], {
cwd: repo,
env: {
...process.env,
GIT_AUTHOR_NAME: "Paseo Test",
GIT_AUTHOR_EMAIL: "test@paseo.local",
GIT_COMMITTER_NAME: "Paseo Test",
GIT_COMMITTER_EMAIL: "test@paseo.local",
},
});
execFileSync("git", ["tag", "release"], { cwd: repo });
const inspected = inspectCatalog({
repos: [repoRecord("repo", repo)],
workspaces: [
{
id: "workspace-tag-only",
repoId: "repo",
branch: "release",
state: "ready",
path: null,
archiveCommit: null,
},
],
});
expect(inspected.projects[0]?.workspaces[0]).toMatchObject({
sourceId: "workspace-tag-only",
disposition: "missing-ref",
notices: [{ code: "missing-workspace-ref" }],
});
});
function createRepository(name: string, settings: string | null): string {
const repo = mkdtempSync(path.join(os.tmpdir(), `paseo-inspect-${name}-`));
cleanup.push(repo);

View File

@@ -134,7 +134,7 @@ function inspectWorkspace(
};
}
if (workspace.branch && refExists(repo.rootPath, workspace.branch)) {
if (workspace.branch && localBranchExists(repo.rootPath, workspace.branch)) {
return {
sourceId: workspace.id,
state: "ready",
@@ -209,6 +209,17 @@ function refExists(rootPath: string, ref: string): boolean {
}
}
function localBranchExists(rootPath: string, branch: string): boolean {
const branchName = branch.replace(/^refs\/heads\//, "");
if (!branchName) return false;
try {
git(rootPath, ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`]);
return true;
} catch {
return false;
}
}
function safeDirectoryName(workspace: ConductorWorkspaceRecord): string {
const candidate = workspace.path ? path.basename(workspace.path.replaceAll("\\", "/")) : "";
if (/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(candidate) && candidate !== "." && candidate !== "..") {

View File

@@ -161,6 +161,7 @@ test("preserves literal or escaped Conductor names and rewrites active variables
arithmeticDirect: { command: "serve --port $(( $CONDUCTOR_PORT ))" },
arithmeticBraced: { command: "serve --port $(( ${CONDUCTOR_PORT} ))" },
arithmeticOffset: { command: "serve --port $(( CONDUCTOR_PORT + 1 ))" },
prefixExpansion: { command: "serve --length ${#CONDUCTOR_PORT}" },
},
},
});
@@ -187,6 +188,12 @@ test("preserves literal or escaped Conductor names and rewrites active variables
message:
"scripts.arithmeticOffset.port_arithmetic: Conductor port arithmetic is not imported because Paseo reserves one service port.",
});
expect(inspected.notices).toContainEqual({
code: "conductor-setting-unsupported",
level: "warning",
message:
"scripts.prefixExpansion: Unsupported Conductor variables: CONDUCTOR_PORT. Command was not imported.",
});
});
test("skips cwd scripts on Windows instead of emitting POSIX shell syntax", () => {

View File

@@ -613,7 +613,7 @@ function collectConductorVariables(command: string): string[] {
const names = new Set<string>();
const mask = activeShellMask(command);
const direct =
/\$\{(CONDUCTOR_[A-Za-z0-9_]+)(?=[^A-Za-z0-9_])|\$(CONDUCTOR_[A-Za-z0-9_]+)(?![A-Za-z0-9_])/g;
/\$\{[#!]?(CONDUCTOR_[A-Za-z0-9_]+)(?=[^A-Za-z0-9_])|\$(CONDUCTOR_[A-Za-z0-9_]+)(?![A-Za-z0-9_])/g;
for (const match of command.matchAll(direct)) {
if (mask[match.index] !== "$") continue;
const name = match[1] ?? match[2];