What's Actually in a Pipeline (And Why the Order Matters)

I wrote about attaching a runner a couple weeks back — the machine a pipeline actually runs on, and the fact that it reaches out to GitHub rather than the other way around. I ended that post with a promise: more on what actually goes into a pipeline once it's running. This is the start of that.
"The pipeline passed" or "the pipeline is red" gets treated like a single verdict, but it's almost never one check — it's a chain of small, independent jobs, each answering a different question, each running because the one before it passed. That's worth taking apart, because most of the value in a well-built pipeline is in the ordering and the boundaries between these jobs, not in any one of them individually. I still remember the stress after each merge request waiting to see if I got an email saying pipeline failed. A failing pipeline does not mean end of life or the software broke but it is a sign that the quality of your code needs a bit of improvement. Back when I started I thought I did something wrong and it added to stress of meeting deadline. On the point of meeting deadlines, I am not sure if it senses fear but right before a deadline it seems that something critical fails and needs adjustment right before it. Not sure if I am the only one who can relate to it.
The chain, roughly in the order it runs
- Lint and format — the fastest, cheapest check: known-bad patterns and enforced code shape, so it runs first and fails fast. For python it is mostly ruff
- Type checking — a whole-program consistency check, catching a class of bug lint structurally can't see because lint never looks past one file at a time.
- Tests — actual behavior, unit and integration, for whatever anyone bothered to write a test for.
- Static analysis / clean-code checks — structural rot that only exists across files: duplicated logic, runaway complexity, the stuff that makes a codebase slower to work in over time. A accumulating Tech debt.
- Build — compiling or bundling the code, then packaging it into an image, where "it works on my machine" stops being a valid excuse.
- Security and compliance scanning — which turns out to be three or four genuinely different checks (dependency CVEs, image scanning, static security analysis, secrets scanning) that just happen to get lumped into one label.
- Registry push and release — actually shipping the image and, if it's headed to Kubernetes, a Helm release — where tagging discipline quietly decides whether a rollback is a five-second command or a scramble.
Idle. Pick a run below.
Run it clean, or pick a stage to fail at and watch everything after it get skipped rather than run — that's the fail-fast ordering in one click, not just a claim in the text above it.
Why the order isn't arbitrary
Cheapest and fastest checks run first, most expensive last — a typo shouldn't cost you five minutes of test-suite runtime to discover, and a test failure shouldn't need to wait behind a full container build to surface. Every stage is also answering a genuinely different question from the one before it: lint asks "is this pattern known-bad," type checking asks "is this internally consistent," tests ask "does this actually behave correctly," static analysis asks "is this going to be maintainable," and security scanning asks "is this safe to expose to the internet." Collapsing any of them into one job loses the ability to tell, at a glance, which kind of problem just broke the build.
How I am doing it in this blog
This site's own pipeline doesn't have all seven of those stages, and that's deliberate, not a gap. It's a static export pushed straight to Firebase — there's no container, so there's no build-image, no registry push, no Helm release. The "build" stage here just one command that bundles, plain HTML/CSS/JS that gets uploaded directly. No image to scan, no cluster to release into.
What it does have: a separate Dependency Audit workflow, kept as its own file rather than a step glued onto the deploy job, specifically so a dependency problem can never block or delay a deploy — it runs npm audit and fails loudly (which is enough to trigger GitHub's own notification), on every push and on a weekly cron, so a CVE disclosed against something already installed gets caught even with zero new commits that week. Dependabot runs alongside it, opening the actual fix PRs. That's the "security scanning" stage from the list above, just sized to what a static site with no server-side attack surface actually needs, instead of the full four-tool version a containerized app would want.
The lesson I keep relearning from working on different kinds of projects: the eight-stage version is a menu, not a mandate. A pipeline should have exactly the stages that match what could actually go wrong for that specific project — no more, and, more easily forgotten, no fewer.
What's coming
Over the next weeks, I'll go through each of those stages individually — what it actually checks, a real example of it catching (or missing) something, and why it earns its own place in the chain instead of being folded into a neighboring job. Starting next week with the one that runs first and gets the least respect: lint.
Enjoyed this one? Get the next post by email — no spam, unsubscribe anytime.
Subscribe →