DEV Community

Cover image for When every check blocks, verifying a small change costs an hour
Guillermo Leyendeker
Guillermo Leyendeker

Posted on Originally published at leyendeker.com

When every check blocks, verifying a small change costs an hour

Your pipeline started out fast. Then someone added the end-to-end tests, because they caught a real bug. Then the security review, because it caught another one. None of those checks is superfluous — that's why nobody proposes removing them — and today verifying a two-file change takes longer than writing it.

It's a natural slope: you add a check because it catches something real; because it catches something real, you don't want it skipped; so you make it mandatory. Repeat that five times over three months and you end up with nearly an hour of checks hanging off every task, however small.

What unblocked it wasn't deleting checks or making them faster. It was changing the question: not is this check useful? — they all were — but what decision does this check block?. Some have to block the individual change. For others, blocking the deploy is enough. Mixing the two is exactly what turns a pipeline into a funnel.

Below is how the split came out and, above all, how I made the change without breaking production: the quality gate was the piece everything else depended on.

The expensive part was the interface review

Within the gate, the slowest check was the model-assisted interface review: an agent that opens the browser, walks the application and judges whether what was implemented looks and behaves as it should.

It's valuable. It catches things no automated test detects. And it's enormously expensive in time, because it means bringing up the environment, navigating, waiting on every screen and reasoning about what it sees.

Running that on every task was the equivalent of asking a designer to review the entire application every time someone changes a line of code.

The right question isn't whether the check is worth it

The change of question reordered everything, and the reason is economic before it is conceptual: a control that stops a task from closing and one that stops a deploy to production are not the same thing. Their waiting costs are completely different. The first is paid dozens of times a week; the second, once per release. Mixing them is what leads a team to eventually disable both.

That's where the two tiers came from.

Tier 1: what blocks each task

Fast, deterministic and per task. Three things: the full backend suite, the end-to-end tests belonging to that task running headless, and a security trace.

The key part is "belonging to that task". Not the full end-to-end suite: only the specs covering what was just touched. That's what takes the time from an hour down to minutes.

Tier 2: what blocks the deploy

Slow, expensive and batched. That's where the full end-to-end suite and the model-driven interface review went, the latter running over tasks Tier 1 didn't cover.

That batch runs at two moments: before deploying, blocking, and every night as a safety net. If something fails, it doesn't jam the day's work — it jams the release, which is where that control actually matters.

On screen the split was made visible with two separate columns, one per tier, each with its own state. Visibility matters: if a check doesn't block, it has to be crystal clear that its result still exists and that someone is going to look at it.

The delicate part: changing something everything depends on

This is the part that gave me the most trouble and the one least often written about.

The quality gate is a load-bearing piece. The completion steps refuse to mark a task as done if the gate didn't pass. And that pass, as it was written, explicitly required the interface review's result.

Which means: if I did the intuitive thing — pull the interface review out of the per-task gate — the entire system would stop being able to close tasks. Not some of them: all of them. Every completion would start failing while looking for data nobody produced anymore.

That's why the plan carries a rule written in capitals: if you stop producing the signal before you stop requiring it, every completion breaks.

The implementation order was fixed in five stages, and the first is the counterintuitive one:

  • First, decouple the requirement. Make the pass stop requiring the interface review. It's a backward-compatible change: if the signal is present it's ignored, if it's absent it no longer blocks. At this point the system behaves exactly as before.
  • Then add the new Tier 1 checks, which is purely additive.
  • Only at the end, make the skill stop producing the per-task signal. It's safe now, because nothing has required it for two steps.
  • The screen columns and the batch runner, which are net-new and don't touch the existing path.

It's the kind of sequence that looks bureaucratic when you write it and saves your day when you execute it. Had I started with the obvious step — removing the interface review — I would have broken task completion across every managed project simultaneously.

A detail that avoided a lot of pain

Every new check reads the project's configuration and skips itself gracefully when the project doesn't declare what it needs.

Some managed projects have no end-to-end tests at all. If the gate demanded them, those projects would be permanently stuck behind a requirement that doesn't apply. The rule that stuck is: the gate can't demand what the project doesn't declare it has.

It sounds obvious written down, but it's the kind of thing you discover when the same mechanism runs across projects of different shapes.

The outcome and what followed

The goal stated in the plan was to bring the per-task gate down from roughly an hour to minutes, across every project at once. That was achieved.

And with the system finally running fast, a problem the slowness had been masking came into view: every time I restarted the server, I lost the entire work plan. That's what the next entry is about.

Top comments (0)