Back

CI/CD

The CI gate that actually holds.

I build clients' systems end to end, and I run my own pipeline for checking code before it ships. One rule sits above everything: a broken build must never reach real users. Making the ship step actually wait for the checks to pass sounds like a one-line job. It is not. Here are the two ways that quietly fail, and the one that holds.

The goal
A broken build cannot ship
Trap one
The steps wait on each other
Trap two
The ship step cannot ask
The fix
One list of steps, in order
Published

The goal

A ship step that only pretends to wait

I hold every client's pipeline to the same rule, including the one behind the hosting product I built for a game studio. A broken build must never reach real users.

The goal sounds trivial. The step that ships to production should refuse to run until the checks pass. One step waits for another. How hard can that be.

On a setup I run myself, the two obvious ways to wire that up both fail. One locks up and never finishes. The other lets a broken build sail straight through.

Trap one

The two steps wait for each other forever

The obvious design has the ship step call the checks first, and ship only if they pass. One clean dependency.

These systems only let so many jobs run at once. Here, one. So the ship job starts, takes the only slot, and calls the checks. The checks now need a slot too. There are none left, because the ship job is holding it and waiting for them.

So the ship job waits for the checks. The checks wait for a slot. Nothing moves, ever. That is a deadlock, and it happens every single time.

The answer is not to debug it. It is to stop making two jobs coordinate at all.

Trap two

The ship step is not allowed to ask

The next design is gentler. A small watcher sits beside the ship step and keeps asking the system one question: have the checks passed yet? It holds the ship until the answer is yes.

The answer never comes. Every time the watcher asks, the system turns it away. Reading another job's result needs permission, and the automatic pass a job is handed does not include it. No permission, no answer, no gate.

The shape underneath is the real problem. Any time one job has to trust another job's word, over the network, inside the same system, you get something that only breaks in production.

The gate that holds

Put every check in one list of steps

The reliable answer is boring. Every check that must pass before shipping goes into a single job, in the same file as the ship step, one after another. The ship step simply comes last. It runs only if everything above it passed.

Nothing crosses a boundary now. There is nothing to lock up, and no other job's word to trust. That same job also signs each release so it cannot be swapped and scans it for known holes, before the ship step is allowed to run.

You pay a little for this. The code gets built twice, once to check it and once to ship it, a few extra minutes. That is a fair price for a gate that cannot quietly pass a broken build.

The lesson, plainly. A gate you can read top to bottom in one file is one you can trust. Coordination between jobs is where the silent failures live.

Three more traps

Three smaller traps that fail without a sound

None of these lock up or throw an error. Each one just ships quietly broken, and each has a fix worth keeping.

Passing secret passwords into a shared sub-job is off by default. Forget to switch it on and every password inside that job arrives blank. Logins fail, signing fails, and nothing says why.

A colon in the wrong place in a setup line breaks the whole thing before anything runs. The file format reads the colon as the start of a new setting, decides the file is broken, and throws it out. It looks like nothing happened at all. Writing that line as a block of text instead of one line makes the colon just a colon again.

And the tool that manages servers, run from the pipeline, has to be told where the current record of your servers is kept. Point a fresh run at an empty record and it will happily offer to build every server you already have, because as far as it knows, none of them exist yet.

Six checks that turn a pipeline which looks safe into one that is. Together they are an afternoon of work, cheaper than the first release that shipped a build nothing really stopped.

The thesis

Self-contained beats coordinated, every time

None of these traps are exotic. They are what happens when you take the parts a build system hands you and use them to make jobs talk to each other, instead of just running steps in order.

The urge to coordinate is understandable. Two jobs feel like they ought to be able to talk. Mostly they should not. A gate you can read top to bottom in one file is a gate that cannot lie to you.

If your deploys pass a build that never really had to succeed, that is worth catching before it costs you a release. Send me the pipeline and I will show you where the coordination is hiding.

Sources

  1. Reusable-workflow secret inheritance is opt-in; without an explicit inherit, secrets evaluate empty inside the called workflowdocs.github.comaccessed 2026-07-06
  2. Container image signing with cosign as a supply-chain gatedocs.sigstore.devaccessed 2026-07-06
  3. Container vulnerability scanning with Trivy as a build-time gatetrivy.devaccessed 2026-07-06
  4. Terraform needs a configured remote backend; an empty local backend plans existing infrastructure as new resourcesdeveloper.hashicorp.comaccessed 2026-07-06
  5. Deploys over a zero-trust tunnel instead of an open inbound SSH portdevelopers.cloudflare.comaccessed 2026-07-06