From 3fed2f2cd930fa382264b619039ce20b215fd1bf Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 19 Jul 2026 05:02:59 +0200 Subject: [PATCH] fix(migrate): validate source refs and expansions --- .../src/sources/conductor/inspect.test.ts | 35 +++++++++++++++++++ .../migrate/src/sources/conductor/inspect.ts | 13 ++++++- .../sources/conductor/project-config.test.ts | 7 ++++ .../src/sources/conductor/project-config.ts | 2 +- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/migrate/src/sources/conductor/inspect.test.ts b/packages/migrate/src/sources/conductor/inspect.test.ts index 2d3e7fc85..ea2c08ef1 100644 --- a/packages/migrate/src/sources/conductor/inspect.test.ts +++ b/packages/migrate/src/sources/conductor/inspect.test.ts @@ -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); diff --git a/packages/migrate/src/sources/conductor/inspect.ts b/packages/migrate/src/sources/conductor/inspect.ts index 94a95dda4..c085543b9 100644 --- a/packages/migrate/src/sources/conductor/inspect.ts +++ b/packages/migrate/src/sources/conductor/inspect.ts @@ -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 !== "..") { diff --git a/packages/migrate/src/sources/conductor/project-config.test.ts b/packages/migrate/src/sources/conductor/project-config.test.ts index d344a6c8f..83f7f1418 100644 --- a/packages/migrate/src/sources/conductor/project-config.test.ts +++ b/packages/migrate/src/sources/conductor/project-config.test.ts @@ -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", () => { diff --git a/packages/migrate/src/sources/conductor/project-config.ts b/packages/migrate/src/sources/conductor/project-config.ts index 1e8fa3645..945e36a51 100644 --- a/packages/migrate/src/sources/conductor/project-config.ts +++ b/packages/migrate/src/sources/conductor/project-config.ts @@ -613,7 +613,7 @@ function collectConductorVariables(command: string): string[] { const names = new Set(); 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];