Skip to content

Fix production deployments showing preview URLs#45

Merged
pablopunk merged 1 commit intomainfrom
fix/production-url-preview
Apr 23, 2026
Merged

Fix production deployments showing preview URLs#45
pablopunk merged 1 commit intomainfrom
fix/production-url-preview

Conversation

@pablopunk
Copy link
Copy Markdown
Owner

@pablopunk pablopunk commented Apr 18, 2026

Summary

  • centralize canonical production URL generation
  • persist the correct production URL after deploys and rollbacks
  • automatically repair stale stored production URLs in live state
  • clear stored production URL when production is stopped

Testing

  • not run locally in this checkout (node_modules missing, so pnpm format / typecheck could not be executed)

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved production URL reliability with automatic repair and validation when production is running.
    • Fixed missing URL information when production status is updated; URLs are now consistently tracked and cleared when production stops.
    • Enhanced URL resolution with fallback mechanism for offline environments, ensuring production connectivity remains stable during state transitions.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
doce-dev-www Ready Ready Preview, Comment Apr 18, 2026 1:31pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

Walkthrough

The changes introduce a production URL management system that dynamically computes and validates production URLs across multiple components. New functions getCanonicalProductionUrl and repairStaleProductionUrl centralize URL resolution logic with Tailscale integration and localhost fallback. Production state updates in rollback, queue handlers, and the live manager now explicitly compute and populate the production URL field, while the production stop handler explicitly clears it. The repair function validates stored URLs against canonical URLs during state building, updating persisted values when mismatches are detected.

Sequence Diagram(s)

sequenceDiagram
    participant Action as Rollback/Queue Handler
    participant URLMgr as URL Manager<br/>(productionUrl.ts)
    participant DB as Database<br/>(updateProductionStatus)
    participant StateMgr as State Manager<br/>(live/manager.ts)
    
    Action->>URLMgr: getCanonicalProductionUrl(project)
    URLMgr->>URLMgr: Resolve Tailscale URL or<br/>fallback to localhost
    URLMgr-->>Action: Return canonical URL
    
    Action->>DB: updateProductionStatus<br/>(id, status, {productionUrl})
    DB-->>Action: Updated
    
    Note over StateMgr: During buildState()
    StateMgr->>URLMgr: repairStaleProductionUrl(project)
    URLMgr->>DB: getProductionStatus(project)
    DB-->>URLMgr: Current status & URL
    
    alt Status is "running"
        URLMgr->>URLMgr: Compute canonical URL
        alt URLs mismatch
            URLMgr->>DB: updateProductionStatus<br/>(id, status, {productionUrl: canonical})
            DB-->>URLMgr: Updated
        end
    end
    
    URLMgr-->>StateMgr: Validated/repaired URL
    StateMgr->>StateMgr: Populate ProductionLiveState<br/>with correct URL
    StateMgr-->>StateMgr: Broadcast state changes
Loading
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Fix production deployments showing preview URLs" accurately captures the main objective of the PR, which is to fix an issue where production deployments incorrectly display preview URLs instead of correct production URLs.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Preview deployment failed.

