Fix All three Vercel production pointers stuck on CANCELED builds since 09-10; deploy-activity monitoring blind to it - #2706
Open
polylane[bot] wants to merge 1 commit into
Open
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes: All three Vercel production pointers stuck on CANCELED builds since 09-10; deploy-activity monitoring blind to it
Since 09-10 16:12Z all three production targets have pointed at CANCELED builds and no signal noticed: the docs-only commit triggered turbo-ignore skips on every project, Vercel moved the production pointers onto the canceled deployments, and production silently kept serving the previous build. Failing builds are just as silent as skips — nothing in the repo observes whether a push actually deployed. This adds an hourly watchdog that reads the GitHub deployments API Vercel already writes to and fails CI when production is not shipping.
flowchart LR A["push to main"] --> B{"Vercel GitHub app"} B -->|"affected app"| C["build READY"] B -->|"turbo-ignore: not affected"| D["build CANCELED"] C --> E["status success"] D --> F["status inactive - Skipped"] E --> G["production serves new build"] F --> H["production serves prior build"] E --> W["watchdog (hourly)"] F --> W G --> W W -->|"green or benign skip"| I["check passes, summary table"] W -->|"failure status or no deployment"| J["check fails, annotation names project"]What caused this
Affected:
acc_01b04ba9f00122r4mmzzgv9q· severity mediumBefore / After
❌ Before: a push to main either deploys or silently doesn't, and nothing in the repo can tell the difference. Docs-only commits skip all three Vercel builds; broken commits fail them — both leave production serving an older build with every visible signal reading healthy, as it has since 09-10 16:12Z.
✅ After: the watchdog runs hourly and writes a per-project table of which build production is actually serving. If a production build fails, a push never reaches Vercel, or a build gets stuck, the check run fails with a named annotation, and anyone reading CI sees production is behind. The skip-on-docs-commit case stays green and visible in the table.
What changed
.github/workflows/vercel-production-watchdog.yml(new): hourly scheduled workflow (plusworkflow_dispatch),contents: read+deployments: readpermissions, onlyghand the GitHub REST API — no new secrets. Per project it reads the Vercel-created deployments underProduction –and: (1) finds the newestsuccessdeployment = what production actually serves; (2) alerts on newer deployments withfailure/errorstatus; (3) alerts if a main push past a 30-minute grace has no Vercel deployment (broken webhook/integration); (4) alerts if the newest build sat inqueued/in_progress/pendingover 60 minutes. Writes a step-summary table per run; skips stay visible but green.Why it's safe
contents: readanddeployments: readand issuesgh apiGETs only — it cannot write, deploy, or revert anything; a bug in the script cannot damage production.inactive, deliberately treated as not-an-alert (the table records what production serves; onlyfailure/errorstatuses, a dead push, or a stuck build fail the check) — the pattern reviewed impact-none on every prior docs-only wave.sort_by(.id)(not created_at) picks the terminal status even when two statuses share a timestamp, andset -euo pipefailmakes any API failure exit non-zero rather than half-evaluating into a false green.Validation
oxfmt --check: exit 0, all files correctly formatted (oxfmt targets JS/TS; the YAML is out of scope by design).oxlintrepo-wide (declared lint): exit 0, 38 pre-existing warnings, 0 errors; no lintable files in the YAML.node scripts/check-doc-refs.mts(check:docsleg ofpnpm verify): exit 0, no dangling refs in 4171 files.bash -n: syntax OK, before and after the final edit.inactive/"Skipped - Not affected" andfailurestates the logic branches on all observed.pnpm test/verify:test— no package code touched; suites need a seeded local database irrelevant to a workflow YAML.Root cause and scoping notes
Root cause
apps/serverMCP docs. All three projects runnpx turbo-ignorewithenableAffectedProjectsDeployments(live settings), so all three builds canceled; Vercel moved the pointers onto the canceled deployments and postedinactivestatuses. Aliases kept serving the prior READY build — but the commit-status surface reads combined statesuccess,deployments_countcounts previews, and the graph mirrors the stale pointer.inactive, summarized) from the harmful modes (failure/error statuses, a push with no deployment, a stuck build), so production-not-shipping becomes a red CI run within an hour.Out of scope / follow-ups
Causal chain
npx turbo-ignorewith enableAffectedProjectsDeployments: true (live project settings). The commit touched no app, so all three production builds canceled and Vercel moved each productionDeploymentId onto the CANCELED deployment (readyStateReason: "The deployment was canceled because the commit didn't affect this project", read live). The skip posts GitHub statusinactive, which the commit-status surface folds into combined state success (verified live). deployments_count counts preview deploys; graph mirrors the stale pointer; per-request Vercel metrics return 400; zero of 13 monitors target tenant domains.npx turbo-ignore, enableAffectedProjectsDeployments true on all three projectsinactive/"Skipped - Not affected" while combined state reads success; c51014f dashboardfailurestatusDetection outcome
The finding's signal: "all three Vercel production pointers stuck on CANCELED builds; deploy-activity monitoring blind to it" — the family of production silently not shipping. Once merged, the hourly run re-evaluates that condition against the GitHub deployments API. The pure-skip state stays a summary row (expected turbo-ignore behavior), but the blindness stops: a failing production build (the a6f2b62 dashboard wave), or a main push that never created a Vercel deployment, now fails a visible check run within an hour instead of persisting undetected for 19 hours. The mechanism removed: nothing in the repo read the only deploy-outcome signal that exists (Vercel's GitHub deployment statuses); now one does, hourly, with no new secrets.
Fix chosen
Chosen: cause (removes the mechanism that produces the failure): Add a scheduled watchdog workflow reading the GitHub deployments API Vercel already populates, failing CI on the silent modes: production builds failing while production serves an older build, pushes that never created a Vercel deployment, and builds stuck in the queue.
Considered and not chosen:
1 file changed (+129/-0)
.github/workflows/vercel-production-watchdog.yml: added, +129/-0Repository conventions present in the repository: AGENTS.md, CLAUDE.md, apps/checker/AGENTS.md, apps/dashboard/AGENTS.md, apps/server/AGENTS.md, apps/status-page/AGENTS.md, apps/web/AGENTS.md, apps/workflows/AGENTS.md, packages/services/AGENTS.md, packages/ui/AGENTS.md.
Repository lint:
pnpm run lint(declared in package.json) could not run in the sandbox because its tool is not installed there; run it before merging.Generated by Polylane.