fix(server): persist worktree runtime port when ambient PORT does not match (#1849) (#1930)

## What was done
Replaced the strict `!nonEmpty(process.env.PORT)` guard in
`maybePersistWorktreeRuntimePorts` with a new `isPortPinnedByRuntimeEnv`
helper function. This function checks if `process.env.PORT` is set, but
only suppresses persisting the port to configuration if the ambient
`PORT` matches the newly allocated `selectedPort`.

## Why it matters
Fixes issue #1849. Previously, if an ambient `PORT` environment variable
was exported globally (like inheriting from the shell running the parent
workspace), worktrees would silently fail to write their
collision-avoiding ports (e.g. 3103 instead of 3100) back to their
respective local `config.json` files. This resulted in orphaned
sub-worktrees and lost port tracking on reboot. With this fix, worktrees
correctly persist their assigned ports even while nested under an
inherited environment variables stack, while continuing to respect
manual, explicit pinning.

## How to verify
1. Export a port in the shell explicitly: `export PORT=3100`.
2. Launch a sub-worktree instance which receives an auto-assigned free
port (e.g., `3103`).
3. View the underlying `config.json` for that worktree inside
`.paperclip/worktrees/`.
4. The config file should correctly contain `{"server": {"port": 3103}}`
rather than dropping the write operation.

## Risks
None expected. The `Number()` and `Number.isInteger()` checks handle
parsing edge cases cleanly, defaulting robustly to preventing writes if
`process.env.PORT` is somehow malformed (e.g., set to a non-integer),
ensuring absolute safety during misconfigurations.

Co-authored-by: manavshrivastavagit <manavshrivastava@users.noreply.github.com>
This commit is contained in:
Manav Shrivastava 2026-08-10 03:59:44 +05:30 committed by GitHub
parent 19be4cf927
commit ebf2b8ff79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 10 additions and 1 deletions

View File

@ -650,6 +650,14 @@ export function maybeRepairLegacyWorktreeConfigAndEnvFiles(): {
return { repairedConfig, repairedEnv };
}
function isPortPinnedByRuntimeEnv(rawValue: string | null | undefined, selectedPort: number): boolean {
const normalized = nonEmpty(rawValue);
if (!normalized) return false;
const parsedPort = Number(normalized);
if (!Number.isInteger(parsedPort) || parsedPort <= 0) return true;
return parsedPort === selectedPort;
}
export function maybePersistWorktreeRuntimePorts(input: {
serverPort: number;
databasePort?: number | null;
@ -667,7 +675,7 @@ export function maybePersistWorktreeRuntimePorts(input: {
const { config, changed } = applyRuntimePortSelectionToConfig(fileConfig, {
serverPort: input.serverPort,
databasePort: input.databasePort,
allowServerPortWrite: !nonEmpty(process.env.PORT),
allowServerPortWrite: !isPortPinnedByRuntimeEnv(process.env.PORT, input.serverPort),
allowDatabasePortWrite: !nonEmpty(process.env.DATABASE_URL),
});
@ -675,3 +683,4 @@ export function maybePersistWorktreeRuntimePorts(input: {
writeConfigFile(context.configPath, config);
}
}