Preview: reuse backend PR's Convex preview instead of redeploying (fix dual-deploy race)#1859
Preview: reuse backend PR's Convex preview instead of redeploying (fix dual-deploy race)#1859chelojimenez wants to merge 7 commits intomainfrom
Conversation
Opens an inspector PR on claude/pr-preview-workflow-Tu645 alongside a backend PR on the same branch name to reproduce the race where both upsert-backend-pr-preview (from the backend PR) and upsert-preview (from this PR) dispatch concurrent `convex deploy --preview-create` against the same Convex preview. https://claude.ai/code/session_01VoAzjkg2L5TuzNqyhnAyT5
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
When an inspector PR opens on a branch that already has an open backend PR, the inspector-PR flow used to unconditionally dispatch `inspector_preview_requested` to the backend, triggering a second `convex deploy --preview-create <branch>` that races the backend-PR flow's deploy of the same Convex preview. The losing deploy's `backend_preview_failed` callback wired this PR's Railway env to staging Convex, crashing `Open preview` on any PR-only schema. Fix: before `railway up`, look up an open backend PR on the same branch and read its `pr-be-<BN>` Railway env's VITE_CONVEX_URL / CONVEX_HTTP_URL. If both look like real preview URLs (distinct from staging), overwrite this env's Convex URLs with them so the build bakes in the shared preview URL, and skip "Dispatch backend preview request" so no redundant deploy fires. If the backend env isn't populated yet (race with backend's sync-backend-preview) the step short-circuits fast and falls through to the original dispatch path — paired with the backend's branch-keyed concurrency fix, that path is now race-safe. https://claude.ai/code/session_01VoAzjkg2L5TuzNqyhnAyT5
The previous Look up matching backend PR step ran `jq -r '.[0].number // empty'` on whatever curl returned. If BACKEND_PREVIEW_DISPATCH_TOKEN lacks pulls:read (it was originally scoped only to branches + repository_dispatch), GitHub returns a single JSON error object, jq errors "Cannot index object with number", and `set -euo pipefail` kills the step. That took out the whole upsert-preview job in ~17s on inspector#1859, leaving the preview URL blank and the comment stuck on "Backend target: preview requested". Fix: - Capture body + HTTP status separately; skip reuse gracefully on any non-200 (warning only, not an error). - Type-guard the jq filter to `type == "array"` so an error-object response falls through instead of aborting. - Allow jq itself to fail without killing the step (|| echo ""). Reuse is a nice-to-have; a missing token scope should never block the preview from deploying at all. https://claude.ai/code/session_01VoAzjkg2L5TuzNqyhnAyT5
Preview watchdogThe "preview requested" state has been stuck for ~6769 minutes. Recover: re-run the Watchdog runs every 15 minutes; this comment updates in place when conditions change. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughThe preview workflow now looks up a matching backend PR via the GitHub API and records existence and number without failing on non-200 responses. If a backend PR exists, the workflow inspects its Railway environment and, when Convex URLs are present, valid, and differ from staging, it rewires the preview’s Convex endpoints and flags reuse. The Convex reachability check and backend dispatch respect the reused URL (skipping dispatch when reused). The preview comment’s BACKEND_MODE notes shared backend info when reuse occurs. A newline was appended to docs/README.md. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
docs/README.md (1)
47-47: Remove the test-only breadcrumb from docs.This hidden comment reads like PR scaffolding, not lasting documentation. Keep the race context in the PR or workflow comments instead.
Proposed cleanup
-<!-- Test PR to reproduce dual-deploy race on shared Convex preview branch. -->🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/README.md` at line 47, Remove the test-only HTML comment "<!-- Test PR to reproduce dual-deploy race on shared Convex preview branch. -->" from the README by deleting that line (it's a transient PR scaffold, not documentation); ensure no other PR-scaffold or test-only breadcrumbs remain in the docs and move any necessary race/context explanations into the PR description or workflow comments instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/pr-preview.yml:
- Around line 241-263: Normalize and validate the VITE_URL and HTTP_URL before
comparing or reusing them: trim trailing slashes and run a simple URL format
check (scheme + host) to avoid malformed strings passing; then compare the
normalized VITE_URL/HTTP_URL against STAGING_VITE_CONVEX_URL and
STAGING_CONVEX_HTTP_URL (also normalized) to decide early exit. Additionally, if
available in the Railway backend env, read CONVEX_DEPLOY_KEY and require it to
start with "preview:" before allowing reuse; only after
normalization+validation+deploy-key check should you call the railway variable
set step and write vite_convex_url/convex_http_url to GITHUB_OUTPUT using the
normalized values.
---
Nitpick comments:
In `@docs/README.md`:
- Line 47: Remove the test-only HTML comment "<!-- Test PR to reproduce
dual-deploy race on shared Convex preview branch. -->" from the README by
deleting that line (it's a transient PR scaffold, not documentation); ensure no
other PR-scaffold or test-only breadcrumbs remain in the docs and move any
necessary race/context explanations into the PR description or workflow comments
instead.
🪄 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: ae63d0cc-3929-4c3f-bd9c-461ab03302fd
📒 Files selected for processing (2)
.github/workflows/pr-preview.ymldocs/README.md
| # Only reuse real Convex preview URLs. If the backend Railway env | ||
| # is still on its staging-duplicated defaults (sync-backend-preview | ||
| # hasn't run yet), skip reuse so we don't mask a real race. | ||
| if [ -z "$VITE_URL" ] || [ -z "$HTTP_URL" ] \ | ||
| || [ "$VITE_URL" = "$STAGING_VITE_CONVEX_URL" ] \ | ||
| || [ "$HTTP_URL" = "$STAGING_CONVEX_HTTP_URL" ]; then | ||
| echo "::notice::Backend env $BE_ENV has no preview Convex URLs yet; falling through to dispatch." | ||
| echo "reused=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| .github/scripts/railway-retry.sh railway link \ | ||
| --project "$RAILWAY_PROJECT_ID" \ | ||
| --environment "${{ steps.meta.outputs.environment }}" \ | ||
| --service "$RAILWAY_INSPECTOR_SERVICE" \ | ||
| --json >/dev/null | ||
| .github/scripts/railway-retry.sh railway variable set \ | ||
| -e "${{ steps.meta.outputs.environment }}" \ | ||
| -s "$RAILWAY_INSPECTOR_SERVICE" \ | ||
| VITE_CONVEX_URL="$VITE_URL" \ | ||
| CONVEX_HTTP_URL="$HTTP_URL" | ||
| echo "reused=true" >> "$GITHUB_OUTPUT" | ||
| echo "vite_convex_url=$VITE_URL" >> "$GITHUB_OUTPUT" | ||
| echo "convex_http_url=$HTTP_URL" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are the current Convex preview deployment URL formats and environment variables that distinguish preview deployments from production/development deployments?
💡 Result:
Convex preview deployment URL formats follow the standard Convex deployment pattern: https://[unique-adjective]-[unique-noun]-[number].convex.cloud, identical to production and development deployments. The unique identifier typically incorporates the preview name (e.g., Git branch name) to distinguish them. All deployment types share the same domain and format; distinction is based on the specific subdomain generated during deployment. To distinguish deployments in code: - Parse process.env.CONVEX_CLOUD_URL (always available in Convex functions, e.g., https://dusty-nightingale-847.convex.cloud). Preview deployments have unique subdomains tied to branch names. - Check process.env.CONVEX_DEPLOY_KEY during deployment (starts with 'preview:' for preview deploy keys vs 'prod:' for production). - No dedicated environment variable like 'DEPLOYMENT_TYPE' is documented for runtime distinction in functions; use URL parsing or deploy key prefix if needed. Development deployments are per-user and also use unique subdomains. Preview deployments are created using a preview deploy key set in CONVEX_DEPLOY_KEY (format: preview:team-slug:project-slug|ey...), triggered in CI like Vercel/Netlify previews.
Citations:
- 1: https://docs.convex.dev/production/hosting/preview-deployments
- 2: https://docs.convex.dev/production/hosting/environment-variables
- 3: https://github.com/get-convex/convex-js/blob/main/src/cli/deploy.ts
- 4: https://docs.convex.dev/cli/deploy-key-types.md
- 5: https://docs.convex.dev/production/hosting/vercel.md
- 6: https://docs.convex.dev/production/hosting/vercel
Harden URL validation, but note that format checks alone won't distinguish preview from production.
Line 244's check for empty/staging-exact values is insufficient—trailing slash mismatches or malformed URLs could pass through. URL normalization is needed to catch those edge cases.
However, the suggested regex validation against .convex.cloud won't actually prevent a production URL from reusing, since production and preview deployments share identical domain formats (https://[adjective]-[noun]-[number].convex.cloud). If the workflow must verify these are preview URLs specifically, the only documented distinguishing signal is the CONVEX_DEPLOY_KEY environment variable (prefix preview: vs prod:), not the runtime URL format.
Consider:
- Normalize URLs to remove trailing slashes and validate format (prevents malformed URLs)
- If available in the Railway backend env, check
CONVEX_DEPLOY_KEYforpreview:prefix to confirm deployment type - Otherwise, rely on the Railway environment itself being correctly configured as preview-only
Normalized URL validation shape
+ normalize_url() {
+ printf '%s' "$1" | sed 's#/*$##'
+ }
+ VITE_URL=$(normalize_url "$VITE_URL")
+ HTTP_URL=$(normalize_url "$HTTP_URL")
+ STAGING_VITE_URL=$(normalize_url "$STAGING_VITE_CONVEX_URL")
+ STAGING_HTTP_URL=$(normalize_url "$STAGING_CONVEX_HTTP_URL")
+
# Only reuse real Convex preview URLs. If the backend Railway env
# is still on its staging-duplicated defaults (sync-backend-preview
# hasn't run yet), skip reuse so we don't mask a real race.
- if [ -z "$VITE_URL" ] || [ -z "$HTTP_URL" ] \
- || [ "$VITE_URL" = "$STAGING_VITE_CONVEX_URL" ] \
- || [ "$HTTP_URL" = "$STAGING_CONVEX_HTTP_URL" ]; then
+ if [ -z "$VITE_URL" ] || [ -z "$HTTP_URL" ] \
+ || [ "$VITE_URL" = "$STAGING_VITE_URL" ] \
+ || [ "$HTTP_URL" = "$STAGING_HTTP_URL" ] \
+ || ! [[ "$VITE_URL" =~ ^https://[A-Za-z0-9-]+\.convex\.(cloud|site)$ ]] \
+ || ! [[ "$HTTP_URL" =~ ^https://[A-Za-z0-9-]+\.convex\.(cloud|site)$ ]]; then
echo "::notice::Backend env $BE_ENV has no preview Convex URLs yet; falling through to dispatch."
echo "reused=false" >> "$GITHUB_OUTPUT"
exit 0
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Only reuse real Convex preview URLs. If the backend Railway env | |
| # is still on its staging-duplicated defaults (sync-backend-preview | |
| # hasn't run yet), skip reuse so we don't mask a real race. | |
| if [ -z "$VITE_URL" ] || [ -z "$HTTP_URL" ] \ | |
| || [ "$VITE_URL" = "$STAGING_VITE_CONVEX_URL" ] \ | |
| || [ "$HTTP_URL" = "$STAGING_CONVEX_HTTP_URL" ]; then | |
| echo "::notice::Backend env $BE_ENV has no preview Convex URLs yet; falling through to dispatch." | |
| echo "reused=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| .github/scripts/railway-retry.sh railway link \ | |
| --project "$RAILWAY_PROJECT_ID" \ | |
| --environment "${{ steps.meta.outputs.environment }}" \ | |
| --service "$RAILWAY_INSPECTOR_SERVICE" \ | |
| --json >/dev/null | |
| .github/scripts/railway-retry.sh railway variable set \ | |
| -e "${{ steps.meta.outputs.environment }}" \ | |
| -s "$RAILWAY_INSPECTOR_SERVICE" \ | |
| VITE_CONVEX_URL="$VITE_URL" \ | |
| CONVEX_HTTP_URL="$HTTP_URL" | |
| echo "reused=true" >> "$GITHUB_OUTPUT" | |
| echo "vite_convex_url=$VITE_URL" >> "$GITHUB_OUTPUT" | |
| echo "convex_http_url=$HTTP_URL" >> "$GITHUB_OUTPUT" | |
| normalize_url() { | |
| printf '%s' "$1" | sed 's#/*$##' | |
| } | |
| VITE_URL=$(normalize_url "$VITE_URL") | |
| HTTP_URL=$(normalize_url "$HTTP_URL") | |
| STAGING_VITE_URL=$(normalize_url "$STAGING_VITE_CONVEX_URL") | |
| STAGING_HTTP_URL=$(normalize_url "$STAGING_CONVEX_HTTP_URL") | |
| # Only reuse real Convex preview URLs. If the backend Railway env | |
| # is still on its staging-duplicated defaults (sync-backend-preview | |
| # hasn't run yet), skip reuse so we don't mask a real race. | |
| if [ -z "$VITE_URL" ] || [ -z "$HTTP_URL" ] \ | |
| || [ "$VITE_URL" = "$STAGING_VITE_URL" ] \ | |
| || [ "$HTTP_URL" = "$STAGING_HTTP_URL" ] \ | |
| || ! [[ "$VITE_URL" =~ ^https://[A-Za-z0-9-]+\.convex\.(cloud|site)$ ]] \ | |
| || ! [[ "$HTTP_URL" =~ ^https://[A-Za-z0-9-]+\.convex\.(cloud|site)$ ]]; then | |
| echo "::notice::Backend env $BE_ENV has no preview Convex URLs yet; falling through to dispatch." | |
| echo "reused=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| .github/scripts/railway-retry.sh railway link \ | |
| --project "$RAILWAY_PROJECT_ID" \ | |
| --environment "${{ steps.meta.outputs.environment }}" \ | |
| --service "$RAILWAY_INSPECTOR_SERVICE" \ | |
| --json >/dev/null | |
| .github/scripts/railway-retry.sh railway variable set \ | |
| -e "${{ steps.meta.outputs.environment }}" \ | |
| -s "$RAILWAY_INSPECTOR_SERVICE" \ | |
| VITE_CONVEX_URL="$VITE_URL" \ | |
| CONVEX_HTTP_URL="$HTTP_URL" | |
| echo "reused=true" >> "$GITHUB_OUTPUT" | |
| echo "vite_convex_url=$VITE_URL" >> "$GITHUB_OUTPUT" | |
| echo "convex_http_url=$HTTP_URL" >> "$GITHUB_OUTPUT" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/pr-preview.yml around lines 241 - 263, Normalize and
validate the VITE_URL and HTTP_URL before comparing or reusing them: trim
trailing slashes and run a simple URL format check (scheme + host) to avoid
malformed strings passing; then compare the normalized VITE_URL/HTTP_URL against
STAGING_VITE_CONVEX_URL and STAGING_CONVEX_HTTP_URL (also normalized) to decide
early exit. Additionally, if available in the Railway backend env, read
CONVEX_DEPLOY_KEY and require it to start with "preview:" before allowing reuse;
only after normalization+validation+deploy-key check should you call the railway
variable set step and write vite_convex_url/convex_http_url to GITHUB_OUTPUT
using the normalized values.
- Drop the `<!-- Test PR to reproduce dual-deploy race ... -->` line from docs/README.md. It was scaffolding to open the test PR on this branch and doesn't belong in main. - Normalize trailing slashes on both the discovered and staging Convex URLs before comparing, so `https://x.convex.cloud/` vs `https://x.convex.cloud` can't slip past the staging-equality check and let staging URLs reach the reuse path. - Reject discovered URLs that don't match the `https://*.convex.{cloud,site}` shape — guards against a manually mis-set Railway var. Domain alone can't distinguish prod from preview Convex deployments (they share the format), but our scoping (only reading from `pr-be-<N>` envs) plus the MCPJAM_NONPROD_LOCKDOWN guard on every preview deployment make a prod-leak path infeasible without an attacker also writing to a backend-PR Railway env. Skipped the CodeRabbit suggestion to check `CONVEX_DEPLOY_KEY` for a `preview:` prefix — that secret lives only on the backend repo's GitHub Actions, not on Railway envs, so it isn't readable here. https://claude.ai/code/session_01VoAzjkg2L5TuzNqyhnAyT5
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/pr-preview.yml:
- Around line 216-280: The script leaves errexit disabled causing failures in
the final mutating Railway calls to be ignored; before invoking
.github/scripts/railway-retry.sh railway link and
.github/scripts/railway-retry.sh railway variable set (the two mutating calls
near the end of the step), re-enable errexit (e.g. restore set -e or set -euo
pipefail) so any non-zero exit from railway-retry.sh surfaces and the script
falls back to dispatch; after the mutating calls you can (optionally) restore
the previous -u behavior if needed.
🪄 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: da799b26-8cac-4846-9aba-6dc3e130222e
📒 Files selected for processing (2)
.github/workflows/pr-preview.ymldocs/README.md
✅ Files skipped from review due to trivial changes (1)
- docs/README.md
The step opened with `set -uo pipefail` (no -e) deliberately, so the early `railway link` probe to `pr-be-<N>` could soft-fail when the backend env doesn't exist yet. But that left the FINAL two `railway-retry.sh` calls — link + variable set on `pr-<N>` — with errexit still off. If either failed, the script would happily write `reused=true` to `$GITHUB_OUTPUT`, the dispatch step would be skipped, and `railway up` would build with the STAGING Convex URLs still on the env (set by "Configure staging-safe preview defaults") baked into the Vite bundle. Initial health check would target the backend's Convex from the step output, falsely report ✅, and the preview comment would claim `shared with <backend>#N` while the deployed inspector actually talks to staging — the exact crash mode this PR is supposed to prevent. Fix: re-enable errexit (`set -e`) right before the mutating calls, after every soft-fall-through path has already exited 0. https://claude.ai/code/session_01VoAzjkg2L5TuzNqyhnAyT5
Picks up `aceade9 "fix tests"` (and #1863 railway.json) so the branch's `Run Tests` CI check goes green.
Summary
Pairs with
MCPJam/mcpjam-backend#145. Fixes the silent-staging-fallback crash the author hits when cross-repo PRs open on the same branch.The bug: when this flow detected a matching backend branch, it unconditionally dispatched
inspector_preview_requestedeven if an open backend PR was already managing the Convex preview for that branch. Backend'sdeploy-previewran a secondconvex deploy --preview-create <branch>concurrently with the first (they were in different concurrency groups). The loser'sconvex env set/ smoke-test lost the race, firedbackend_preview_failed, and this Railway env was wired to staging Convex → "Open preview" crashed on any PR-only schema.The fix (this PR):
Look up matching backend PRfinds an open backend PR on the same branch.Reuse existing backend preview Convex URLsreads the backend-PR'spr-be-<BN>Railway envVITE_CONVEX_URL/CONVEX_HTTP_URLviarailway run printenv. If both look like real preview URLs (distinct from staging), it overwrites this env's Convex URLs beforerailway upso the Vite build bakes in the shared preview URL.Dispatch backend preview requestis skipped when reuse succeeded — no redundant deploy fires.Verify initial preview can reach Convexnow targets the reused Convex URL when reuse succeeded.Backend target: shared with <backend_repo>#<N>on reuse.Graceful fallback: if the backend env doesn't exist yet (PR opened seconds ago) or still has its staging-duplicated defaults (backend's
sync-backend-previewhasn't run yet), the reuse step short-circuits fast and the original dispatch path runs. Paired with the backend's branch-keyed concurrency fix, that path is now race-safe on its own.CORS: no change needed —
DEFAULT_ALLOWED_ORIGINSinmcpjam-backend/convex/http.ts:29-40already containshttps://*.up.railway.appandmatchesAllowedOriginhandles the wildcard, so both Railway preview URLs can talk to the shared Convex preview.Test plan
MCPJam/mcpjam-backend#145on the same branch.Backend target: shared with MCPJam/mcpjam-backend#145, notstaging fallback.deploy-previewrun (not two).mcpjam-backend#145, push a new commit here — inspector falls back to dispatching its owninspector_preview_requestedand gets a working preview standalone.https://claude.ai/code/session_01VoAzjkg2L5TuzNqyhnAyT5
Note
Medium Risk
Changes PR preview deployment logic in GitHub Actions to conditionally reuse backend PR Convex URLs and skip dispatching backend deploys; mistakes here could misconfigure preview environments or bake incorrect backend URLs into builds.
Overview
Prevents a dual-deploy race in the
pr-preview.ymlworkflow by detecting an open backend PR on the same branch and, when present, reusing that backend PR’s Convex preview URLs (read from the backend PR’spr-be-<N>Railway env) before runningrailway up.When reuse succeeds, the workflow now skips dispatching
inspector_preview_requested, updates the initial Convex health check to target the reused URL, and adjusts the PR comment to report that the backend target is shared with the matching backend PR. Also includes a no-op whitespace tweak indocs/README.md.Reviewed by Cursor Bugbot for commit 18759cc. Bugbot is set up for automated code reviews on this repo. Configure here.