From 6d6c4ddf15f12c90d3cbadbe3a4c50faf0d90d61 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 16 Aug 2026 19:35:18 +0000 Subject: [PATCH] fix(runtime): preserve merge resolutions after rebase Co-Authored-By: Paperclip --- .../0219_runtime_service_exposure.sql | 6 +-- pnpm-lock.yaml | 12 ----- ...kspace-runtime-https-live-exercise.test.ts | 16 ++++--- .../src/__tests__/workspace-runtime.test.ts | 20 +++++---- server/src/services/workspace-runtime.ts | 44 +++++++++++++++++++ 5 files changed, 67 insertions(+), 31 deletions(-) diff --git a/packages/db/src/migrations/0219_runtime_service_exposure.sql b/packages/db/src/migrations/0219_runtime_service_exposure.sql index 3a3b464f648..cc74e83ccd4 100644 --- a/packages/db/src/migrations/0219_runtime_service_exposure.sql +++ b/packages/db/src/migrations/0219_runtime_service_exposure.sql @@ -1,3 +1,3 @@ -ALTER TABLE "workspace_runtime_services" ADD COLUMN "exposure" jsonb; -ALTER TABLE "workspace_runtime_services" ADD COLUMN "exposure_handle" text; -ALTER TABLE "workspace_runtime_services" ADD COLUMN "backend_url" text; +ALTER TABLE "workspace_runtime_services" ADD COLUMN IF NOT EXISTS "exposure" jsonb; +ALTER TABLE "workspace_runtime_services" ADD COLUMN IF NOT EXISTS "exposure_handle" text; +ALTER TABLE "workspace_runtime_services" ADD COLUMN IF NOT EXISTS "backend_url" text; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0313d7eec7..a6441c9b960 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -714,18 +714,6 @@ importers: specifier: ^22.20.1 version: 22.20.1 - packages/tailscale-https-broker: - devDependencies: - '@types/node': - specifier: ^22.20.1 - version: 22.20.1 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - vitest: - specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(jsdom@28.1.0(@noble/hashes@2.2.0))(vite@7.3.1(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.1)) - packages/teams-catalog: devDependencies: '@types/node': diff --git a/server/src/__tests__/workspace-runtime-https-live-exercise.test.ts b/server/src/__tests__/workspace-runtime-https-live-exercise.test.ts index 010cf4da1cd..1ec69a4d8f0 100644 --- a/server/src/__tests__/workspace-runtime-https-live-exercise.test.ts +++ b/server/src/__tests__/workspace-runtime-https-live-exercise.test.ts @@ -96,13 +96,15 @@ if (optedIn && !live) { // An ephemeral legacy port rather than the real template's 45439, so this // never contends with the live workspace runtime on the same host. const reservePort = async () => { - const probe = net.createServer(); - await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve)); - const address = probe.address(); - const port = typeof address === "object" && address ? address.port : null; - await new Promise((resolve, reject) => probe.close((e) => (e ? reject(e) : resolve()))); - if (!port) throw new Error("failed to reserve a legacy port"); - return port; + for (let attempt = 0; attempt < 100; attempt += 1) { + const probe = net.createServer(); + await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve)); + const address = probe.address(); + const port = typeof address === "object" && address ? address.port : null; + await new Promise((resolve, reject) => probe.close((e) => (e ? reject(e) : resolve()))); + if (port && port <= 55_535 && (port < 42_000 || port > 42_999)) return port; + } + throw new Error("failed to reserve a legacy port outside the broker range"); }; const companyId = randomUUID(); diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index f26e0acf6c6..48acade375e 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -6580,15 +6580,17 @@ describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { process.env.PAPERCLIP_INSTANCE_ID = `runtime-https-backfill-${randomUUID()}`; const reservePort = async () => { - const probe = net.createServer(); - await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve)); - const address = probe.address(); - const port = typeof address === "object" && address ? address.port : null; - await new Promise((resolve, reject) => { - probe.close((error) => error ? reject(error) : resolve()); - }); - if (!port) throw new Error("Failed to reserve HTTPS backfill test port"); - return port; + for (let attempt = 0; attempt < 100; attempt += 1) { + const probe = net.createServer(); + await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve)); + const address = probe.address(); + const port = typeof address === "object" && address ? address.port : null; + await new Promise((resolve, reject) => { + probe.close((error) => error ? reject(error) : resolve()); + }); + if (port && port <= 55_535 && (port < 42_000 || port > 42_999)) return port; + } + throw new Error("Failed to reserve an HTTPS backfill test port outside the broker range"); }; const isLoopbackPortFree = async (port: number) => { const probe = net.createServer(); diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index 4ea66151eac..76570e3dd53 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -4801,6 +4801,48 @@ async function spawnLocalRuntimeService(input: StartLocalRuntimeServiceInput): P scopeType: input.scopeType, scopeId: input.scopeId, }); + let fixedPortRegistryMatch = false; + if (!exposureConfig && canAllocateFixedPort && identityPort) { + const identityTemplateData = buildTemplateData({ + workspace: input.workspace, + agent: input.agent, + issue: input.issue, + adapterEnv: input.adapterEnv, + port: identityPort, + }); + const identityExpose = parseObject(input.service.expose); + const identityReadiness = parseObject(input.service.readiness); + const identityUrlTemplate = + asString(identityExpose.urlTemplate, "") + || asString(identityReadiness.urlTemplate, ""); + const identityBackendUrl = identityUrlTemplate + ? renderTemplate(identityUrlTemplate, identityTemplateData) + : null; + const identityServiceKey = createLocalServiceKey({ + profileKind: "workspace-runtime", + serviceName, + cwd: identity.serviceCwd, + command, + envFingerprint: serviceIdentityFingerprint, + port: identityPort, + scope: { + scopeType: input.scopeType, + scopeId: input.scopeId, + executionWorkspaceId: input.executionWorkspaceId ?? null, + reuseKey: input.reuseKey, + }, + }); + fixedPortRegistryMatch = Boolean(await findAdoptableLocalService({ + serviceKey: identityServiceKey, + profileKind: "workspace-runtime", + serviceName, + command, + cwd: identity.serviceCwd, + envFingerprint: serviceIdentityFingerprint, + port: identityPort, + url: identityBackendUrl, + })); + } const runtimeId = input.runtimeServiceId ?? stoppedReuseCandidate?.id ?? randomUUID(); // An exposed runtime always takes its port from the dedicated broker range, so // a configured or previously used port is a *preference*, not a constraint. It @@ -4838,6 +4880,8 @@ async function spawnLocalRuntimeService(input: StartLocalRuntimeServiceInput): P remediation: "Retry the start or configure a different runtime service port.", }); } + } else if (!reservedExposure && canAllocateFixedPort && fixedPortRegistryMatch) { + port = explicitPort; } else if (!reservedExposure && canAllocateFixedPort) { port = await allocateIsolatedWorkspacePort({ db: input.db,