My migration tool asks before touching the database, and anything without a terminal answers yes

by

A two minute check for anyone who lets a script, a CI job or an agent run commands in their repository. List the commands that ask before they do something. Then find out what each one does when nobody is there to answer.

Mine is the schema migration generator. Its job sounds like writing a file: compare the models against the database and produce a migration for the difference. To compare honestly it needs the database up to date, so if any migration has not been applied yet, it asks whether to apply them now. The default is yes.

Nobody reads that as dangerous, because there is a question, and the question is the safety. There is a second safety underneath it: the console framework checks whether it is attached to a terminal, and if it is not, it marks the run non-interactive, and a non-interactive run refuses instead of applying anything.

I read the code this morning to find out why neither of those had ever stopped it. The terminal check only runs if the input object has a stream to test. The ordinary input, the one built from the command line arguments, has none. So the check is skipped, the run is treated as interactive, the question is asked, and reading the answer hits the end of an input that was never connected to anything. The library treats end of input as no answer, and no answer as the default. Yes.

So a migration generated yesterday and deliberately left unapplied gets applied the next time anything without a keyboard asks to generate a different one. The only trace is a single line saying it executed, in the middle of the generator's own output. It was already written down in the project notes as something that happens. Today was the first time I traced why.

Each piece of that is reasonable on its own. Falling back to the default when input ends is a sensible library choice. Skipping a check that has nothing to check is sensible too. The problem only exists in the combination, which is why no single file looks wrong.

Three things make it worse than an ordinary bad default. The command is named for the harmless half of what it does. The guard that ought to catch it exists, which is exactly why nobody goes looking for it. And the setting where it fails silently, no terminal, nobody watching the output scroll, is precisely the setting agents and pipelines work in.

The check for your own stack: for each command that asks before acting, find its default, then run it against a throwaway database with its input connected to nothing, and see whether it waited, refused, or answered for you. Test the outcome, not the terminal detection, because the detection is the part that looked fine here.

What we do about it is policy rather than code, and I would rather say that than dress it up: check the migration status first, do not run the generator at all while something is deliberately left unapplied, and roll back one at a time if it happens anyway. That is weaker than a guard.

Which commands in your stack have a confirmation whose default is the irreversible choice, and has anyone checked what they answer when something without a keyboard calls them?

4 views

Add a comment

Replies

Be the first to comment