Continuous Integration Explained: What a CI/CD Pipeline Is For
DevOpsLesson 1 of 45 min read
What you'll learn
- Explain continuous integration, continuous delivery and continuous deployment, and how they differ
- Describe what a pipeline is and what triggers and stages it typically has
- Write a local script that mimics a CI check and understand why exit codes drive pipelines
- Recognise the habits that make CI useful rather than ceremonial
On this page
Five developers work on a fare-calculation engine for a metro system. On Friday afternoon one of them tweaks an off-peak rate, merges, and goes home. Another has been working for three days on a branch that assumed the old rate. When the two collide on Monday, the build is red, two people blame each other, and nobody is sure which of the last twenty commits broke what. Continuous integration is the practice that makes this Monday impossible: merge small changes often, and have a machine check every one of them within minutes.
What continuous integration actually means
Continuous integration (CI) is the habit of merging changes into a shared main branch frequently (at least daily, ideally several times a day) and verifying every change automatically. The word integration matters: the risk it removes is not "my code has a bug" but "my code does not work together with everyone else's code". Long-lived branches hide that risk until the worst moment. Small frequent merges surface it while the cause is still obvious.
A pipeline is the automated sequence that performs the verification. A server (GitHub Actions, GitLab CI, Jenkins and others) is told to run it on certain triggers: a pull request opened or updated, a push to main, a tag, or a schedule. It starts a clean machine, checks out the code, and runs stages such as install, lint, test and build. Each step succeeds or fails, and the overall result is shown next to the commit or pull request.
trigger: pull request / push to main
|
v
[checkout] -> [install] -> [lint] -> [unit tests]
|
v
[build + package]
|
v
[store artifact / image]
|
(CD part) v
[deploy to staging]
|
v
[approve] -> [deploy to prod]CI, continuous delivery and continuous deployment
The abbreviation CI/CD hides three distinct ideas, and people routinely mix them up.
| Term | What is automated | What a human still decides |
|---|---|---|
| Continuous integration | Build and test every change on a shared branch | When to merge and what to release |
| Continuous delivery | Every passing change is packaged and ready to deploy at any time | Whether and when to press the production button |
| Continuous deployment | Every passing change goes to production automatically | Nothing per release; confidence comes from tests, flags and monitoring |
Most teams practise continuous delivery, and some move to continuous deployment for services where tests, monitoring and rollback are strong. Neither is better in the abstract. A regulated payments system may deliberately keep a human approval, while an internal dashboard might ship forty times a day.
A pipeline is just commands and exit codes
Before touching any CI product, build the core idea by hand. A pipeline step passes if its command exits with status 0 and fails otherwise, and the whole run stops at the first failure. That is all a CI server really relies on. Here is a tiny fare engine and a script that behaves like a miniature pipeline.
PEAK_RATE = 2.5
OFF_PEAK_RATE = 1.8
BASE = 10.0
def fare(distance_km: float, peak: bool, concession: bool = False) -> float:
rate = PEAK_RATE if peak else OFF_PEAK_RATE
total = BASE + distance_km * rate
if concession:
total *= 0.5
return round(total, 2)import unittest
from fare import fare
class FareTests(unittest.TestCase):
def test_off_peak_adult(self):
self.assertEqual(fare(10, peak=False), 28.0)
def test_peak_costs_more(self):
self.assertGreater(fare(10, peak=True), fare(10, peak=False))
def test_concession_is_half(self):
self.assertEqual(fare(10, peak=False, concession=True), 14.0)
if __name__ == "__main__":
unittest.main()#!/usr/bin/env bash
set -euo pipefail
echo "== syntax check =="
python -m py_compile fare.py test_fare.py
echo "== unit tests =="
python -m unittest -v
echo "== all checks passed =="Output
== syntax check == == unit tests == test_concession_is_half (test_fare.FareTests.test_concession_is_half) ... ok test_off_peak_adult (test_fare.FareTests.test_off_peak_adult) ... ok test_peak_costs_more (test_fare.FareTests.test_peak_costs_more) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK == all checks passed ==
set -euo pipefail makes Bash stop at the first failing command, which is the "fail fast" behaviour every CI system gives you. The run time shown will vary. Now imagine a teammate changes OFF_PEAK_RATE to 2.0 without updating the expectations. Running the same script produces a failure and a non-zero exit status, and a CI server would mark the pull request red:
== syntax check ==
== unit tests ==
test_concession_is_half ... FAIL
test_off_peak_adult ... FAIL
test_peak_costs_more ... ok
FAIL: test_off_peak_adult
File "test_fare.py", line 7, in test_off_peak_adult
self.assertEqual(fare(10, peak=False), 28.0)
AssertionError: 30.0 != 28.0
FAILED (failures=2)Either the tests need updating (the rate genuinely changed) or the change was a mistake. Either way a human is told within minutes, on the pull request, while the author still remembers why they did it. That is the whole value proposition. A hosted CI service adds a clean machine, parallelism, history and integration with your pull requests, but not a different idea.
Common mistakes and the symptoms they cause
- Long-lived branches. Symptom: merge conflicts in dozens of files and a "merge day" nobody wants. Fix: merge small, behind a feature flag if the feature is unfinished.
- A red main branch that people tolerate. Symptom: developers start ignoring failures, so real breakages hide among the known ones. Fix: treat a broken main as the top priority and revert quickly.
- Flaky tests. Symptom: the same commit passes on re-run, and people click retry without reading. Fix: quarantine and repair them; every ignored failure erodes trust.
- A pipeline that takes forty minutes. Symptom: developers batch changes to avoid the wait, which defeats the point. Fix: run quick checks first and parallelise; the fourth lesson covers this.
- Tests that only pass on one machine. Symptom: green locally, red in CI, with errors about missing files, environment variables or versions. Fix: pin versions and let CI be the clean-room reference.
How this shows up in real projects and interviews
In an interview you will be asked to "walk me through a CI pipeline for a web service". A good answer follows a change from pull request to production: triggers, fast checks first, tests, build once, store an artifact, deploy it somewhere safe, then promote. Be ready to distinguish delivery from deployment. On a real team, the unglamorous skill is reading a red build quickly: which step failed, is it my change, is it flaky, can I reproduce it locally. The next lesson turns this idea into a real workflow file.
Try it yourself
Write a check script for one of your own projects that runs a syntax or lint check and the test suite, and stops at the first failure. Introduce a deliberate bug, confirm the script exits with a non-zero status (echo $? right after it), then fix the bug and confirm it exits with 0.
Show a hint
Use set -euo pipefail at the top of a Bash script. On Windows PowerShell you can check $LASTEXITCODE instead of $?.
Key takeaways
- CI means merging small changes often and verifying each one automatically to find integration problems early.
- A pipeline is a triggered sequence of stages; each step passes or fails by its exit code.
- Continuous delivery keeps every change releasable; continuous deployment releases every passing change automatically.
- Keep pipeline logic in scripts you can run locally, with the CI file only orchestrating them.
- A trusted pipeline needs a green main branch and tests that are not flaky.
Test yourself
A trick from this lesson's ride: Integrate small and often: a clash found today has one suspect, found next week it has twenty. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is the difference between CI and CD?
CI (continuous integration) automatically builds and tests every change merged into a shared branch. CD stands for either continuous delivery, where every passing change is ready to release with a manual approval, or continuous deployment, where every passing change is released to production automatically.
What is a CI/CD pipeline?
It is an automated workflow that runs when a trigger such as a pull request or a push to main happens. It checks out code on a clean machine and runs stages like install, lint, test, build and deploy, stopping and reporting if any step fails.
Why is continuous integration important?
It finds integration problems while changes are small and the author remembers them, instead of days later when many changes have piled up. It also keeps the main branch in a releasable state and gives reviewers an objective signal that tests passed.