I have a release script with fifteen automated checks. It passed all fifteen. Then I opened my own live demo and the About screen showed the previous version.
Here is what it checks, on the release in question:
PASS version 1.8.0 consistent (pubspec = CHANGELOG = app About screen)
PASS public changelog announces v1.8 and marks it Latest
PASS working tree clean (what you see is what packages)
PASS no merge-conflict markers in tracked files
PASS every local link on the sales site resolves
PASS archive contains no internal-only files
PASS archive contents are built from the current version (1.8.0)
PASS the site is up (/ returns 200 and is not a Pages error page)
PASS all 8 landing-page images load from the live site
PASS analytics beacon present on every public page
Not one of those was wrong. The version really was consistent. The changelog really was updated. The site really was up.
The demo is a separately compiled build, and my deploy script publishes the marketing site. Nothing built the demo, and nothing looked at what the demo was running.
Every check I own is scar tissue
The "is the site up" check exists because I once took the site offline. The "do the images load" check exists because a deploy deleted every image while the page still returned 200 — a status check alone said "fine" the whole time the logo was a broken icon.
They are all reactions to something that already went wrong. Which makes a fully green run a precise statement about my past mistakes, and silent about everything else:
green == "none of my previous mistakes recurred"
green != "nothing is wrong"
Those feel identical at 1am when you want to ship.
The fix is embarrassingly cheap
Flutter writes a version.json next to the web build, populated from pubspec:
{"app_name":"my_app","version":"1.8.0","build_number":"7"}
It's live, it's public, and it's a couple of hundred bytes. So you can just ask the deployed app what it is:
curl -s https://example.com/app/version.json
The check compares that to the version you think you're shipping:
$live = (New-Object Net.WebClient).DownloadString(
'https://example.com/app/version.json')
if ($live -match '"version"\s*:\s*"([0-9.]+)"' -and $Matches[1] -ne $pubVer) {
Fail "live demo runs $($Matches[1]) but you are shipping $pubVer"
}
The message matters as much as the check. Mine spells out the recovery, because forgetting the steps is the bug:
FAIL live demo at /app/ still runs 1.7.0 but you are shipping 1.8.0.
Rebuild and redeploy it: flutter build web --base-href /app/,
inject_beacon.ps1 build/web, then copy into the Pages clone.
Ten minutes of work for a hole that had been open for who knows how many releases.
Then I fixed it in the wrong place
This is the part worth your time, because the first failure is ordinary and this one isn't.
I rebuilt the demo, copied it into the GitHub Pages repo, pushed, watched the live version flip to the new number, and moved on.
Then I ran my deploy script for an unrelated change, and it put the old build back.
The layout is the whole story:
site/ <- source of truth, synced wholesale by the deploy script
index.html
app/ <- the published copy of the demo ** still 1.7.0 **
admin/
paragon-demo/ <- the GitHub Pages repo
app/ <- what I fixed by hand ** now 1.8.0 **
I had fixed the destination while the source still held the previous build. The next deploy faithfully republished the stale one, exactly as designed.
For a few minutes I had a "verified fixed" demo on a countdown to breaking again, and the thing that would break it was my own tooling working correctly.
Four things I took from it
Verify the artifact, not the action. "I deployed the fix" and "the fix is live" are different claims. I had confirmed the first and assumed the second.
Poll, don't peek. My first "it's live now" check passed against a CDN copy that hadn't rebuilt yet. Checking once and believing it is how you confirm a lie:
until curl -s "$URL/version.json" | grep -q '"1.8.0"'; do sleep 5; done
Fix the source, not the copy. If a pipeline generates a destination from a source, editing the destination is an illusion with an expiry date.
Check what the user touches. My pipeline was thorough about inputs: source files, version strings, archive contents, links. It had nothing to say about the compiled thing a stranger clicks, which is the only artifact that actually matters.
I found this the ordinary way, by looking at my own product like a user would, hours after telling myself it was done. Every check on that list was earned the same way. This one just cost less than the others, because I got there before a customer did.
Top comments (0)