
CI/CD Pipelines: Beginner's Blog Tutorial
CI/CD sounds like enterprise jargon, but the idea fits in one sentence: every time you push code, a machine checks it and, if everything passes, ships it. No "works on my machine", no Friday-evening manual deploys. This tutorial builds a real pipeline with GitHub Actions, from your first green checkmark to an automatic deployment. Every workflow in this guide has been run before it was written down.
The two halves, in plain words
- CI — Continuous Integration: on every push, install dependencies, run the linter, run the tests. Broken code is caught in minutes, not discovered by a colleague next week.
- CD — Continuous Deployment: when the checks pass on your main branch, the new version goes live automatically.
We use GitHub Actions because it is built into GitHub, free for public repositories, and comes with a generous free allowance for private ones. The concepts transfer directly to GitLab CI, Bitbucket Pipelines and others.
What you need
- A GitHub account and a repository with a small Node.js project (any project works; adjust the commands to your stack)
- A
package.jsonwith atestscript — even one passing test is enough to start
Step 1: Your first workflow
A pipeline is a YAML file in your repository under .github/workflows/. 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: 22
cache: npm
- run: npm ci
- run: npm testCommit and push it. Open the Actions tab of your repository: a run is already going. Line by line, this file says:
on:— run this on every push tomainand on every pull request.runs-on: ubuntu-latest— GitHub starts a fresh Linux machine for every run. Nothing from previous runs survives, which is exactly the point: the pipeline proves your project builds from scratch.actions/checkout— clones your code onto that machine.actions/setup-node— installs Node.js 22 and caches your npm downloads between runs.npm ci— installs exactly whatpackage-lock.jsonspecifies. In pipelines, alwaysnpm ci, nevernpm install.npm test— the actual check. A non-zero exit code fails the run and paints the commit red.
Step 2: Add a lint step
Tests tell you the code works; a linter tells you it is consistent. Add a step before the tests:
- run: npm run lint
- run: npm testOrder matters for speed: linting takes seconds, so let it fail fast before the slower test suite runs. The pipeline stops at the first failing step.
Step 3: Protect your main branch
A pipeline nobody has to obey is decoration. In your repository settings, open Branches → Add branch ruleset and require the CI check to pass before merging. From now on, a red pull request physically cannot be merged. This one setting is where the real culture change happens: the pipeline stops being advice and becomes the gate.
Step 4: Secrets — never in the YAML
Deployments need tokens and keys. They do not go in the workflow file; they go in Settings → Secrets and variables → Actions. In the workflow you reference them like this:
- run: npx some-deploy-tool
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}GitHub masks secret values in logs. Two habits on top of that:
- Scope tokens as narrowly as the provider allows (one project, one permission).
- If a secret ever lands in a commit, rotate it immediately. Deleting the commit does not unpublish it.
Step 5: The D — deploy automatically
The simplest honest setup for a website: connect your repository to a hosting platform like Vercel, Netlify or Render. They watch your main branch and deploy on every push, so your "CD" is one integration click, and your CI ruleset from step 3 guarantees only green code reaches main.
When you outgrow that and want the deploy inside your own pipeline, add a second job that only runs on main, after the tests:
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "run your deploy command here"
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}needs: test chains the jobs: deploy waits for tests and is skipped when they fail. The if: line keeps pull requests from deploying.
Step 6: Read a failing run
Sooner or later the checkmark turns red. The debugging routine:
- Open the run in the Actions tab and click the failing step. The log shows exactly what the machine saw.
- Reproduce locally: run the same command (
npm ci && npm test) in a clean state. "Works locally, fails in CI" is almost always a file you forgot to commit, an environment variable that only exists on your machine, or a version difference. - Pin what the pipeline proved wrong: if CI caught a dependency missing from
package.json, that is the pipeline doing its job.
Habits that keep pipelines pleasant
- Keep it under five minutes. A slow pipeline gets ignored or bypassed. Cache dependencies, split slow suites.
- Fix red immediately. A main branch that stays red for a day teaches everyone that red is normal.
- One workflow file per purpose. A
ci.ymlfor checks, a separate file for scheduled jobs. Small files stay readable. - Add a status badge to your README so the state of main is visible at a glance:
Where to go next
- Add a build step (
npm run build) so broken production builds are caught before deploy, not during. - Containerising your app first? The pipeline can build and push a Docker image — our Docker tutorial covers the image side.
- Deploying to functions instead of servers? See the serverless tutorial, and browse the rest of the Cloud & DevOps chapter.
Did a workflow in this guide fail on your repository? Send us the failing step — we retest and correct reported issues.
Comments
No comments yet. Be the first to share your thoughts.


