GitHub Actions CI/CD: Automate Your Build, Test, and Deploy Pipeline
GitHub Actions is a CI/CD platform built directly into GitHub. Every time you push code, open a pull request, or create a release, Actions can automatically build your project, run tests, and deploy to production. It’s free for public repositories and includes 2,000 minutes/month for private repos.
How GitHub Actions Works
Workflows are defined in YAML files inside .github/workflows/ in your repository. Each workflow contains:
- Triggers — Events that start the workflow (push, PR, schedule, manual)
- Jobs — Groups of steps that run on a virtual machine
- Steps — Individual commands or actions to execute
Your First Workflow
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Build
run: npm run build
Push this file and GitHub will automatically run the workflow on every push to main and on every pull request.
Workflow Triggers
Push and Pull Request
on:
push:
branches: [main, develop]
paths:
- 'src/**'
- 'package.json'
pull_request:
branches: [main]
Scheduled (Cron)
on:
schedule:
- cron: '0 3 * * *' # Daily at 3 AM UTC
Manual Dispatch
on:
workflow_dispatch:
inputs:
environment:
description: 'Deploy environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
Trigger manually from the Actions tab in GitHub.
Release
on:
release:
types: [published]
Real-World Examples
Node.js with Testing and Coverage
name: Node.js CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
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 -- --coverage
- name: Upload coverage
if: matrix.node-version == 20
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
This tests against Node.js 18, 20, and 22 simultaneously using a matrix strategy.
Build and Push Docker Image
name: Docker Build
on:
push:
branches: [main]
tags: ['v*']
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
username/myapp:latest
username/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Deploy Static Site to Cloudflare Pages
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-site
Deploy to a VPS via SSH
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm ci --production
pm2 restart myapp
Secrets and Environment Variables
Store sensitive data in GitHub Secrets:
- Go to repository Settings → Secrets and variables → Actions
- Click New repository secret
- Add secrets like
DOCKER_TOKEN,SSH_PRIVATE_KEY,API_KEY
Use them in workflows:
env:
API_URL: https://api.example.com
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: curl -H "Authorization: $API_KEY" $API_URL/deploy
Caching Dependencies
Speed up workflows by caching dependencies:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Built-in npm caching
For custom caching:
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
Job Dependencies and Conditions
Sequential Jobs
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test # Only runs after 'test' succeeds
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Only on main branch
steps:
- run: echo "Deploying..."
Conditional Steps
steps:
- name: Deploy to staging
if: github.event_name == 'pull_request'
run: deploy-staging.sh
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: deploy-production.sh
- name: Notify on failure
if: failure()
run: curl -X POST $SLACK_WEBHOOK -d '{"text":"Build failed!"}'
Reusable Workflows
Create shared workflows that other repos can call:
.github/workflows/reusable-deploy.yml:
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
deploy-key:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to ${{ inputs.environment }}"
Call it from another workflow:
jobs:
deploy:
uses: org/repo/.github/workflows/reusable-deploy.yml@main
with:
environment: production
secrets:
deploy-key: ${{ secrets.DEPLOY_KEY }}
Useful Actions from the Marketplace
| Action | Purpose |
|---|---|
actions/checkout@v4 | Checkout repository code |
actions/setup-node@v4 | Set up Node.js |
actions/cache@v4 | Cache dependencies |
actions/upload-artifact@v4 | Save build artifacts |
docker/build-push-action@v5 | Build and push Docker images |
cloudflare/wrangler-action@v3 | Deploy to Cloudflare |
appleboy/ssh-action@v1 | Run commands via SSH |
peaceiris/actions-gh-pages@v3 | Deploy to GitHub Pages |
slackapi/slack-github-action@v1 | Send Slack notifications |
Browse more at github.com/marketplace?type=actions.
Debugging Workflows
Enable debug logging
Add a repository secret ACTIONS_STEP_DEBUG with value true for verbose output.
Re-run failed jobs
Click Re-run jobs in the Actions tab. You can re-run only failed jobs to save time.
Local testing with act
Test workflows locally using act:
brew install act # macOS
act # Run default workflow
act -j test # Run specific job
act pull_request # Simulate PR event
Pricing
| Plan | Minutes/month | Runners |
|---|---|---|
| Free | 2,000 | Linux only |
| Team | 3,000 | Linux, Windows, macOS |
| Enterprise | 50,000 | All + self-hosted |
Public repositories get unlimited free minutes.
Conclusion
GitHub Actions eliminates the need for separate CI/CD tools like Jenkins or CircleCI. With workflows defined as code in your repository, every team member can see, modify, and review the automation pipeline. Start with a basic build-and-test workflow, then gradually add deployment, notifications, and scheduled tasks as your project grows.