Why pipeline design matters before you write any YAML
Most pipelines are not designed. They accumulate. A first workflow runs tests, someone adds a Docker build, someone else bolts on a deploy script, and a year later the release process lives in three places and two heads. The symptoms are familiar: builds that take longer each quarter, deploys that need a specific person, and a staging environment that no longer resembles production.
Designing the pipeline deliberately turns it from an incident source into infrastructure. This page describes how we approach CI/CD pipeline design and implementation as an engineering exercise, starting from the release you want and working back to the stages that prove it is safe.
What a well-designed pipeline looks like
A good pipeline has a small number of properties, and each one can be checked.
- One path to production. Every change goes through the same stages. No side doors, no “just this once” manual deploys.
- Build once, promote many. The artefact tested in staging is the artefact that reaches production. Rebuilding per environment hides bugs.
- Fast feedback first. Cheap checks (formatting, linting, unit tests) run before expensive ones, so most failures surface in minutes.
- Pipeline as code. Definitions live in the repository, change through pull requests and are reviewable.
- Observable. You can see build duration, failure rate and queue time without asking anyone.
- Reversible. A deploy has a defined way back, covered in our automated rollback work.
For a broader catalogue of practices, our guide to CI/CD pipeline best practices covers the wider landscape. This page concentrates on how the design and build is actually run.
How we design the pipeline
Start from the release path, not the tool
We begin by tracing one real change from commit to production. We ask who approves it, which environments it passes through, what data or secrets each step needs, and where it has failed before. The answers shape the design more than any tool preference does.
The output is a short design document with a diagram of stages, a list of gates and a decision on artefact storage. You review and approve it before implementation starts. This costs a few days and prevents the most expensive kind of rework: building the wrong pipeline well.
Typical stage layout
A common shape for a containerised service looks like this:
| Stage | Purpose | Runs on |
|---|---|---|
| Validate | Lint, format, type check, secret scan | Every push |
| Test | Unit tests, then integration tests | Every push |
| Build | Container image or package, tagged with the commit SHA | Pull request and main |
| Scan | Dependency and image vulnerability scan | Pull request and main |
| Publish | Push artefact to a registry | Main only |
| Deploy to staging | Automatic, with smoke tests | Main only |
| Deploy to production | Gated by approval | Main only, after staging |
Not every team needs every stage. A static site does not need an image scan. A regulated product may need extra evidence stages. The table is a starting point for the conversation.
A concrete skeleton
Here is the kind of thin end-to-end workflow we build first, in GitHub Actions:
name: ci
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t app:${{ github.sha }} .
Three details here are deliberate. concurrency cancels superseded runs, which saves compute and queue time. permissions starts at read-only and is widened only per job. timeout-minutes stops a hung job from consuming a runner for hours. The equivalent in GitLab CI uses stages, rules: and needs:, and in Jenkins a declarative Jenkinsfile.
Choosing between GitHub Actions, GitLab CI and Jenkins
The honest answer is usually “the one you already have”.
GitHub Actions is the natural choice if your code is on GitHub: tight pull request integration, a large action marketplace, and hosted runners with no servers to run. The marketplace also means third-party code in your pipeline, which needs pinning and review.
GitLab CI shines when you want source control, registry, CI and security scanning in one product, and its include and component model suits shared templates.
Jenkins still makes sense with heavy custom plugins, unusual build hardware or strict on-premises rules. The price is upkeep: controller upgrades, plugin compatibility and agent management. If that price is already hurting, see our Jenkins migration path.
We do not push a platform change as part of a design project unless the current tool is the actual blocker.
When not to build a new pipeline
A rewrite is not always the answer. If your pipeline is slow only because of one uncached step, fix that step. If it is flaky because of three bad tests, fix the tests. A full redesign is justified when releases depend on a person, when environments have drifted apart, when security review keeps failing on credentials, or when you are consolidating after an acquisition. If you are unsure, our DevOps maturity assessment is a quick way to see where the biggest gaps are.
Common failure modes we design around
- The big bang cutover. Switching everything on one day. We migrate service by service with a parallel run.
- Pipelines only one person understands. We use shared templates and written decisions, so knowledge sits in the repository.
- Environment drift. Staging that differs from production gives false confidence. We keep configuration differences explicit and small.
- Over-gating. Ten approvals on every pull request teaches people to route around the pipeline. We put heavy gates on production and light ones on feature branches.
- Treating the pipeline as finished. Actions, plugins and base images age. Renovate or Dependabot keeps them current in small, reviewable changes.
Delivery and handover
Implementation is staged. The skeleton for one service ships first, so your team can react to something real rather than a diagram. We then widen coverage, add security stages and move services across in order of risk.
At handover you receive the pipeline code in your repositories, a short runbook covering how to add a service, debug a failure and roll back, and a walkthrough with your engineers. We also agree who owns the pipeline afterwards. If you want ongoing care, that is the role of our managed service, but nothing about the design requires it.
How to tell the new pipeline is working
Measure the same things before and after: median pull request check time, failure rate excluding genuine test failures, time from merge to production, and the number of manual steps in a release. Our approach to comparing them is described in build time benchmarks. If those move in the right direction and engineers stop mentioning the pipeline in retrospectives, the design is doing its job.
To talk through your current release path, book a consultation.