What you'll learn
Quick Answer
Continuous integration means every change is automatically built and tested when pushed, so problems are caught within minutes rather than at release. Continuous delivery means those verified changes are automatically prepared for release, and continuous deployment means they go to production automatically. A pipeline is the sequence of steps that runs — typically install, lint, test, build, deploy — defined in a file in your repository.
The Problem It Solves
Without automation, a team's work diverges quietly. Four people work on separate branches for two weeks, each testing only on their own machine. At integration time everything conflicts at once, and nobody knows which change broke what.
The other half of the problem is deployment. If releasing means someone manually building, copying files and restarting a server, then it is slow, error-prone and depends on that person remembering every step. So releases become rare and large, which makes each one riskier — the opposite of what you want.
Continuous integration addresses the first: merge small changes frequently, and have a machine build and test every one automatically. A break is caught within minutes, by the person who caused it, while the change is still fresh in their mind.
Continuous delivery addresses the second: every change that passes is automatically packaged and ready to release, so deploying is a decision rather than a project.
Continuous deployment goes one step further — passing changes go to production with no human approval. Many teams deliberately stop at delivery and keep a manual approval step, which is a perfectly reasonable choice.
What a Pipeline Actually Does
A pipeline is a sequence of steps a service runs on a fresh machine whenever a trigger fires — usually a push or a pull request.
1. Checkout get the code at this commit
2. Install install dependencies from the lockfile
3. Lint check style and obvious errors
4. Test run the test suite
5. Build produce the artefact — a bundle, an image, a binary
6. Deploy publish it (often only from the main branch)Any step failing stops the pipeline and reports it. On a pull request, that failure usually blocks merging — which is the point.
The detail that makes CI valuable: it runs on a clean machine. That is why it catches the classic "works on my machine" failures — a dependency you installed globally and forgot, a file you never committed, an environment variable only set locally, or a case-sensitive import path that Windows tolerated and Linux does not.
Common triggers: every push to any branch, every pull request, a push to main only, a schedule, or a manual run. Most projects run tests on every pull request and deploy only from main.
A First Workflow You Can Copy
GitHub Actions is the easiest starting point because it needs no separate account. Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci # installs exactly from the lockfile
- run: npm run lint
- run: npm test
- run: npm run buildPush that and it runs on every pull request, showing a green tick or a red cross next to the commit.
Use npm ci rather than npm install in CI. It installs precisely what the lockfile specifies and fails if they disagree, which is exactly the reproducibility you want — and it is faster.
The Python equivalent is the same shape:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: pytestStart with just the test step. Add linting, then building, then deployment as you need them — a pipeline that only runs tests is already most of the value.
Secrets and Deployment
Deployment steps need credentials, and this is where beginners make a serious mistake.
Never put secrets in the workflow file. It is in your repository, visible to anyone with access and permanently in the git history. Use the platform's encrypted secrets store and reference them:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.shSecrets are masked in logs, so they do not leak through printed output.
Gate deployment on the branch so pull requests do not deploy:
deploy:
needs: test # only if tests passed
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latestThe needs line is important — without it, jobs run in parallel and you can deploy code whose tests are still running or have failed.
If a secret is ever committed, rotate it. Deleting the file is not enough; it remains in the history and may already have been scraped. Create a new credential at the provider, update it, then revoke the old one.
Getting Value From It Without Overdoing It
Keep it fast. A pipeline taking twenty minutes stops being useful, because people push and move on rather than waiting. Cache dependencies, and run independent jobs in parallel. Under five minutes is a good target.
A failing pipeline must be fixed immediately. Once a team gets used to a red main branch, CI has stopped working — nobody trusts the signal, and real failures hide among the ignored ones.
Do not test everything on day one. A pipeline that runs a handful of meaningful tests is far better than an ambitious one that is abandoned. Add coverage where bugs actually occur.
What to avoid as a beginner: Kubernetes, complex multi-environment promotion, and elaborate approval chains. These solve problems of scale that a personal project or small team does not have, and they add real maintenance cost.
Why it is worth doing on personal projects. A repository with a passing CI badge signals that you understand testing and automation — genuinely uncommon in fresher portfolios. It also catches the mistakes that would otherwise embarrass you when someone else tries to run your project.
Start with one file that runs your tests. That is CI, and it is most of the benefit.