Check the workflow logs for details.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/actions/projects.ts`:
- Around line 541-548: getCanonicalProductionUrl(project) can throw after the
rollback container is started, causing updateProductionStatus(input.projectId,
...) to be skipped; wrap the await getCanonicalProductionUrl(project) call in a
try/catch and on error fall back to the same localhost pattern used by
productionWaitReady (i.e. compute a localhost URL instead of throwing), then
call updateProductionStatus with productionHash: input.toHash,
productionStartedAt: new Date(), and productionUrl set to either the resolved
URL or the localhost fallback so the final updateProductionStatus always runs.

In `@src/server/productions/productionUrl.ts`:
- Around line 27-34: The repair currently calls
updateProductionStatus(project.id, project.productionStatus, { productionUrl:
canonicalUrl }) which overwrites the stored production_status with a stale
snapshot; replace this with a status-guarded URL-only update so you only set
productionUrl when the DB row still has the same status. Concretely, either
extend updateProductionStatus or add a new helper (e.g.,
updateProductionUrlIfStatusMatches) and have it perform an UPDATE that sets
production_url = canonicalUrl WHERE id = project.id AND production_status =
project.productionStatus (or accept an expectedStatus parameter), so you don't
write the stale productionStatus back into the row; call
getCanonicalProductionUrl and then this status-guarded URL-only updater instead
of the current updateProductionStatus invocation.
- Around line 5-15: The getCanonicalProductionUrl function should guard against
null/invalid productionPort and resolver exceptions: wrap the
getTailscaleProjectUrl(project.slug, "production", project.id) call in try/catch
and, on any error or if it returns null/undefined, fallback to a localhost URL
built from a validated port; treat project.productionPort as nullable and only
use it if Number.isInteger(+project.productionPort) && +project.productionPort >
0, otherwise use a safe default (e.g. 3000). Update getCanonicalProductionUrl to
perform this validation and error handling so it never returns
"http://localhost:null" and centralizes resolver-failure fallback behavior.

In `@src/server/queue/handlers/productionWaitReady.ts`:
- Around line 112-115: The fallback for computing productionUrl uses
getCanonicalProductionUrl(project) which can fall back to
project.productionPort; update the catch path in the Effect.tryPromise around
productionUrl to build the localhost fallback using the ready job's port
(payload.productionPort) instead of relying on project.productionPort so the
handler always uses the validated port; locate the Effect.tryPromise call that
assigns productionUrl in productionWaitReady.ts and change the catch to return
`http://localhost:${payload.productionPort}` (or equivalent string construction)
so the persisted URL won’t point at a stale DB port.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6284b8a7-d3ca-47e7-b85c-26554f482d3a

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc3134 and 419cea1.

📒 Files selected for processing (5)
  • src/actions/projects.ts
  • src/server/live/manager.ts
  • src/server/productions/productionUrl.ts
  • src/server/queue/handlers/productionStop.ts
  • src/server/queue/handlers/productionWaitReady.ts

