Field Notes

My Pipeline Diagnosed the Same Deploy Bug Three Times and Fixed It Zero Times

August 13, 2026 · By Italo Campilii

My Pipeline Diagnosed the Same Deploy Bug Three Times and Fixed It Zero Times

TL;DR

On August 4 I put this site under version control. That single act changed where my deploys landed, because wrangler pages deploy infers the target branch from git: my local branch is master, the Pages project's production branch is main, so every subsequent deploy exited 0, printed a healthy *.pages.dev URL, and published to a preview environment while campilii.com served stale content. My agent caught it three separate times — August 5, August 8, August 12 — diagnosed it correctly each time, patched that day's deploy each time, and wrote the same action item each time: put --branch main in the brief. Nobody ever did. The recommendation was written to engine/log/decision-*.md, a file no downstream process reads, so the next run began from zero knowledge and paid the full diagnosis cost again. The defect was never the missing flag. It was a pipeline with a working detector and no actuator. A postmortem that terminates in prose is a postmortem that will be written again.

The interesting number in this postmortem is not the bug. It is three — the number of times my own automation found the bug, understood it, wrote it down clearly, ranked it as the highest-value fix available, and then let the next run walk into it.

Here is the second-worst part: every one of those diagnoses was good. If you handed me the August 8 writeup cold, I would have called it a solid piece of debugging. And here is the worst part: the fix was one flag.

What actually changed on August 4#

For months the deploy step was one line, and it worked:

wrangler pages deploy . --project-name campilii --commit-dirty=true

With no git repository present, wrangler has no branch to infer, and the deployment lands in production. That is the behavior I built on and stopped thinking about.

On 2026-08-04 at 22:26:04 EDT I ran git init and committed the site — an unambiguously good decision, made for unrelated reasons, with no apparent connection to deployment. But that command now had a repository to read. It found branch master. The Cloudflare Pages project's production_branch is main. A deploy whose branch is not the production branch is, by definition, a preview deployment.

Nothing errored. That is the whole problem. The command exits 0, prints a real, reachable, HTTP-200 URL, and reports success — because it did succeed. It deployed exactly what I asked, to exactly the place I implicitly told it to go.

The deployment history draws the boundary with unusual precision. Every deploy before the repo existed carries no commit metadata and landed in production. Every deploy after it produced a preview deploy first:

Date (UTC) Environment Branch Commit
2026-07-20 → 2026-08-04 (19 deploys) Production main (none)
2026-08-05 10:09:28 Preview master 531bab6
2026-08-05 10:10:31 Production main 531bab6
2026-08-08 12:17:22 Preview master 0a69361
2026-08-08 12:19:35 Production main 0a69361
2026-08-12 12:17:12 Preview master 0a69361
2026-08-12 12:17:51 Production main 0a69361

Read the pairs. Each bad deploy is followed by a corrective production deploy 63, 133, and 39 seconds later. Those gaps are the signature of an agent noticing, re-diagnosing, and re-fixing — in real time, from scratch, three times.

This is a class of failure worth naming, because it does not look like the failures you instrument for. The tool inferred a critical parameter from ambient environment state that changed for unrelated reasons. No config file was edited. No credential expired. No code path changed. The deploy command is byte-identical before and after. What changed was the world the command reads, and the command's contract says nothing about it.

The part I keep getting wrong: the exit code is not the outcome#

The thing that let this run for a week is that my validation checked the wrong object.

The pipeline verified that the deploy command succeeded. It did. It verified the returned URL served the new post with a 200. It did — the preview host was perfectly healthy. Both checks were green and both were measuring an artifact that had no bearing on whether a reader could see the page.

A *.pages.dev preview URL returning 200 is not weak evidence that production is fine. It is zero evidence. It is the deploy tool confirming its own work to itself, inside a loop that never touches the domain anyone actually visits. I had built the automation equivalent of testing a mock.

The cost was real. Per the August 8 writeup, the posts published on August 6 and August 7 returned 404 on the live domain until that day's corrective production deploy shipped them. Two days of work that the pipeline had already recorded as successfully published.

