← sz3yan.com

#szejo ·

Three days, three silent failures — what a directory move actually breaks

Symptom There wasn't one. That's the whole story. I reorganised the directory layout of a platform repo. The code moved, the tests passed, the site stayed up, and nothing in any log said otherwise…

5 min read

Symptom

There wasn't one. That's the whole story.

I reorganised the directory layout of a platform repo. The code moved, the tests passed, the site stayed up, and nothing in any log said otherwise. Over the next three days I found three separate things that had been broken the entire time, each discovered by accident, none of which had ever produced an error message.

Investigation

The first turned up when a publishing pipeline quietly shipped nothing. The workflow was correct, the trigger was correct, the files existed on my machine — but the directory holding them was covered by an ignore rule, so they were never committed, and a checkout on the build machine simply found an empty folder. Nothing failed. There was just nothing there.

The second turned up a day later, after I pushed a fix for a real bug and went to watch it build. Out of habit I listed the recent runs instead of assuming, and the build I was waiting for wasn't in the list at all. Its workflow filtered on a path from before the move. A filter that matches nothing doesn't warn you; it just never queues the job. An absent run looks exactly like "nothing to do here", which is why a green dashboard had been telling me the truth and nothing useful at the same time.

The third I went looking for, and the way I found it was the interesting part.

I had removed a service from the platform the day before and wanted to be sure no traces were left, so I searched the repo for its name. That search was a dead end — a handful of comments, all of them tombstones I'd written myself saying the thing was gone. Useful tidying, no bugs.

Then I searched for something else: not the deleted service, but the old directory prefixes from the move. Thirty-one hits across fifteen files, some of them referencing a layout two reorganisations old. Most were comments, harmless to execution. Three were not:

  • Every one of the GitOps controller's application definitions pointed at a per-service directory that the move had flattened into single files. On its next sync, each of them would have failed to find its manifests.
  • The ignore rules meant to keep a provisioning tool's scratch output and its downloaded provider binaries out of version control named two directories, neither of which existed any more. So neither was being ignored. (I later found 77 MB of platform-specific binaries already committed, in a repository whose history is 176 MB.)
  • The contributor guide — the file both humans and coding agents read first to learn where things live — pointed at the pre-move location for service source.

There was a second dead end here worth naming. I nearly dismissed the GitOps finding because the cluster was healthy: if the application definitions were broken, surely something would be out of sync? Then I checked, and the controller wasn't running at all. Its namespace existed and was empty. A broken configuration for a component that isn't running produces exactly as much noise as a correct one.

Root cause and fix

All three are the same bug wearing different clothes.

A directory move updates the things that execute, because those fail loudly and immediately. It misses the things that merely declare, because a declaration that no longer matches reality doesn't crash — it silently describes nothing. A path filter that matches no files. An ignore rule that ignores nothing. A deployment definition pointing at a directory that isn't read until the next sync. A guide that misleads only the next person.

The fixes were mechanical once found: repoint all thirty-one references, and give each application definition an explicit include so that three of them sharing one flat directory don't each try to claim — and, with automatic pruning on, delete — the others' files.

While I was in there, the same instinct turned up a fourth: a service whose deployment names an identity it needs in order to spawn workloads, where that identity and its permissions existed only in the live cluster. They'd been created by hand during a rename months of uptime ago and never written back to the repository. It works today and would keep working forever — right up until the cluster is rebuilt from the repo, which is the one moment nobody is watching for it.

Lesson

Two, and the second is the one I'd keep.

After any move, grep the whole tree for the old prefix — not just the source. Config, ignore files, CI definitions, deployment manifests, documentation. Then, for each hit, ask the only question that matters: does anything actually read this? That question sorts thirty-one comment fixes from three real bugs in about a minute.

Categorise your failure modes by whether they can shout. Code that breaks tells you. Declarations cannot — the best they can do is match nothing, and matching nothing is indistinguishable from having nothing to do. Anywhere your system's behaviour depends on a string matching a path, you have a component that will fail in perfect silence, and you should go and look at it on a schedule rather than waiting to be told.

The cheap version of that check, for anything applied to a live system: apply your file in dry-run mode and see whether it reports unchanged. "Unchanged" means the file describes reality. Anything else is drift you didn't know you had.