Background
I run a fleet of small sites that update themselves twice a day.
A scheduled agent regenerates content, builds, deploys, and commits the result.
For a long time that commit step was git add -A.
It was the obvious choice, and it worked right up until it didn't.
What went wrong
One morning the run committed ten files that had nothing to do with it.
They were half-finished changes I had left in the working tree from a different task the night before.
The agent did not know that.
git add -A stages everything, including work in progress, and the run pushed it straight to origin/main.
Nobody was watching.
That is the part that matters: an interactive git add -A is recoverable because you see the diff.
An unattended one is not, because the first person to notice is whoever pulls a broken intermediate state hours later.
The rule I settled on
The commit step now reads an allow-list and stages only paths that match it.
The config looks roughly like this, keyed by scope:
{
"defaultSiteAllow": ["data/", "public/"],
"scopes": {
"fleet": [
"automation/log/",
"automation/state.json",
"automation/issues.json"
]
}
}
A trailing slash means "everything under this directory", anything else is an exact match.
Each site gets data/ and public/ by default, because those are the generated artifacts a content run is supposed to produce.
Everything else is left alone.
Not committed, and — this is the important half — not deleted or reverted either.
Three outcomes, not two
The interesting design decision was the exit code.
A run that finds unexpected changes could fail, but that would stop content generation for a reason the machine cannot fix.
It could also silently ignore them, which is how you end up with a working tree nobody ever looks at.
So there are three outcomes:
- exit 0 — everything staged was in the allow-list
- exit 3 — the commit succeeded, and there were changes outside the allow-list
- exit 4 — the commit succeeded but the push never reached origin
Exit 3 is a success for the run and a signal for me.
The unexpected paths get appended to that day's log automatically, under a heading I can grep for.
Here is what today's run actually reported:
{
"committed": true,
"pushed": true,
"commit": "412959d",
"note": "想定外の変更を検出したためコミットから除外しました(作業ツリーには残っています)。",
"exitCode": 3
}
The excluded paths were a Python script from an unrelated feature branch and some CSS in a dashboard file.
Both are things I am actively editing.
Under the old behaviour, both would now be on main.
When you do want to include something
Sometimes the run legitimately needs to commit code — for example when it fixes a bug it found in its own tooling.
For that there is an explicit flag:
node automation/run.js git:commit --scope fleet \
--message "fix(preflight): detect uncommitted build inputs" \
--allow "automation/scripts/common/preflight.js,test/preflight.test.js" \
--push
--allow takes a comma-separated list of paths that get added to the allow-list for this one commit.
The point is not that it is hard to type.
The point is that the decision to include code now appears in the command, so it shows up in the shell history and in the log.
There is a second guard behind this one: even a path that matches the allow-list is skipped if it has a code extension, unless --allow names it.
That exists because some scopes list broad directories, and a broad directory will eventually contain a .js file that nobody meant to ship.
What I would tell past me
Reverting or stashing the unexpected changes would have been worse than committing them.
An automated job destroying uncommitted human work is a much harder failure to recover from than one that leaves a mess and tells you about it.
So the rule I would give anyone automating commits: stage an explicit list, leave everything else exactly where it is, and make "there was something else" a distinct, visible outcome.
Silence is the only option that is actually dangerous.
One of the sites this runs against is https://cve.autoarticles.net if you want to see what the output looks like.
This article is about my own side project. It was written with AI assistance.
Top comments (0)