Shipping & Infra5 min read

Every job was green for two weeks. The site was stale for two weeks.

A weekly collector bot succeeded every run. Sixty new products, 692 pages built. The live site had been frozen since 07-27.

#ci-cd#automation#gotchas#deployment
Left: three weekly bot jobs all reporting success. Right: over the same period, the live site's sitemap frozen two weeks in the past
Left is the Actions tab. Right is the actual site. They told different stories for two weeks.

One of my product catalog sites runs on weekly bots. Mondays it discovers new products and commits them as data; Tuesdays it approves image candidates. Both jobs had been success for weeks.

One question for you. Does a green CI run mean your users see the result? I had been treating those as the same sentence.

The jobs were genuinely fine

Straight from the log:

Discovered 1312 raw product rows.
Added 60 new product rows.
Imported 60 products. Catalog now has 474 products.
Generated 692 pages
Checked 693 HTML files; no missing internal links.

Discovery works, import works, the build works, the link checker passes. It committed and pushed. Not a single failed run in the history.

Until I counted the live site

Then the deploy history caught my eye. The last Deploy run was 07-31. After that there were three data commits — 08-03, 08-04, 08-10 — and zero deploys.

So I counted the site itself.

$ curl -s https://<site>/sitemap-pages.xml | grep -c "<loc>"
572
$ curl -s https://<site>/sitemap-pages.xml | grep -o "<lastmod>[^<]*" | sort -u | tail -1
<lastmod>2026-07-27

The repo had 474 products and 692 pages. The live site had 572 pages and a lastmod of 07-27. Two weeks of new products did not exist on the web. The bot worked hard; the output piled up in the repository only.

The cause was a GitHub safety rule

The deploy trigger is unremarkable:

on:
  push:
    branches: [main]

And the data bots push using GITHUB_TOKEN. That's the trap. A push made with GITHUB_TOKEN does not trigger other workflows. It's a deliberate GitHub rule that stops workflows from waking workflows forever.

Which means the pipeline was always shaped like this:

  • Human pushes → deploy runs (so it looked healthy through 07-31)
  • Bot pushes → nothing happens, silently

My last hand-written commit was 07-31, and there were no human pushes after it. The pipeline broke the moment I stopped touching it.

How would you reconnect it?

There's a choice. Issue a personal access token, have the bot push with that, and the trigger comes back. Simple. And you now own one more secret that will one day expire and stop things silently. I had already lived through a token dying quietly and taking bots with it.

So I picked the path without a new token. Open a workflow_call trigger on the deploy workflow and have the data workflows call it directly.

# deploy.yml
on:
  push:
    branches: [main]
  workflow_dispatch:
  workflow_call:      # bot commits never fire the push trigger, so accept direct calls
 
# catalog-audit.yml
  deploy:
    needs: refresh
    if: needs.refresh.outputs.pushed == 'true'
    uses: ./.github/workflows/deploy.yml
    secrets: inherit

One extra piece: the commit step writes pushed=true/false to $GITHUB_OUTPUT, and the deploy job only runs when it's true. So a week with no changes doesn't touch production. Deploying is an action with side effects, and side effects are better spent only when there's a reason.

Verify on the live site, not the log

The push triggered a deploy. Then I counted again.

Item Before After
Live pages 572 692
sitemap lastmod 07-27 08-10
A new product page (absent) HTTP 200

Verified with live numbers, not repo numbers. That habit comes from the time the deployed page was an entirely different app. A 692 in the build log means "692 on my machine." Only the 692 in the sitemap means "692 in the world."

Bonus: sometimes silence is correct

The same pipeline hadn't sent a Telegram approval message in weeks either. I assumed that was broken too. The last line of the log answered it:

Discovered 1 image candidates for 16 products.
Auto-approved 1 images.
No image candidates waiting for Telegram approval.

Images from trusted domains auto-approve, so nothing was left to ask a human about. Here, no notification was the design working. Telling a broken silence from a correct one comes down to reading that job's final line of output.

Three lines of self-diagnosis

  1. In any repo where a bot pushes, when did you last deploy? Filter the Actions list to Deploy and compare its latest date to your data commits. Everything in between shipped nowhere.
  2. Are you counting the live artifact? The <loc> count and <lastmod> in sitemap.xml are a free deploy verifier that needs no login. Count that, not the build log.
  3. Do you know the last healthy output of your quiet alert channel? "Nothing arrived" can't distinguish a failure from an unmet condition.

The honest part

This site gets almost no traffic. Two weeks stale, and nobody complained — which is exactly why it lasted two weeks. Failures that don't hurt live a long time. I didn't find it by looking at the site either; I found it while checking whether collection was still running.

And once more, the real lesson isn't CI syntax. I was using "job success" as evidence of shipping. It isn't evidence. Automation does not prove that it made it all the way through. The proof has to be counted at the last point in the chain: the screen a user sees.

When did you last deploy? Would you check it on the actual site rather than the Actions tab?

Related