CI/CD with GitHub Actions: From Code to Production in Minutes
Learn how to build a robust CI/CD pipeline with GitHub Actions, including automated testing, building, and deploying to production.
Jean-Pierre Broeders
Freelance DevOps Engineer
CI/CD with GitHub Actions: From Code to Production in Minutes
Every developer knows the feeling: you've finished a feature, but deploying it means a manual process full of clicking, waiting, and hoping nothing breaks. GitHub Actions eliminates that pain — with a handful of YAML files, you can automate the entire journey from code commit to live production.
What Is CI/CD, Really?
Continuous Integration (CI) means every code push triggers an automatic test run. Push code, tests run, get feedback. No surprises waiting for you at release time.
Continuous Deployment (CD) takes it further: when tests pass, the code is automatically rolled out to staging or production. No manual handoffs, no deployment scripts run by hand.
GitHub Actions is Microsoft's built-in answer to this challenge, woven directly into GitHub. No external CI server to maintain, no third-party integrations to configure.
Your First Workflow
Workflows live in .github/workflows/ and are written in YAML. A basic workflow for a Node.js project looks like this:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
Save this file and push it — that's all it takes to automatically run tests on every push and pull request.
Managing Secrets Safely
Your deployment step almost always needs credentials: API keys, SSH keys, passwords. These should never live in your code. GitHub provides Secrets for exactly this:
- Go to your repo → Settings → Secrets and variables → Actions
- Add a secret, for example
DEPLOY_SSH_KEY - Reference it in your workflow:
- name: Deploy to server
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
echo "$SSH_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
ssh -i /tmp/deploy_key user@server.example.com "cd /app && git pull && npm install && pm2 restart all"
Secrets are stored encrypted and never appear in logs — even if someone forks your repository.
Deploying to Multiple Environments
In practice, you want a clear separation between staging and production. Environments and branch rules handle this elegantly:
jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: ./deploy.sh staging
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
needs: [test] # Only runs after successful tests
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: ./deploy.sh production
The needs: [test] directive ensures deployment only happens when all tests pass. Non-negotiable for production.
Matrix Builds: Test Multiple Versions in Parallel
Want to be sure your code works on Node 18, 20, and 22? Use matrix strategy:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
GitHub spins up three parallel jobs, one per version. When one version fails, you know exactly which one — without manually switching environments.
Caching for Faster Builds
Every time npm install runs, GitHub downloads all packages from scratch. That adds up. Caching solves this:
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
The cache stores downloaded packages keyed by a hash of package-lock.json. If the lockfile hasn't changed, packages load from cache — often 2–3x faster builds.
Production-Ready Tips
Pin action versions — never use @main:
uses: actions/checkout@v4 # Good
uses: actions/checkout@main # Dangerous — can break unexpectedly
Restrict permissions to only what your workflow needs:
permissions:
contents: read
packages: write
Set timeouts so hung jobs don't run indefinitely:
jobs:
build:
timeout-minutes: 15
Use concurrency groups to prevent duplicate deployments:
concurrency:
group: production-deploy
cancel-in-progress: false # Let running deployments finish
From Zero to Automated in an Afternoon
GitHub Actions has a low barrier to entry: all you need is a GitHub account and a repository. The free tier provides 2,000 minutes per month for private repos, with unlimited minutes for public repos.
Start small: add tests first. Once that's running smoothly, layer in the deployment step. Incrementally, you build a reliable pipeline that saves you hours every week — and eliminates that pre-deployment anxiety for good.
GitHub Actions makes CI/CD accessible to any team: automated testing, multi-environment deployments, and parallel matrix builds all from a single YAML file. Start with tests, add deployment, and iterate from there.
