Set Up GitHub Actions CI/CD -- A Hands-On Guide for Any Project
Set Up GitHub Actions CI/CD -- A Hands-On Guide for Any Project
Every time you push code, you should not have to manually run tests, build the project, check formatting, and deploy. That is what CI/CD is for -- it automates the boring, repetitive parts of shipping software so you can focus on writing code.
GitHub Actions is built into every GitHub repository for free. This guide walks you through setting up real CI/CD pipelines, not toy examples.
How GitHub Actions Works
GitHub Actions uses YAML files in the .github/workflows/ directory of your repository. Each file defines a workflow -- a set of jobs that run in response to events like pushes, pull requests, or schedules.
Key concepts:
- •Workflow: A YAML file that defines the automation. You can have multiple workflows per repository.
- •Event: What triggers the workflow (push, pull_request, schedule, manual dispatch).
- •Job: A set of steps that run on the same machine. Jobs run in parallel by default.
- •Step: A single task within a job. Can be a shell command or a pre-built action.
- •Action: A reusable unit of code. The marketplace has thousands of them.
- •Runner: The machine that runs your job. GitHub provides free Linux, macOS, and Windows runners.
Your First Workflow
Create the directory and file:
mkdir -p .github/workflows
Create .github/workflows/ci.yml:
name: CI
>
on:
push:
branches: [main]
pull_request:
branches: [main]
>
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
Push this file and go to the Actions tab in your GitHub repository. You will see the workflow running.
What This Does
- 1Triggers on every push to main and every PR targeting main
- 2Checks out your code
- 3Sets up Node.js 20
- 4Installs dependencies with npm ci (clean install)
- 5Runs your test suite
Real-World CI Pipeline for Node.js
Here is a production-ready CI pipeline that does more than just run tests:
name: CI
>
on:
push:
branches: [main]
pull_request:
branches: [main]
>
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run lint
>
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npx tsc --noEmit
>
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm test -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
>
build:
needs: [lint, typecheck, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run build
What Makes This Better
- •Parallel jobs: Lint, typecheck, and test run simultaneously, cutting total pipeline time
- •Dependency caching: The cache: "npm" line caches node_modules between runs, making installs near-instant
- •Build depends on quality gates: The build job only runs if lint, typecheck, and test all pass
- •Coverage artifact: Test coverage is uploaded and available for download from the workflow run
CI Pipeline for Python
name: CI
>
on:
push:
branches: [main]
pull_request:
branches: [main]
>
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: python-version-from-matrix
cache: "pip"
- run: pip install -r requirements.txt
- run: pip install pytest pytest-cov
- run: pytest --cov=src tests/
>
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install ruff
- run: ruff check .
- run: ruff format --check .
The matrix strategy runs tests across multiple Python versions simultaneously.
Docker Build and Push
For projects that deploy as Docker containers:
name: Build and Push Docker Image
>
on:
push:
branches: [main]
>
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
>
- uses: docker/setup-buildx-action@v3
>
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: github-actor
password: secrets-github-token
>
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/your-username/your-app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
This builds your Docker image and pushes it to GitHub Container Registry (ghcr.io). The cache-from and cache-to lines use GitHub Actions cache for Docker layers, making subsequent builds much faster.
Managing Secrets
Never put API keys, tokens, or passwords in your workflow files. Use GitHub Secrets:
- 1Go to your repository Settings -> Secrets and variables -> Actions
- 2Click "New repository secret"
- 3Add your secret (e.g., DEPLOY_TOKEN)
Use it in your workflow:
- run: deploy --token my-token
env:
DEPLOY_TOKEN: secrets-deploy-token
Secrets are masked in logs -- if they accidentally appear in output, GitHub redacts them automatically.
Environment Secrets
For different secrets per environment (staging vs production):
- 1Create environments in Settings -> Environments
- 2Add secrets to each environment
- 3Reference the environment in your job:
> jobs:
> deploy:
> runs-on: ubuntu-latest
> environment: production
> steps:
> - run: deploy --token my-production-token
Deployment Workflows
Deploy to Vercel
> name: Deploy
>
> on:
> push:
> branches: [main]
>
> jobs:
> deploy:
> runs-on: ubuntu-latest
> steps:
> - uses: actions/checkout@v4
> - uses: amondnet/vercel-action@v25
> with:
> vercel-token: secrets-vercel-token
> vercel-org-id: secrets-vercel-org-id
> vercel-project-id: secrets-vercel-project-id
> vercel-args: "--prod"
Deploy to a VPS via SSH
> deploy:
> runs-on: ubuntu-latest
> needs: [test, build]
> steps:
> - uses: actions/checkout@v4
> - uses: appleboy/ssh-action@v1
> with:
> host: secrets-server-host
> username: secrets-server-user
> key: secrets-ssh-private-key
> script: |
> cd /var/www/my-app
> git pull origin main
> npm ci --only=production
> npm run build
> pm2 restart my-app
Caching for Speed
Caching dependencies is the single biggest speedup for CI pipelines.
Node.js
The setup-node action has built-in caching:
> - uses: actions/setup-node@v4
> with:
> node-version: 20
> cache: "npm"
Custom Caching
For anything else, use the cache action:
> - uses: actions/cache@v4
> with:
> path: ~/.cache/some-tool
> key: tool-cache-hash-of-lockfile
> restore-keys: |
> tool-cache-
Scheduled Workflows
Run workflows on a schedule using cron syntax:
on:
schedule:
- cron: "0 9 1" # Every Monday at 9:00 UTC
Useful for:
- •Dependency update checks -- run npm audit or pip audit weekly
- •Stale issue cleanup -- close issues with no activity
- •Performance benchmarks -- track performance over time
- •Database backups -- trigger backup scripts on a schedule
Workflow Tips
Conditional Steps
> - run: npm run deploy
> if: github.ref == 'refs/heads/main' && github.event_name == 'push'
Manual Triggers
> on:
> workflow_dispatch:
> inputs:
> environment:
> description: "Environment to deploy to"
> required: true
> default: "staging"
> type: choice
> options:
> - staging
> - production
This adds a "Run workflow" button in the Actions tab with a dropdown to select the environment.
Reusable Workflows
If multiple repositories need the same CI pipeline, create a reusable workflow:
> # In a shared repo: .github/workflows/node-ci.yml
> on:
> workflow_call:
> inputs:
> node-version:
> required: false
> type: string
> default: "20"
Then call it from other repos:
> jobs:
> ci:
> uses: your-org/shared-workflows/.github/workflows/node-ci.yml@main
> with:
> node-version: "20"
Debugging Failed Workflows
- •Read the logs carefully. Click on the failed step to see the full output. The error is almost always in the last few lines.
- •Run locally. Use a tool like act to run GitHub Actions workflows on your machine before pushing.
- •Add debug logging. Set the ACTIONS_STEP_DEBUG secret to true for verbose output.
- •Check runner limits. Free GitHub runners have 7GB RAM and 14GB disk. If your build needs more, use larger runners or optimize your pipeline.
Free Tier Limits
GitHub Actions is free for public repositories. For private repositories:
- •2,000 minutes/month on the Free plan
- •3,000 minutes/month on the Team plan
- •50,000 minutes/month on the Enterprise plan
macOS runners use 10x minutes (1 minute of macOS = 10 minutes from your quota). Linux runners are 1:1.
To stay within limits: cache aggressively, skip CI on documentation-only changes, and cancel redundant workflow runs with the concurrency setting.
For more developer guides and free tools, check out our blog and explore our developer tools.