DEPLOY TARGET BY RUN — THE BOUNDARY IS THE GIT INIT JUL 20 AUG 4 AUG 12 19 DEPLOYS — ALL PRODUCTION git init → branch master AUG 5 +63s AUG 8 +133s AUG 12 +39s PREVIEW (master) — reported success, live domain stale PRODUCTION (main) — manual re-diagnosis, every time

The real defect: a detector with no actuator#

Everything above is a one-flag bug. It would not be worth a post. What is worth a post is what happened after each detection.

August 5 — caught it same-day. Diagnosed the git-inference mechanism exactly right. Wrote: "Action needed for future runs: the daily deploy command in the generator brief must include --branch main, or the local branch must be renamed to main. Without one of those, every future daily deploy will silently publish to a preview URL only."

That is a correct diagnosis, a correct prediction, and a correct prescription. Three days later the prediction came true.

August 8 — caught it again, and re-derived the entire mechanism from scratch, apparently unaware the same analysis existed three days earlier. It went further, running wrangler pages deployment list to establish the pattern historically, and correctly identified the 08-06 and 08-07 posts as 404 on the live domain. It ranked the fix explicitly: "Fix the deploy command in the wrapper brief… This is the highest-value item here."

August 12 — caught it again. Re-derived it from scratch again. Wrote ACTION REQUIRED in capital letters: "otherwise tomorrow's post deploys green and is invisible on the live domain."

Which brings us to today, where I opened engine/POST-BRIEF.md and found line 66 exactly as it has always been:

wrangler pages deploy . --project-name campilii --commit-dirty=true

Three correct diagnoses. Three written action items. One of them ranked highest-value, one in capital letters. Zero source changes.

The failure is architectural, and it is embarrassingly simple once you see it. Every one of those findings was written to engine/log/decision-YYYY-MM-DD.md. Nothing reads that directory. It is an append-only archive of things I have already learned, consulted by nobody and nothing, including the next run of the very system that wrote it. The pipeline could see perfectly and could not act. It had a detector and no actuator, and the gap between them was a human who was not reading the logs — because the whole point of the automation was that he would not have to.

This is the specific way autonomous systems rot. Not by failing to notice problems. By noticing them into a void.

How your situation differs from mine#

The mechanism generalizes well past Cloudflare, so before the fixes, locate yourself:

If you are a solo operator with agents that write their own postmortems, you have my exact failure and probably do not know it. Your agent is smart enough to diagnose but has no write access to the config that would prevent recurrence, and the loop closes through you reading a log file daily. You will not read it daily. Grep your last thirty days of agent output for "action required" or "action needed" and count how many were implemented. My score was 0 for 3.

If you are on a small team with CI, your version is subtler: the finding goes into a Slack thread or an incident doc, gets a nod, and never becomes a check. The tell is a runbook step phrased as "remember to…". Every "remember to" is an unimplemented actuator with a human as the failing component.

If your deploys are triggered from a laptop rather than CI, you are maximally exposed to the ambient-inference class specifically. Your machine's git state, shell environment, credential cache, and tool version are all inputs your command reads and your command's text does not mention. Any of them can change for reasons unrelated to deployment. CI does not remove the problem — it just makes the environment declared instead of inherited.

If you deploy infrequently — a marketing site, a docs site, a personal hub — the detection latency is what will hurt you. My engine runs daily and still took a week. At weekly cadence this class of bug hides for a month, and the stale content is live the entire time.

If you have a preview or staging environment at all, you have the specific hazard that made this invisible: a second, healthy, HTTP-200 URL that is not the one that matters. Any validation that accepts "a URL returned 200" without asserting which host will pass while production is frozen.

The fixes, in the order they matter#

1. Pin the target. Never infer it. The one-flag fix, applied to engine/POST-BRIEF.md today:

wrangler pages deploy . --project-name campilii --branch main --commit-dirty=true

The general rule is bigger than the flag: any deploy parameter that can be inferred from ambient state should be stated explicitly instead. Inference is a convenience that converts your environment into undeclared configuration. The failure mode is not that inference is wrong — it is that inference is silently right about the wrong thing, and produces a success.

2. Verify the artifact your reader touches, not the one your tool returns. Also added to the brief:

curl -s -o /dev/null -w '%{http_code}\n' "https://campilii.com/blog/<slug>/"

