CI/CD Pipelines: Best Practices for Modern Software Delivery

CI/CD Pipelines: Best Practices for Modern Software Delivery

If you’ve been in software long enough, you remember the days of manual deployments—someone SSHing into a production server at 2am, fingers crossed, typing git pull && systemctl restart app. Those days aren’t entirely gone (some of you know who you are), but CI/CD pipelines have fundamentally changed how software gets from a developer’s brain to a running process.

But here’s the thing: most teams implement CI/CD and then call it done. They set up a basic pipeline that runs tests and maybe does a deploy, and they think they’re done. They’re not. A pipeline that just exists isn’t a good pipeline.

What CI/CD Actually Means (and Why People Get It Wrong)

Continuous Integration means code is merged frequently into a shared branch, and every merge triggers an automated build and test. The key word is frequently. If your developers are working on feature branches for two weeks before merging, you don’t have CI—you have scheduled pain.

Continuous Delivery means your software is always in a deployable state. Every passing pipeline produces an artifact that could be deployed to production. Whether it is deployed is a human decision.

Continuous Deployment takes it further—every passing pipeline automatically deploys to production. No human gate.

Most teams say they do CI/CD. Most teams actually do “merge occasionally, run some tests, manually deploy when someone remembers.”

Pipeline Stages That Matter

A well-structured pipeline has distinct stages with clear purposes:

stages:
  - validate
  - build
  - test-unit
  - test-integration
  - security-scan
  - publish
  - deploy-staging
  - test-e2e
  - deploy-production

Validate Early, Validate Fast

Your first stage should be the fastest possible sanity check. Lint your code. Check your YAML syntax. Validate your Dockerfiles. Fail in 30 seconds rather than wasting 10 minutes on a build that was always going to fail.

# GitHub Actions example
validate:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Lint
      run: |
        npm run lint
        yamllint .
        hadolint Dockerfile

Don’t Skip Security Scanning

Security scanning belongs in the pipeline, not as an afterthought. Tools like Trivy, Snyk, and SonarQube should be non-negotiable. Fail the pipeline on critical CVEs. Your security team will thank you; your developers will grumble and then patch their dependencies.

# Trivy scan in CI
trivy image --exit-code 1 --severity CRITICAL myapp:latest

Artifact Versioning Is Non-Negotiable

Every artifact your pipeline produces should be immutably versioned. Use semantic versioning tied to your git tags, or use commit SHA-based tags. Never, ever push to :latest as your only tag. When something breaks in production and you need to roll back, you need to know exactly what you’re rolling back to.

myapp:1.4.2
myapp:1.4.2-abc1234

Not:

myapp:latest  # the tag of shame

Pipeline Performance: The Forgotten Discipline

Slow pipelines get ignored. If your pipeline takes 45 minutes to run, developers will stop waiting for it and merge anyway. Aim for:

  • Unit tests: under 5 minutes
  • Full pipeline including integration: under 15 minutes
  • End-to-end: under 30 minutes (and run these less frequently)

Techniques to speed things up:

Parallelism: Run independent stages concurrently. Your lint check and unit tests can run at the same time.

Caching: Cache your dependency downloads. In a team running 50 pipelines a day, downloading npm packages from scratch each time is pure waste.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

Test splitting: Split your test suite across multiple runners. Don’t run 500 tests sequentially when you can run 5 groups of 100 in parallel.

The Branching Strategy Conversation Nobody Wants to Have

Your CI/CD strategy is inseparable from your branching strategy. Trunk-based development with feature flags is the approach that actually scales. Long-lived feature branches are how you get merge conflicts that take a full day to untangle and pipelines that become irrelevant because the branch has drifted so far from main.

Trunk-based development feels scary. Merging small, incomplete changes to main every day feels dangerous. That’s what feature flags are for. A change can land in main, hidden behind a flag, tested continuously, and enabled when it’s ready.

Environments and Promotion

Your pipeline should enforce an environment promotion model:

dev → staging → production

Code doesn’t skip environments. Staging should be as close to production as possible—same infrastructure, same secrets structure (different values), same configuration. The classic “it worked in staging” excuse is often because staging didn’t actually match production.

What “Done” Actually Looks Like

A mature CI/CD pipeline:

  1. Runs on every commit to every branch
  2. Gives developers feedback in under 5 minutes for basic checks
  3. Produces immutably versioned, security-scanned artifacts
  4. Automatically deploys to non-production environments
  5. Has rollback automation, not just rollback documentation
  6. Tracks metrics: pipeline duration, failure rate, deployment frequency, mean time to recovery (MTTR)

If you’re not measuring your pipeline, you’re not improving it. Deployment frequency and MTTR are the two metrics that will tell you more about your engineering organization than almost anything else.

Closing Thoughts

CI/CD isn’t a tool you install—it’s a discipline you build. The tooling (GitHub Actions, Jenkins, GitLab CI, Tekton, whatever) is secondary. The culture of shipping small changes frequently, catching problems early, and making deployments boring is what matters.

Make deployments boring. That’s the goal.

Share this post: LinkedIn Reddit WhatsApp Mastodon
Jesse Borden

Jesse Borden

Software Engineer with an interest in hands on learning

I have several years of professional Information Technology (IT) experience leading staff and projects within the Department of War (DOW). I have managed Service Desk, Web Application Development, and System Administration teams. My two greatest passions are learning and conti...