24.2. Continuous Integration
Table of Contents
Why Continuous Integration Matters
Continuous Integration, often shortened to CI, is a practice where developers frequently merge their code changes into a shared repository and automatically verify that the changes do not break the project.
Instead of waiting days or weeks to integrate features, CI encourages you to integrate many small changes multiple times per day. Each integration triggers automated checks, such as running tests and linters.
In a backend context, CI becomes the safety net every time you push code to your API, database layer, background workers, or infrastructure scripts.
Key idea: CI is about frequent code integration combined with automatic checks that run on every change.
A simple timeline comparison helps:
| Approach | Integration Frequency | Manual Work | Risk of Breakage | Feedback Speed |
|---|---|---|---|---|
| No CI | Rare (big batches) | High | Very high | Slow |
| Basic CI (few tests) | Often (small batches) | Medium | Lower | Faster |
| Mature CI (full pipeline) | Very often | Low | Much lower | Very fast |
Backend projects become easier to maintain when you can trust an automated system to tell you quickly if something is broken.
Core Concepts of Continuous Integration
CI is not a single tool or service. It is a process built around several ideas that work together.
Shared main branch
A typical backend repository has one main integration branch, often called main or master. All new code, after being reviewed and tested, ends up here.
Developers create feature branches like:
feature/add-user-notificationsbugfix/fix-order-totalchore/update-dependencies
They push these branches, open pull requests (or merge requests), and use CI to verify that the branch is safe to merge.
Automated builds
A build step prepares your application for running:
- Installing dependencies
- Compiling or bundling where necessary
- Generating artifacts (for example a Docker image)
In a Python backend, the build step can be as simple as:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtThe CI system runs this automatically on every push or pull request so you do not have to do it manually.
Automated tests
Tests give you confidence that existing behavior still works. CI runs them every time you push.
Common test types for a backend:
| Test Type | Checks | Example |
|---|---|---|
| Unit tests | Individual functions or methods | Email validation function returns correct result |
| Integration | Components working together | API endpoint reads from PostgreSQL correctly |
| API tests | HTTP endpoints and responses | /api/orders returns 201 and correct JSON |
| Smoke tests | Very basic health checks | Service starts and health check endpoint works |
Running tests locally is good. Running them in CI is better, because every change is checked in a clean environment that resembles production.
Rule: Every push to a shared branch must trigger tests. If tests fail, do not merge.
Fast, frequent feedback
CI works only if feedback is fast.
- If a test suite takes 45 minutes to run, developers will stop caring about it.
- If it takes 3 minutes, developers are willing to wait and fix issues immediately.
For backend projects, aim for:
- A quick pipeline for every push that finishes in a few minutes.
- Heavier tests (for example full integration with many services) that may run less frequently, for example nightly.
Single source of truth
The CI pipeline is the trusted definition of how your project is built and verified.
This avoids the situation where:
- Developer A runs tests one way on their laptop.
- Developer B runs a different set of commands.
- Production is deployed in yet another way.
By encoding commands into CI config files (YAML or similar), you make the process explicit, repeatable, and reviewable.
Typical Continuous Integration Workflow
To understand CI, walk through a concrete developer story on a backend project.
Local work
- Pull the latest
mainbranch. - Create a new feature branch, for example
feature/add-task-search. - Implement new functionality.
- Run tests locally, at least for the parts you touched.
- Commit your changes.
Push and create a pull request
You push your branch:
git push origin feature/add-task-search
Then you open a pull request into main on GitHub, GitLab, or another platform.
At that moment, CI typically:
- Starts a new pipeline.
- Shows its status directly on the pull request, usually as a green check or a red cross.
CI pipeline stages
Most CI systems define stages that run in order.
A simple backend CI pipeline might have:
- Lint
- Run static analysis tools, for example
flake8,black --check,mypy. - Test
- Run unit tests and small integration tests, for example
pytest. - Build
- Build a Docker image or other deployment artifact.
Example of a simple logical flow:
| Stage | Example Commands | Purpose |
|---|---|---|
| lint | pip install -r dev-requirements.txt<br>flake8 . | Catch style or simple code issues |
| test | pytest -q | Ensure behavior is correct |
| build | docker build -t my-api:pr-123 . | Ensure code is buildable |
If any stage fails, the pipeline stops and marks the pull request as failing.
Fix and repeat
If CI fails:
- You inspect the logs in the CI interface.
- You fix the problem locally.
- You commit and push again.
- CI re-runs automatically on the new commit.
Once all stages pass and code review is complete, the branch merges into main.
Continuous Integration vs Continuous Delivery vs Continuous Deployment
These three concepts are related but not the same.
| Concept | Main Focus | Automation Level |
|---|---|---|
| Continuous Integration | Integrate and test code frequently | Build + tests on every change |
| Continuous Delivery | Keep code always deployable | CI + automatic packaging + manual deploy trigger |
| Continuous Deployment | Deploy automatically on every successful build | CI + CD + auto deploy to production |
In this chapter you focus only on Continuous Integration, that is build and test automation. Deployment automation is handled in separate chapters on Continuous Delivery, Continuous Deployment, and CI/CD pipelines.
What CI Does for Backend Projects
Backend applications often have more moving parts than small frontend apps. CI helps keep these parts consistent and working.
Consistent environments
Instead of each developer having a slightly different environment on their laptop, CI runs:
- On a known base image or virtual machine.
- With defined versions of Python, PostgreSQL, Redis, and other tools.
- With clear setup steps, for example installing dependencies and environment variables.
This reduces the classic line:
"It works on my machine."
If it does not work in CI, you treat that as the truth.
Catching integration bugs early
Even if your code works alone, integration with:
- Existing modules,
- The database schema,
- Authentication components,
- Background jobs,
can introduce subtle bugs.
By integrating small changes frequently, you discover integration problems while they are still easy to fix.
Enforcing quality standards
CI can run checks that are easy to forget:
- Code formatting (for example
black). - Static type checks (for example
mypy). - Security checks (for example dependency scanners).
- Linting (for example
flake8orpylint).
You do not have to remind team members to run these manually. The CI system enforces them consistently.
Simple Examples of CI Configuration
Concrete examples help make this less abstract. Imagine a basic Python/FastAPI backend.
Example: GitHub Actions CI for a Python backend
A minimal .github/workflows/ci.yml:
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r requirements.txt
pip install -r dev-requirements.txt
- name: Run linters
run: |
source .venv/bin/activate
flake8 .
black --check .
mypy .
- name: Run tests
env:
DATABASE_URL: "postgresql://test:test@localhost:5432/testdb"
run: |
source .venv/bin/activate
pytest -qKey observations:
- The job runs on
pushandpull_requesttomain. - It checks out the code.
- It sets up Python.
- It installs dependencies.
- It runs linting and tests.
You do not need to memorize GitHub Actions syntax in this chapter. Instead, notice how the CI file becomes the script that defines your integration process.
Example: GitLab CI for a backend project
A simple .gitlab-ci.yml:
stages:
- lint
- test
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
lint:
stage: lint
image: python:3.11
script:
- pip install -r dev-requirements.txt
- flake8 .
- black --check .
- mypy .
artifacts:
when: always
paths:
- flake8.log
expire_in: 1 week
test:
stage: test
image: python:3.11
services:
- name: postgres:15
alias: db
variables:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
DATABASE_URL: "postgresql://test:test@db:5432/testdb"
script:
- pip install -r requirements.txt
- pip install -r dev-requirements.txt
- pytest -qHere:
lintandtestare stages.- Tests run against a real PostgreSQL service that CI spins up.
Again, the idea is: define how to integrate and test your backend using configuration, not manual steps.
Good Practices for Continuous Integration
Even a simple CI setup benefits from some basic principles.
Keep pipelines fast
A quick pipeline encourages frequent integration.
Ideas to keep it fast:
- Run unit tests on every push.
- Run heavier tests, for example very slow integration or load tests, on a schedule such as nightly.
- Cache dependencies when the CI system supports it.
For example, caching pip packages:
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-Fail fast
Configure CI to fail as soon as a serious problem is detected.
For example:
- If dependencies cannot be installed, stop.
- If tests fail, stop.
- If linting fails, you do not need to run tests for that commit.
This saves compute time and gives feedback faster.
Treat warnings seriously
If tests pass but CI shows warnings, do not ignore them.
Warnings can indicate:
- Deprecated functions that will break later.
- Security issues in dependencies.
- Flaky tests that sometimes fail and sometimes pass.
Fixing them early keeps your backend more stable.
Run CI on every branch that might be merged
Running CI only on main is too late. A branch could break the build and block everyone.
Configure CI to run on:
- Pull requests into main.
- Possibly feature branches when pushed.
This makes sure you never merge unverified code.
Rule: Do not merge a pull request if the CI status is failing or missing.
Simple CI Checklist for Beginners
When you start a new backend project, you can use this short list.
| Step | Question to Ask |
|---|---|
| Define main branch | What is the integration branch (for example main)? |
| Choose a CI platform | GitHub Actions, GitLab CI, or others? |
| Trigger conditions | Should CI run on push, pull request, or both? |
| Install dependencies | How will Python and packages be installed? |
| Run tests | What pytest command should CI run? |
| Run quality checks | Which linters and formatters will you use? |
| Pipeline speed | Does it finish in a few minutes? |
If you can answer these questions, you have the basics of Continuous Integration in place for your backend project.
Views: 9
KAHIBARO