The domain is asserted literally. A 200 from any *.pages.dev host no longer counts as evidence of anything, which is the rule I most wish I had written in July. Note the propagation quirk I logged on both August 5 and August 12: the live URL can 404 for roughly 30–40 seconds after a production deploy before serving 200. Retry before you conclude — otherwise this check becomes a flaky alarm you learn to ignore, which is its own failure.

3. Give findings somewhere the pipeline reads. This is the one that actually fixes the class, and I am being deliberate about the design because the obvious version does not work. Writing better prose in decision-*.md changes nothing; the file is not the problem, its readership is. The finding has to land somewhere the next run is structurally forced to encounter — an assertion in the deploy path, a line in the brief the run executes, a preflight check that fails loudly. My interim version is the smallest thing that closes the loop: the deploy step now carries the constraint and the reason inline, so the flag cannot be removed by anyone — human or model — who has not first read why it exists.

The durable version is a preflight assertion, because a comment is still just prose. Before deploying, compare the local branch against the project's production_branch and refuse to proceed on mismatch. That converts a silent wrong-target success into a loud stop, and it survives the next well-intentioned refactor in a way a note never will. It is the same reasoning behind logging agent state where the code can read it rather than where a person might.

4. Treat a repeated diagnosis as a separate, higher-severity incident. When the same finding appears twice, the finding is no longer the story — the remediation channel is. I should have escalated on August 8, when a second correct diagnosis arrived. Instead I treated it as a second success, because it looked like one: the agent caught the bug, fixed the day, and moved on. Repeated correct detection is not resilience. It is a system paying full price for the same knowledge every day, which is the most expensive possible way to be right. It belongs in the same family as scheduled agents that fail without telling you — the difference being that here the telling worked fine and the listening did not.

The rule#

A finding that lands somewhere the system cannot read is not a fix. It is a rehearsal.

Autonomous pipelines fail this way far more often than they fail at detection, because detection is the part that feels like the hard work and gets the attention. My agent did the hard part correctly three times running. The system still shipped the bug three times running, because between "we know" and "it cannot happen again" there was a markdown file and a hope that someone would open it.

The audit takes two minutes and I recommend doing it before you finish reading. Open your agents' last month of output and grep for action required, action needed, TODO, should fix. For each hit, check the source file. If the count of correct-and-unimplemented findings is greater than zero, you do not have a detection problem, a model problem, or a prompt problem — you have an open loop, and every day it stays open you will keep paying full price to rediscover something you already knew. That is the same structural gap as agents whose work is never verified against production reality, arriving one step later in the cycle.

The bug in this post cost me a flag. Not knowing the difference between a detector and an actuator cost me a week, and it would have kept costing a week at a time indefinitely, in perfectly diagnosed daily installments.

SHIPPING WITH AGENTS AND NOT SURE IT ACTUALLY WENT LIVE?

The expensive failures in an automated pipeline are almost never wrong answers. They are correct answers landing where nothing can act on them — green deploys that published nowhere, findings written to files nobody reads, checks that verify the wrong artifact. Building the layer that closes those loops is the work I do. Get in touch, or see the press and hire page.

— Italo Campilii. Measured 2026-08-13 on my own infrastructure. The deployment table is read directly from the Cloudflare Pages API (GET /accounts/{id}/pages/projects/campilii/deployments, 25 records returned); environments, branches, commit hashes and timestamps are exact values from that response, and the 63/133/39-second gaps are computed from its created_on fields. production_branch = main comes from the same API's project endpoint; the local branch master from git rev-parse --abbrev-ref HEAD. The repository creation time is the first commit's author date. The three diagnoses and their quoted action items are verbatim from engine/log/decision-2026-08-05.md, decision-2026-08-08.md, and decision-2026-08-12.md. The 08-06 and 08-07 live-domain 404s are as recorded in the 08-08 decision log. engine/POST-BRIEF.md:66 was confirmed unchanged before this post was written and was fixed as part of this run. All live URLs checked 200 before publication. Nothing here is estimated.

IC

Italo Campilii

AI systems builder and one-person operator. I run the infrastructure for five brands solo with Claude Code agent fleets — SEO, content, e-commerce ops, and the verification layer that keeps it honest. The Build Log is where I write down what actually works.