I remember the first time I set up a CI/CD pipeline. It took me an entire weekend of reading docs, copying YAML files, and wondering why my builds kept failing. I’m writing this so you don’t have to go through that.
What Are We Building?
By the end of this article, you’ll have a GitHub Actions workflow that automatically runs your tests on every push, checks code quality, and deploys to production when you merge to main. We’ll use a Node.js project as our example, but the concepts apply to any language.
Your First Workflow File
Create .github/workflows/ci.yml in your repository:
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
- run: npm run lint
That’s it for a basic pipeline. Push this file and GitHub will automatically run your tests on Node 18 and 20 every time you push code or open a pull request.
Adding Deployment
Let’s add automatic deployment when code is merged to main:
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to production
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
npm ci --production
npm run build
# Your deployment command here
The needs: test line ensures deployment only happens after all tests pass. The if condition ensures it only deploys from the main branch.
Caching Dependencies
One of the biggest time saves is caching. Without it, every build downloads all your npm packages from scratch:
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
This typically cuts build times by 30-50%.
Managing Secrets
Never put API keys or tokens directly in your workflow files. Use GitHub’s encrypted secrets:
- Go to your repo Settings > Secrets and variables > Actions
- Click “New repository secret”
- Add your secret (e.g., DEPLOY_TOKEN)
- Reference it in workflows as
${{ secrets.DEPLOY_TOKEN }}
Common Gotchas
- YAML indentation: Use spaces, never tabs. A single wrong indent breaks everything
- Branch names: Make sure your workflow triggers match your actual branch names
- Timeouts: Add
timeout-minutes: 10to jobs so hung processes don’t burn your free minutes - Concurrency: Use the
concurrencykey to cancel redundant builds when you push multiple times quickly
Start simple, get it working, then add complexity. A basic pipeline that runs your tests is infinitely better than a complex one that you never finish setting up.
