Featured Work

Replacing a Bespoke Schema-Migration System with Doctrine — and Finding a CLI Bug Nobody Knew About

Deleting a homegrown migration system is the easy part to describe. Enforcing its replacement as the only path for schema changes is what actually found the bugs that would have broken a real deployment.

June – July 2026 · Two phases · ~112 commits

Before — three paths, nothing enforces one YAML files no shared owner Upgrade scripts no shared owner Schema classes no shared owner After — one path, enforced Doctrine Migrations the only path now CLI bug caught here long flags never worked

Deleting the old system was the easy part. Enforcing the new one as the only path is what actually found the bugs.

The setup

TEAMBOX ran schema changes and upgrades through a homegrown, YAML-driven system: hundreds of YAML schema files, a set of legacy upgrade scripts, and a handful of custom schema classes to tie it together. It worked, in the sense that it had been running for years. It had also never been forced through the specific failure modes that only show up once you stop hand-rolling every schema change and put a real migration tool in charge of it.

Phase one: a replacement, not a patch

Built and shipped, solo, a full replacement of the old system with Doctrine Migrations: the entire old schema-definition layer, its YAML fixtures, and the legacy upgrade scripts that went with it are gone outright — a net reduction of thousands of lines across hundreds of files, as one continuous piece of work. A dedicated fixture-management layer, paired with Doctrine Migrations, now owns schema and test-database setup end to end.

Along the way: the CLI framework's long-flag parsing had never actually worked anywhere in the codebase. It silently fell back to defaults instead of erroring out — an untrimmed-whitespace bug that had gone unnoticed for years behind a short-flag fallback path, exactly the kind of thing any long-lived codebase accumulates. Fixed it and pinned the correct behavior down with a regression test, so it can't silently regress again.

Also added MySQL reconnect-on-disconnect handling and an advisory lock around the baseline-migration guard — failure modes the old system had simply never defended against, because it had never needed to. And chased an intermittent CI-only authentication failure through several dead ends — retry logic first, then in-process table drops — before landing on the actual fix: reusing a single shared database connection instead of opening a new one per test. Fifteen historical legacy-upgrade directories were deleted, but only once their behavior was confirmed already covered by the replacement — and staging-scenario migrations were deliberately written and reverted before merge, as a test method, not leftover cruft left behind by accident.

Phase two: making it the only path

Deleting the bespoke system is what made this pipeline fundamentally different from what came before. But enforcing Doctrine Migrations as the single path for schema changes — no more parallel legacy upgrade routes left running alongside it "just in case" — is what actually found the bugs that would have broken a real deployment.

A CLI flag parser where no long-form flag had ever worked, caught again from a different angle once enforcement made the gap unavoidable. A separate upgrade flag that crashed on every single real execution — not an edge case, the normal path. And a CI database-authentication failure traced to a password-decoding mismatch between the old credential convention and a newer one, the kind of gap that only appears once both conventions exist side by side and something finally has to pick.

The most serious find: old-system installs couldn't mark the new migration baseline as applied, because the metadata-storage table the new system expected didn't exist yet on those installs. That's a real deployment blocker, and it's the kind that only shows up the first time someone actually runs the migration for real — no test environment that starts from a clean slate would ever hit it. The migration strategy itself now lives as an architecture decision record that gates schema-change review going forward, instead of staying tribal knowledge that only one person actually understood.

What actually mattered

Deleting a bespoke system wholesale and rebuilding it on a real, maintained tool is what made this migration pipeline fundamentally different from what it replaced. But the flag-parsing fix, the table-existence check, and the connection-reuse fix are what made it safe to actually point at production.

None of those three were things the original migration work would ever have caught on its own, because none of them were "does the migration run." They were "does the tooling around it hold up under conditions nobody had actually tested" — and the only way to find that out was to take away the fallback path and force everything through the new one.

Doctrine Migrations Release Engineering CI/CD Database