Here is the worst kind of broken: the kind that looks fine. A pipeline that throws an error is a pipeline you fix in an hour. A pipeline that reports success while doing nothing can run for a week before anyone notices the site is frozen. That was this week. Here's the whole thing, because the pattern generalizes to every stack that auto-deploys from a Git push.
The setup
Standard modern static-site plumbing. Push to main on GitHub. A hosting provider (Cloudflare Pages, in our case, but Netlify and Vercel work identically) watches the branch, clones the repo, builds it, and swaps the live site. Zero-click deploys. It had worked for months.
Then it quietly stopped. The tell wasn't an alert — it was a human noticing that a copy change from three days ago still wasn't live.
The signature
Pull the deployment history and the shape of the bug jumps out immediately:
06:43 production trigger: github:push stage: clone_repo FAILURE commit: 052f0c7
06:43 production trigger: ad_hoc stage: deploy SUCCESS
06:36 production trigger: github:push stage: clone_repo FAILURE commit: 177b7c6
06:36 production trigger: ad_hoc stage: deploy SUCCESS
18:06 production trigger: github:push stage: clone_repo FAILURE commit: 2ff1728
Two things are true at once here, and holding both in your head is the whole diagnosis:
- Every push is reaching the host. A
github:pushdeployment gets created on every commit. The webhook is alive. - Every one of those dies at the first stage —
clone_repo— before a single line of the build runs.
And the reason nobody screamed: interleaved between the failures, someone had been quietly running manual uploads (trigger: ad_hoc) that did succeed. So the live site was updating — just by hand, every time, while the automation underneath it was dead. The workaround was masking the outage. That is the trap. A manual fallback that works is a smoke detector with the battery pulled out.
Why the error message lies
Open the failed clone and it says this:
remote: Repository not found.
fatal: repository 'https://github.com/<owner>/<repo>/' not found
Failed: error occurred while fetching repository
"Repository not found" is a lie — or rather, it's GitHub being deliberately vague. The repo exists. Commits are landing on it every few minutes. What actually happened: the repo is private, and the build runner's credential lost permission to read it. When you ask GitHub for a private repo you're not allowed to see, it does not return 403 Forbidden — that would confirm the repo exists. It returns 404 Not Found, identical to a repo that was never there. GitHub refuses to leak the existence of private repos to unauthorized callers. Good security. Terrible for debugging, because the message points you at the wrong problem.
So why does the push webhook still fire if the credential is dead? Because the two are different mechanisms. The webhook notification ("hey, there's a new commit") and the clone credential ("here's a token to actually read the code") are separate grants. Revoke access to the repo and the notification can keep arriving while the clone gets slammed shut. The pipeline looks connected and is functionally severed.
Is it the webhook, or the clone? Read the stage.
This is the fork that tells you where to look. Do not guess — the failing stage names the culprit.
★ No deployment appears at all
- The webhook itself is dead — the host never hears about the push
- Cause: integration uninstalled, branch filter wrong, or the whole Git connection removed
- Look at: the repo's webhook delivery log and the host's Git connection status
★ Deployment appears, dies at clone_repo
- The webhook is fine — the credential lost repo access
- Cause: app access revoked, repo recreated with a new ID, or made private without re-auth
- Look at: the host's GitHub App / OAuth grant and its selected-repositories list
Our failures were the right-hand column: deployments created, clone denied. That points at exactly one thing — the connected app's access to the repository, not the connection itself.
The fix
When a host clones a private repo it does it as a GitHub App installation (Cloudflare Pages, Netlify, and Vercel all work this way). If that installation's repository access gets edited — someone switches it to "only select repositories," or the repo gets recreated and inherits a new internal ID the old grant doesn't cover — the app silently loses read access. The clone starts 404-ing and nothing else changes.
- Go to your GitHub account's installed apps (
github.com/settings/installations). - Open the hosting provider's app (Cloudflare Pages / Netlify / Vercel) and hit Configure.
- Under Repository access, confirm the deploying repo is actually in the list. If it's on "only select repositories" and the repo is missing, add it — or switch to "all repositories."
- Save. That re-grants the installation token, and the next clone reads clean.
Then verify the trigger type, not just the color. A green deploy triggered by a manual upload proves nothing — you want to see a fresh github:push deployment reach success on its own. That is the only signal that the automation is actually back.
What the receipts say
The lesson worth keeping
Your deploy monitor should not alert on "latest deployment failed." It should alert on "no push-triggered deployment has succeeded in N hours." Those are different questions, and only the second one catches the silent deploy. A pipeline that fails loudly is healthy. A pipeline that fails quietly while a human props it up is the one that costs you a week — and, on a site that sells, real money in stale prices, dead links, and copy that shipped everywhere except where customers could see it.
Same root disease as the fire-and-forget tax: work that reports success while quietly dropping the payload. The cure is always the same — instrument the thing that's allowed to fail, and make sure the failure is louder than the workaround.
clone_repo with "repository not found." The repo is private and the host's GitHub App lost read access; the 404 is GitHub hiding a private repo, not a missing one. Re-grant the app's repository access, then confirm a real github:push deploy goes green. Alert on "no push-triggered success in N hours," never on color alone.