Pipeline as Code

A pipeline is only trustworthy if it's defined the same way your application is: as version-controlled code, reviewed and reproducible. "Pipeline as code" turns the path to production from clicked-together settings in a web UI into a file in your repository — and that shift, from configuration to code, brings the whole discipline of software engineering to bear on how you ship software.

We’ve covered what flows through a pipeline; this post is about how the pipeline itself is defined. Modern CI/CD systems — GitHub Actions, GitLab CI, and others — express pipelines as declarative files committed alongside your code. Understanding this model, and the principles for doing it well, is what separates a maintainable delivery system from a fragile one.

From clicked config to committed code

Early CI servers were configured by clicking through a web UI — defining build steps in forms, storing them in the server’s database. That worked until it didn’t: the configuration wasn’t versioned, wasn’t reviewable, wasn’t reproducible, and lived only in one server’s state. Nobody could see why the pipeline changed, or restore a previous version, or stand up an identical pipeline elsewhere.

Pipeline as code fixes this by defining the pipeline in a file in the repository.github/workflows/*.yml, .gitlab-ci.yml, a Jenkinsfile. The consequences are the same benefits version control brings to any code: - Versioned — the pipeline’s history is in git; you can see every change and who made it, and roll back a bad pipeline change like any other. - Reviewed — pipeline changes go through pull requests and code review, just like application changes. - Reproducible — the pipeline is fully described by a file, so it’s identical everywhere and can be recreated from scratch. - Co-located and co-evolving — the pipeline lives beside the code it builds and changes with it in the same commit, so a change that needs a new build step ships its pipeline update atomically.

This is the same “as code” shift that Infrastructure as Code brought to servers: replace clicked-together, un-auditable state with declarative, version-controlled definitions. It’s foundational to everything else.

The declarative model: workflows, jobs, steps

Most pipeline systems share a structure worth knowing generically (names vary by tool):

A minimal GitHub Actions example shows the shape:

name: CI
on: [push, pull_request]        # event triggers
jobs:
  test:                          # a job
    runs-on: ubuntu-latest
    steps:                       # sequential steps
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm test

The model is declarative: you describe what the pipeline consists of (triggers, jobs, steps, dependencies) and the system figures out how to schedule and run it — parallelizing independent jobs, ordering dependent ones. You declare the graph; the runner executes it. This declarative nature is what makes pipelines readable and reproducible.

Principles for maintainable pipelines

A pipeline is code, so treat it like code — the same practices that keep applications maintainable apply:

The meta-principle: everything you’d do to keep application code healthy — DRY, small units, reviews, versioning, readability — applies to pipeline code, because it is code now.

Runners, environments, and matrix builds

Two more concepts round out the model:

Together, the pipeline-as-code model gives you a versioned, reviewed, reproducible definition of your entire path to production — declarative, maintainable, and evolving in lockstep with your application. It’s the substrate the security and operations concerns of the next posts build on: you can’t secure or reliably operate a pipeline that only exists as clicks in a web UI.

Key takeaways

Further reading

Sources & References

Declarative workflows, jobs, steps, matrix builds