How to Set Up CI/CD Pipelines People Don't Learn to Ignore

    5 min read3 approaches compared

    A CI pipeline has exactly one job: make the team confident enough to deploy. It fails at that job in two ways — being so slow that people stop waiting, or so flaky that a red build means nothing and everyone re-runs it on reflex.

    Both failures are worse than having no CI at all, because they cost time while providing false assurance. Most of the work in a good pipeline is keeping it fast and trustworthy, not adding more steps to it.

    Fast and trustworthy beats comprehensive

    A ten-minute pipeline everyone waits for catches more bugs than a forty-minute one people merge past. Optimize ruthlessly for the feedback loop.

    • Target under ten minutes for the PR pipeline. Past that, people context-switch and the feedback arrives after they've moved on.
    • Zero tolerance for flaky tests. Quarantine a flaky test the day it's spotted — one is enough to teach everyone that red doesn't mean broken.
    • Cache dependencies, and verify the cache is hitting. A misconfigured cache key silently misses every run and nobody notices.
    • Run fast checks first and fail early. Lint and typecheck in 30 seconds beats discovering a syntax error after an eight-minute test suite.
    • Never expose production secrets to CI running untrusted pull requests.

    The approaches

    GitHub Actions

    // best for: Most teams on GitHub. It's where the ecosystem is.

    • GitHub Actions
    • Git
    • Docker
    • Node.js
    • Python

    Config lives in the repo, runs on push, and has a marketplace action for essentially everything. The generous free tier for public repositories has made it the default.

    The two things worth learning properly are caching and matrix builds. `actions/cache` keyed on a lockfile hash turns a two-minute install into a five-second restore, and it's the single biggest speedup available. Matrix builds run combinations in parallel — several Node versions, or splitting a test suite into shards that run concurrently and cut wall-clock time proportionally.

    Concurrency groups are the underused feature: cancelling superseded runs on the same branch stops five stale builds queuing while someone pushes fixes, which is where a lot of runner time quietly goes.

    yaml
    # Cancel superseded runs so a branch with five quick pushes
    # doesn't queue five full pipelines.
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true
    
    jobs:
      test:
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: npm          # keyed on the lockfile automatically
          - run: npm ci           # ci, not install: fails on lockfile drift
          - run: npm run typecheck # fast checks first
          - run: npm test

    The gotcha

    `pull_request_target` runs with full repository secrets and, unlike `pull_request`, is triggered by forks. Combining it with a checkout of the PR's head means arbitrary code from a stranger's fork runs with access to your production secrets. It is the most exploited misconfiguration in GitHub Actions.

    Platform-native pipelines (Vercel, Netlify, Railway, Render)

    // best for: Frontend and full-stack apps already deployed on that platform.

    • Vercel
    • Netlify
    • Railway
    • Render
    • Next.js

    The platform watches your repository, builds on push, and gives every pull request a preview URL. There is essentially no configuration, and the build cache is tuned for the framework you're using.

    For a lot of projects this is the whole pipeline, and adding a separate CI system on top duplicates the build for no benefit. The usual sensible split is: platform handles build and deploy, a lightweight Actions workflow runs tests and linting in parallel.

    Where it runs out is anything beyond build-and-deploy. Integration tests needing a database, scheduled jobs, or multi-repository coordination all want a general-purpose runner.

    The gotcha

    Platform build caches are aggressive and occasionally serve stale artifacts after a dependency change, producing a build that succeeds with code that no longer exists. When a deploy behaves impossibly, clear the build cache before debugging anything else.

    Self-hosted runners and alternative CI

    // best for: Large test suites, specialized hardware, or workloads that need VPC access.

    • GitLab CI
    • Docker
    • Kubernetes
    • AWS
    • Terraform

    Self-hosted runners make sense for three reasons: cost at high volume, machine specs the hosted tiers don't offer, and network access to private infrastructure for integration tests against a real database in your VPC.

    GitLab CI deserves mention on its own merits — its pipeline model with stages, DAG dependencies, and built-in container registry is coherent in a way that Actions' YAML sometimes isn't, particularly for complex multi-stage builds.

    The cost is that runners are now infrastructure you maintain: patched, monitored, and scaled. Autoscaling runners on spot instances gets the economics right but is a project.

    The gotcha

    Self-hosted runners reuse their filesystem between jobs by default, so state leaks across runs — a file written by one job is visible to the next, and a passing build may depend on residue from an earlier one. Run jobs in ephemeral containers, or you'll eventually chase a failure that only reproduces on one runner.

    Common pitfalls

    Tolerating flaky tests

    One test that fails 5% of the time teaches the whole team that red builds are noise, and from then on real failures get re-run instead of investigated. Quarantine flaky tests immediately and fix them as their own work item. The credibility of the pipeline is the asset.

    Secrets available to fork pull requests

    Any workflow that gives secrets to code from an untrusted fork is an exfiltration path. Use `pull_request` for untrusted contributions, require approval before running workflows on first-time contributors, and scope secrets to the jobs that need them.

    Caches that never hit

    A cache key including a timestamp or an unstable value misses every run while looking configured. Check the cache-hit line in your logs — a lot of pipelines are paying full install cost on every run without anyone realizing.

    No branch protection

    A pipeline nobody is required to pass is a suggestion. Require the checks on the default branch, or the one time someone merges past a red build will be the time it mattered.

    Questions people actually ask

    How fast should CI be?

    Under ten minutes for the pull request pipeline. Beyond that, developers switch tasks and the feedback lands after they've lost context. Parallelize by sharding tests and run the fast checks first.

    Should deploys be automatic on merge to main?

    Yes, once you can roll back in under a minute and have monitoring that would tell you something broke. Continuous deployment is safer than batching, because small changes are easier to attribute when something goes wrong.

    Do I need a separate CI system if my host already builds?

    Only for things the host doesn't do: integration tests against a real database, scheduled jobs, cross-repo coordination. Running the same build twice adds cost and no signal.

    Need someone who's done this before?

    Browse vetted developers who work with these tools day to day.

    Related guides