Build, test & Continuous Integration
Every commit is built, tested and proven — automatically
CI turns 'it works on my machine' into 'it provably works, every time'. Learn pipelines, a real test strategy, the major CI systems, and how artifacts are versioned and stored.
On this page · 6 lessons
CI/CD concepts
Pipelines, stages, jobs, runners, artifacts, caching, secrets and environments — the vocabulary of every CI system.
Continuous Integration (CI) means every commit is automatically built and tested, so integration problems surface in minutes, not at the end of a release. Continuous Delivery/Deployment (CD) extends that: every change that passes automatically becomes a release candidate (delivery) or goes straight to production (deployment).
A pipeline is the automation that runs on each push. It's made of stages (build → test → deploy) containing jobs that execute on runners (worker machines). Jobs produce artifacts (a compiled binary, a Docker image) that later stages consume.
Concepts that make pipelines fast and safe:
- Caching dependencies so builds don't reinstall everything each time.
- Secrets injected securely at run time — never hard-coded.
- Environments (dev, staging, prod) with approvals gating production.
For releasing without downtime, learn two patterns: blue/green (run two identical environments, switch traffic instantly) and canary (send 5% of traffic to the new version, watch the metrics, then ramp up).
Test strategy & quality gates
The test pyramid, what to run in CI, coverage as a signal not a target, and why flaky tests are a real bug.
CI is only as valuable as the tests it runs. A pipeline that runs no meaningful tests is just an expensive way to compile code.
The test pyramid tells you where to invest:
- Unit tests (the wide base) — test one function in isolation. Milliseconds, thousands of them. Catch most bugs, cheapest to maintain.
- Integration tests (the middle) — test components together: your app and a real database. Slower, fewer.
- End-to-end tests (the narrow top) — drive the whole system like a user. Slowest, most brittle — have a handful covering critical journeys only.
Invert this pyramid (mostly E2E) and your pipeline becomes slow and flaky, and people start ignoring failures — the death of CI.
Other checks belong in the pipeline too: linting and formatting, type checks, security/dependency scans, and code coverage (useful as a signal, dangerous as a target — 100% coverage of trivial code proves nothing).
Enforce this with quality gates: the PR cannot merge unless everything is green. And treat a flaky test as a real bug — one test that fails randomly teaches the whole team to ignore red builds.
GitHub Actions
Workflows, events, jobs and steps, matrix builds, secrets and reusable workflows. The easiest place to start.
GitHub Actions is the easiest place to learn CI/CD because it lives right next to your code. A workflow is a YAML file in .github/workflows/ that runs on events (a push, a PR, a schedule).
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm test
The pieces: a job runs on a fresh runner; steps run in order; uses: pulls a reusable action from the Marketplace (checkout, language setup, cloud login), while run: executes shell commands.
Level up with matrix builds (test across several versions at once), secrets (${{ secrets.TOKEN }}) for credentials, and reusable workflows so teams don't copy-paste pipelines. Actions can also build and push Docker images and deploy to your cloud — a complete path from commit to production.
GitLab CI
Stages, jobs, runners, artifacts, cache, rules and manual production gates in .gitlab-ci.yml.
GitLab CI is the other CI system you'll meet constantly, and it's fully integrated into GitLab — repo, CI, registry, and issues in one product. Pipelines live in .gitlab-ci.yml at the repo root.
stages: [build, test, deploy]
build-job:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths: [dist/]
test-job:
stage: test
script: npm test
deploy-prod:
stage: deploy
script: ./deploy.sh
environment: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual # require a human click for prod
The model: stages run in sequence, jobs inside a stage run in parallel, and jobs execute on runners (which you can host yourself). Jobs pass files forward as artifacts, speed up with cache, and are conditionally included with rules. environment: gives you deployment tracking and one-click rollback in the UI.
The concepts map almost one-to-one onto GitHub Actions — stages/jobs/steps, artifacts, caching, secrets, manual approvals. Learn one deeply and you can read any of them.
Jenkins & pipeline-as-code
The enterprise veteran: controllers, agents, stages, plugins — and why you always use a Jenkinsfile.
Jenkins is the veteran of CI — self-hosted, endlessly pluggable, and still running the pipelines of a huge share of enterprises. Modern SaaS CI is nicer to use, but Jenkins is everywhere in real jobs, so knowing it makes you employable in a large installed base.
You define builds as pipeline as code in a Jenkinsfile committed to your repo:
pipeline {
agent any
stages {
stage('Build') { steps { sh 'make build' } }
stage('Test') { steps { sh 'make test' } }
stage('Deploy') {
when { branch 'main' }
steps { sh './deploy.sh' }
}
}
}
The model: a controller schedules work onto agents (worker nodes), pipelines are made of stages and steps, and thousands of plugins integrate everything from Docker to Slack. That plugin ecosystem is both its superpower and its weakness — plugins are a maintenance and security burden, so keep them few and updated.
Practical advice: never configure jobs by clicking in the UI ("freestyle" jobs) — they can't be reviewed or version-controlled. Always use a Jenkinsfile, so your pipeline lives in Git like everything else.
Artifacts & registries
Build once, deploy everywhere: immutable versioned artifacts, container registries, and retention policies.
An artifact is the thing your build produces — a JAR, a binary, a Python wheel, a container image, a Helm chart. The golden rule of delivery: build once, deploy everywhere.
Why it matters: if you rebuild the app separately for staging and for production, you have not tested what you shipped — the two builds can differ (a dependency moved, a base image updated). Instead, build a single immutable, versioned artifact, and promote that exact artifact through each environment.
Artifacts live in a registry/repository:
- Container images — Docker Hub, GitHub Container Registry, AWS ECR, Harbor.
- Language packages — npm, PyPI, Maven — usually proxied through Nexus or Artifactory, which also caches public packages (faster builds, and you survive an upstream outage).
Good practice: tag artifacts immutably with a version or Git SHA (app:1.5.0, app:git-a1b2c3d), never overwrite a published tag, scan images on push, sign them so you can prove provenance, and set retention policies — registries grow without limit and get expensive.
- $Build a pipeline that lints, tests, scans, builds a container image, and pushes it to a registry on every push to main.
- $Add a staging deploy plus a manual approval gate before production, and make the version a Git tag.