When did you last read your deploy script? Read, not run.
I read mine that day, because I wasn't in a hurry. If I had been, I'd have run it, and a day's work would have quietly disappeared.
The situation
I needed to ship a fix for some wrong strings inside the app. The deploy script already existed: upload the bundle, update the listing, commit — all in one run.
Before running it, I read it.
The script overwrites live
Every run overwrites the live listing with the repo's listing files.
Normally that's correct. The repo is the source of truth and the deploy reflects it.
The problem was that the day before, I had bulk-edited the store listings via API. Localised titles, rewritten bodies, filled short descriptions. Those changes landed in the store and never landed in the repo files.
I diffed them.
| App | Fields drifted |
|---|---|
| A | 47 |
| B | 14 |
| C | 8 |
| D | 0 |
One app's repo English body was 663 characters; live was 2,000. Sixteen titles were English in the repo and localised live.
Running the deploy would have reverted the entire previous day. And that revert is silent — the deploy exits successfully and the old copy goes to the store. No failure log, no warning.
Which side would you call the source of truth?
There's a decision here: is the repo authoritative, or is live?
This time live was authoritative, because the API edits were the newest state. So I overwrote the repo with the live values. Not the other direction.
for locale in repo_listing:
for field in ("title", "shortDescription", "fullDescription"):
if repo[locale][field].strip() != live[locale][field].strip():
drift.append((locale, field))
repo[locale][field] = live[locale][field]The .strip() is there for a reason. Seven locales had two leading spaces on every line, and counting whitespace-only differences as drift makes the list useless.
Another defect in the same script
It takes no arguments, so passing --dry-run is ignored and it commits straight to production.
That already caused an incident once — my dry run shipped to production. Today there's a guard that dies if the flag appears. Dying beats ignoring: if it's ignored, a human believes they ran in safe mode.
The listing API is also a full replace
One more thing to keep in mind: the listing update API is a full replace.
Unspecified fields get erased, so even a one-field edit means reading the rest and writing it back. That was the same trap when comparing app locales against store locales.
So this incident is the combination of a full-replace API plus a stale repository. Either one alone is harmless.
Three-line self-check
- Which direction does your deploy script overwrite? If it's repo → live, ask whether anyone has ever edited live directly via API or console.
- Is there a diff step before the deploy? Without one, a revert is indistinguishable from a successful deploy.
- Do you have a script that doesn't know
--dry-run? If it doesn't know the flag, it should die rather than ignore it.
The honest part
I found this by reading the script, not by catching it automatically. In a hurry I'd have run it and noticed the revert days later. Process didn't save me; having spare time that day did.
What I actually did was sync the repo files from live. The structure is unchanged — the next time someone edits a listing via API and doesn't update the repo, this happens again. The right fix is a diff step inside the script, and I haven't done it. This post is partly a record of that unfinished work.
There was also a phantom locale folder in the repo that doesn't exist live. I don't know why it's there. I didn't delete it either — deleting something you don't understand is also a revert.
If you have a deploy script, check one line before your next run: what does this overwrite? A revert doesn't look like a failure.