Comment thread src/actions/projects.ts
Comment on lines +541 to 548
const { getCanonicalProductionUrl } = await import(
"@/server/productions/productionUrl"
);
await updateProductionStatus(input.projectId, "running", {
productionHash: input.toHash,
productionStartedAt: new Date(),
productionUrl: await getCanonicalProductionUrl(project),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep rollback state updates resilient to URL resolution failures.

getCanonicalProductionUrl(project) can reject, and this happens after the rollback container has already been started. Use the same localhost fallback pattern as productionWaitReady so a transient URL-resolution failure does not skip the final updateProductionStatus.

🛠️ Proposed fix
 			const { getCanonicalProductionUrl } = await import(
 				"@/server/productions/productionUrl"
 			);
+			const productionUrl = await getCanonicalProductionUrl(project).catch(
+				() => `http://localhost:${productionPort}`,
+			);
 			await updateProductionStatus(input.projectId, "running", {
 				productionHash: input.toHash,
 				productionStartedAt: new Date(),
-				productionUrl: await getCanonicalProductionUrl(project),
+				productionUrl,
 			});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/projects.ts` around lines 541 - 548,
getCanonicalProductionUrl(project) can throw after the rollback container is
started, causing updateProductionStatus(input.projectId, ...) to be skipped;
wrap the await getCanonicalProductionUrl(project) call in a try/catch and on
error fall back to the same localhost pattern used by productionWaitReady (i.e.
compute a localhost URL instead of throwing), then call updateProductionStatus
with productionHash: input.toHash, productionStartedAt: new Date(), and
productionUrl set to either the resolved URL or the localhost fallback so the
final updateProductionStatus always runs.

Comment on lines +5 to +15
export async function getCanonicalProductionUrl(
project: Pick<Project, "id" | "slug" | "productionPort">,
): Promise<string> {
const tailscaleUrl = await getTailscaleProjectUrl(
project.slug,
"production",
project.id,
);

return tailscaleUrl ?? `http://localhost:${project.productionPort}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden canonical URL fallback for missing ports and resolver failures.

productionPort is nullable, so the current fallback can produce http://localhost:null. Also, callers now need to catch Tailscale resolver failures individually; centralizing that fallback here keeps rollback, wait-ready, and live repair behavior consistent.

🛠️ Proposed fix
 export async function getCanonicalProductionUrl(
 	project: Pick<Project, "id" | "slug" | "productionPort">,
 ): Promise<string> {
-	const tailscaleUrl = await getTailscaleProjectUrl(
-		project.slug,
-		"production",
-		project.id,
-	);
+	const tailscaleUrl = await getTailscaleProjectUrl(
+		project.slug,
+		"production",
+		project.id,
+	).catch(() => null);
 
-	return tailscaleUrl ?? `http://localhost:${project.productionPort}`;
+	if (tailscaleUrl) return tailscaleUrl;
+
+	if (project.productionPort == null) {
+		throw new Error(`Cannot resolve production URL without productionPort for project ${project.id}`);
+	}
+
+	return `http://localhost:${project.productionPort}`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/productions/productionUrl.ts` around lines 5 - 15, The
getCanonicalProductionUrl function should guard against null/invalid
productionPort and resolver exceptions: wrap the
getTailscaleProjectUrl(project.slug, "production", project.id) call in try/catch
and, on any error or if it returns null/undefined, fallback to a localhost URL
built from a validated port; treat project.productionPort as nullable and only
use it if Number.isInteger(+project.productionPort) && +project.productionPort >
0, otherwise use a safe default (e.g. 3000). Update getCanonicalProductionUrl to
perform this validation and error handling so it never returns
"http://localhost:null" and centralizes resolver-failure fallback behavior.

Comment on lines +27 to +34
const canonicalUrl = await getCanonicalProductionUrl(project);
if (project.productionUrl === canonicalUrl) {
return canonicalUrl;
}

await updateProductionStatus(project.id, project.productionStatus, {
productionUrl: canonicalUrl,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard repair writes against stale status snapshots.

updateProductionStatus always writes productionStatus. If a live poll fetched this project as "running" and production is stopped before line 32 executes, this repair can set the row back to "running" and restore the URL. Persist the repair with a status-guarded URL-only update instead of writing the stale status snapshot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/productions/productionUrl.ts` around lines 27 - 34, The repair
currently calls updateProductionStatus(project.id, project.productionStatus, {
productionUrl: canonicalUrl }) which overwrites the stored production_status
with a stale snapshot; replace this with a status-guarded URL-only update so you
only set productionUrl when the DB row still has the same status. Concretely,
either extend updateProductionStatus or add a new helper (e.g.,
updateProductionUrlIfStatusMatches) and have it perform an UPDATE that sets
production_url = canonicalUrl WHERE id = project.id AND production_status =
project.productionStatus (or accept an expectedStatus parameter), so you don't
write the stale productionStatus back into the row; call
getCanonicalProductionUrl and then this status-guarded URL-only updater instead
of the current updateProductionStatus invocation.

Comment on lines +112 to +115
const productionUrl = yield* Effect.tryPromise({
try: () => getCanonicalProductionUrl(project),
catch: () => `http://localhost:${payload.productionPort}`,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the ready job’s port when computing the fallback URL.

This handler has already validated readiness on payload.productionPort, but getCanonicalProductionUrl(project) falls back internally to project.productionPort. If the DB value is stale/null, the persisted URL can point at the wrong localhost port without hitting this catch block.

🛠️ Proposed fix
 			const productionUrl = yield* Effect.tryPromise({
-				try: () => getCanonicalProductionUrl(project),
+				try: () =>
+					getCanonicalProductionUrl({
+						id: project.id,
+						slug: project.slug,
+						productionPort: payload.productionPort,
+					}),
 				catch: () => `http://localhost:${payload.productionPort}`,
 			});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/queue/handlers/productionWaitReady.ts` around lines 112 - 115, The
fallback for computing productionUrl uses getCanonicalProductionUrl(project)
which can fall back to project.productionPort; update the catch path in the
Effect.tryPromise around productionUrl to build the localhost fallback using the
ready job's port (payload.productionPort) instead of relying on
project.productionPort so the handler always uses the validated port; locate the
Effect.tryPromise call that assigns productionUrl in productionWaitReady.ts and
change the catch to return `http://localhost:${payload.productionPort}` (or
equivalent string construction) so the persisted URL won’t point at a stale DB
port.

@github-actions
Copy link
Copy Markdown

Preview deployment failed.

Check the workflow logs for details.

@pablopunk pablopunk merged commit 361f1fa into main Apr 23, 2026
17 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant