← Back to the section

Shipping a new version used to look like this: a developer finished the code, handed it off to a "release team", and they built, tested, and — days or weeks later — deployed it to the server by hand. If something went wrong, troubleshooting took a long time, and mistakes got repeated.

CI/CD is an automated pipeline in which every commit follows the exact same path from change to production. A human makes decisions in this process ("do we merge?", "do we ship?") rather than performing each step by hand.

What a pipeline is made of

A pipeline is a chain of stages. Each stage either lets the change move forward or stops it. There is no partially-green result: either it is good, or it is a stop.

commit → build → fast checks → slow checks → artifact → staging → verify → production
         build    unit, lint     integration    image      auto      smoke

Every team names its stages differently, but the logic is the same: the more time a stage costs, the later it comes in the chain.

The artifact is built once

The most common mistake is rebuilding the code from scratch for every environment. "This build is for staging; for production we'll build it again, properly this time."

The problem: a rebuild can pull in different dependency versions and a different compilation context. As a result, the artifact that goes to production is not the one that was tested.

The correct approach is called build once, deploy many: the image is built once per commit and tagged with its hash (SHA). That exact image goes to both staging and production. The differences between environments live only in the configuration on the outside (environment variables, secrets, database addresses), never in the artifact itself.

Fast before slow

Imagine that compilation and basic tests take 2 minutes, while integration tests take 30. If you run everything together, a compilation error reaches the developer after 32 minutes — when they could have known in 2.

The fail fast principle: the fastest checks come first.

  • Compilation and unit tests — minutes. They run immediately.
  • Integration tests — tens of minutes. Only if the fast ones passed.
  • Deploy to staging and smoke tests — after the integration tests.

There is also a practical limit: if the pipeline up to "ready to deploy" takes more than 15 minutes, developers start pushing changes in batches to avoid waiting. The feedback loop breaks, and the point of CI is lost. Pipeline time is a metric just like a service's response time.

The pipeline is described in code

The bad option: the pipeline is configured by clicking around in the CI system's web interface. Nobody remembers exactly how it is set up. You cannot roll it back, it is hard to reproduce on another machine, and it is impossible to review in a pull request.

The good option is pipeline as code: the pipeline is described by a file in the repository, right next to the code. In GitHub Actions this is .github/workflows/, in GitLab it is .gitlab-ci.yml, in Jenkins it is Jenkinsfile. This file is versioned, reviewed, and rolled back just like any other code.

A green main is the foundation of teamwork

If main is broken, nobody on the team can build or deploy. Work grinds to a halt.

That leads to a few rules:

  • Direct pushes to main are blocked. Only through a pull request.
  • A PR is merged only when the pipeline is green.
  • If main does break — fix it immediately and with top priority. Reverting the broken commit (revert) is faster than investigating. The investigation can wait.

A team whose main is red for weeks effectively has no CI: it has a server that nicely shows that everything is broken.

Deploy and release are different things

Many people are afraid to deploy often because deploy means "users see the new thing right away". If something breaks, that is scary.

But deploy and release can be separated:

  • Deploy — a technical step: the new code has landed on the server.
  • Release — a product step: the feature has become available to users.

The tool for this separation is called feature flags: the new code goes to production turned off, is turned on by a separate command, and turned off without a code rollback.

This removes the main fear: you can deploy often and calmly. You release when a feature is ready from the product's point of view, not when you have "worked up the nerve".

Environments should be identical

Staging (the test environment) exists precisely to verify what is going to production. If staging is set up differently, it verifies something else.

Common reasons for divergence:

  • Docker on staging, hand-run on the server in production.
  • Different versions of databases or middleware.
  • Staging configuration that is "roughly like production, but done by hand".

The ideal: both environments are stood up from the same declarations (configuration files), differing only in configuration values (addresses, secrets, data volumes). Such an environment can be recreated from scratch — and that means it can be trusted.

The pipeline is also a system that is maintained

The pipeline is not a background process that nobody thinks about. It has:

  • Availability: if CI is down, the team is stuck.
  • Secrets: tokens and keys are stored properly, not in plain text.
  • Execution time: a slowing pipeline is a signal that something has gone wrong.
  • Cost: long parallel pipelines cost money.

The pipeline is monitored, and its failures are investigated the same way as production incidents.

Common mistakes

MistakeWhat happensThe right way
Rebuilding the artifact for productionProduction runs something other than what was testedBuild once, tag with SHA
A 40+ minute pipelineDevelopers push in batches, CI becomes a formality~15 min budget; parallelism, caches
Pipeline configured by clicking in the UIImpossible to reproduce and reviewA file in the repository
Red main "we'll fix it later"The team is blockedRevert immediately, investigate later
Release = deployFear of deploys, nightly release windowsFeature flags: deploy often, release deliberately
Staging "almost like production"You verify something other than what shipsIdentical declarations, different config
Manual steps between stages"Forgot to run the migration"The whole path is automated

In short

  • CI/CD is an automated pipeline: the same path from commit to production for every change.
  • Build once, deploy many: the artifact is built once and goes to both staging and production without a rebuild.
  • Fast checks come before slow ones — so an error is found as early as possible.
  • The pipeline is described by a file in the repository, reviewed and versioned together with the code.
  • A green main is the team's priority: a broken one is reverted immediately.
  • Deploy and release are separated: the new code goes to production turned off and is turned on separately via feature flags.
  • Staging and production are stood up from the same declarations — only the configuration differs.
  • The pipeline is monitored and maintained like any other system.

Further reading

  • CI for Java/Spring: build, tests, quality gates — the concrete pipeline steps for a Java project.
  • Release strategies — what happens after a deploy to production.
  • Branches and the release cycle — how the workflow that feeds the pipeline works.