Back to all articles
Feature image for CI/CD Pipelines: From Commit to Production Without the Midnight Deploy

CI/CD Pipelines: From Commit to Production Without the Midnight Deploy

CI/CD pipelines automate the path from a code commit to a running release. Here's what continuous integration and delivery actually mean, the gates a real pipeline runs, lint, test, build, publish, deploy and a walkthrough of the exact GitHub Actions → ECR → ArgoCD flow that ships this site, plus how to start small without over-building it.

Shipping software should be boring. Not the building part — the shipping part. When a merge to main quietly becomes a running production release without anyone SSHing into a box at 11pm, you have a CI/CD pipeline doing its job. This is a practical walk through what CI/CD actually is, the stages a real pipeline runs, and a look at the exact pipeline behind this site.

What is a CI/CD pipeline, really?

A CI/CD pipeline is an automated path that takes a code change from a commit to a deployed release without manual steps in between. It splits into two halves:

  • CI (Continuous Integration) — every change is merged, built, and tested automatically, so integration problems surface in minutes instead of at release time. Martin Fowler's canonical write-up on Continuous Integration is still the clearest definition of the practice.
  • CD (Continuous Delivery/Deployment) — a change that passes CI is automatically packaged and rolled out. Delivery stops at a deployable artifact awaiting a click; deployment goes all the way to production on its own (Fowler draws the same delivery-vs-deployment line).

The payoff is small, frequent, low-risk releases instead of big, scary ones. Google's long-running DORA research consistently finds that teams deploying more often and recovering faster outperform teams that batch changes into rare, large releases — speed and stability move together, not against each other.

The stages of a typical pipeline

Most pipelines are a sequence of gates. A change only moves forward if the stage before it passed:

  1. Source — a push or pull request triggers the run.
  2. Lint & static analysis — style and security checks (for this repo: RuboCop and Brakeman).
  3. Test — the automated suite runs against a clean environment.
  4. Build — the app is compiled into an immutable artifact, usually a container image.
  5. Publish — the artifact is pushed to a registry, tagged by version.
  6. Deploy — the new artifact is rolled out to an environment.
The six stages of a CI/CD pipeline A left-to-right sequence of gates: Source, then Lint, Test, Build, Publish, and Deploy. Each gate must pass before the next begins. Each gate must pass before the next runs Source Lint Test Build Publish Deploy
A change only advances when the previous gate passes.

A change only advances when the previous gate passes.

A real pipeline: how this site ships

Theory is cheap, so here is the actual flow behind this API. It runs on GitHub Actions and splits work by trigger:

  • On every pull request — lint, test, and a build (the image is built but not pushed). This proves the change is releasable before it ever merges.
  • On every push to main — lint and test run again on the merged result.
  • On a version tag (v1.2.3) — the release build kicks in: it builds a linux/amd64 image, tags it, and pushes it to Amazon ECR.

Cutting a release is deliberately a single, boring command:

git tag v1.0.0 && git push origin v1.0.0

From there, Argo CD takes over. It watches a separate Kubernetes manifests repo, notices the new image tag, and reconciles the cluster to match — a pull-based model called GitOps, where the desired state lives in Git and the cluster continuously converges on it. Nobody runs kubectl apply by hand.

The release flow that ships this site A version tag triggers GitHub Actions to build an image, which is pushed to Amazon ECR. Argo CD detects the new tag and reconciles the Kubernetes cluster. Pull requests and pushes to main run lint and test only. A tag — not a merge — triggers a release git tagv1.2.3 GitHubActions AmazonECR Argo CD(GitOps) Kubernetescluster
Pull requests and pushes to main run lint + test only; the build-and-push path fires solely on a version tag.

Why split the pipeline by trigger?

Building on PRs catches breakage early and cheaply. Pushing images only on tags keeps the registry clean and makes releases intentional: a merge is not a release, a tag is. That separation is what lets the whole thing stay boring.

What a good pipeline actually gives you

  • Fast feedback — you find out a change is broken in minutes, in a PR, not in production.
  • Repeatability — the same steps run the same way every time; no "works on my machine".
  • Immutable artifacts — the exact image tested is the exact image deployed.
  • An audit trail — every release maps to a tag, a commit, and a green pipeline run.

Getting started without over-building it

You do not need Kubernetes and GitOps on day one. Start with the smallest pipeline that removes manual work:

  1. Run your tests automatically on every pull request. That alone is most of the value of CI.
  2. Add lint and a security scan as separate, non-blocking-then-blocking gates.
  3. Build a container image so your artifact is reproducible.
  4. Automate the deploy last — once you trust the gates above it.

Add complexity only when a real pain point demands it. A pipeline that runs tests on every PR and deploys on a tag will carry a project a very long way before it needs anything fancier.

The bottom line

CI/CD is not a tool you buy — it is the discipline of automating the path from commit to production so that shipping stops being an event. Get the gates right, keep artifacts immutable, and let a version tag do the work an on-call engineer used to do at midnight.

Need infrastructure work like this done right? See what I do or look at recent builds.

Related articles