GitHub Actions is a CI/CD platform built directly into GitHub that automates your build, test, and deployment pipeline whenever you push code or open a pull request. It's free for public repositories and offers generous limits for private ones.
1. Terminology & Architecture
Core Concepts
| Term | Description |
|---|---|
| Workflow | The automated process defined in a YAML file (the entire CI/CD pipeline). |
| Event | What triggers the workflow (e.g., push, pull_request, schedule). |
| Job | A set of steps that run sequentially on the same runner. |
| Step | An individual task (shell command, Action, or script). |
| Action | A reusable application (e.g., actions/checkout@v4, custom actions). |
| Runner | The server that executes the workflow (GitHub-hosted or self-hosted). |
File Structure
.github/
├── workflows/
│ ├── ci.yml # Lint, build, test on PR
│ ├── deploy-staging.yml # Deploy to staging on develop push
│ ├── deploy-production.yml # Deploy to prod on main push
│ ├── security-scan.yml # Nightly security scans
│ ├── release.yml # Create releases on tag push
│ └── cleanup.yml # Weekly cleanup tasks
├── dependabot.yml # Dependency updates
└── CODEOWNERS # Required reviewers2. Workflow Events: What Triggers Execution?
Push Event
on:
push:
branches:
- main
- develop
- 'release/**' # Matches release/v1, release/v2, etc.
paths:
- 'src/**' # Only trigger if src/ changed
- 'package.json'
tags:
- 'v*' # Semantic versioning tagsPull Request Event
on:
pull_request:
types:
- opened
- synchronize # New commits pushed to PR
- reopened
branches:
- main
- develop
paths:
- 'src/**'Schedule (Cron)
on:
schedule:
- cron: '0 2 * * 1-5' # 2 AM UTC, Mon-Fri (nightly builds)
- cron: '0 0 * * 0' # Midnight UTC, every Sunday (weekly)Manual Trigger (workflow_dispatch)
on:
workflow_dispatch:
inputs:
environment:
description: 'Which environment to deploy to?'
required: true
default: staging
type: choice
options:
- staging
- production
version:
description: 'Version to deploy'
required: false
type: stringWorkflow Trigger
on:
workflow_run:
workflows: ["CI"] # Triggered after CI workflow completes
types:
- completed
branches:
- mainRepository Dispatch (Via API)
on:
repository_dispatch:
types:
- trigger-deploymentTrigger via curl:
curl -X POST https://api.github.com/repos/USER/REPO/dispatches \
-H "Authorization: token YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"event_type":"trigger-deployment","client_payload":{"ref":"main"}}'3. Workflow Syntax: Complete Reference
Basic Structure
name: CI/CD Pipeline # Workflow name (shown in Actions tab)
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env: # Workflow-level environment variables
NODE_ENV: production
REGISTRY: ghcr.io
jobs:
lint: # Job ID (used in dependencies)
name: Code Quality # Job display name
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lintJob Configuration
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30 # Job timeout (default 360 min)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Cancel previous runs on new push
strategy:
matrix: # Test on multiple versions
node-version: [ 18, 20, 22 ]
os: [ ubuntu-latest, windows-latest ]
max-parallel: 4 # Run max 4 jobs in parallel
fail-fast: false # Don't cancel other matrix jobs
environment: production # Require deployment approval
outputs:
artifact-id: ${{ steps.build.outputs.id }} # Share outputs with other jobs
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Get full history for versioning4. Steps & Context Variables
Running Scripts
steps:
- name: Run build
run: npm run build
working-directory: ./frontend # Change working directory
shell: bash # Explicit shell (bash, sh, pwsh, cmd)
env:
DEBUG: true # Step-level environment variableUsing Actions
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Enable dependency caching
cache-dependency-path: 'package-lock.json'Context Variables
- name: Print context
run: |
echo "GitHub Actor: ${{ github.actor }}"
echo "Repository: ${{ github.repository }}"
echo "Branch: ${{ github.ref }}"
echo "Commit: ${{ github.sha }}"
echo "Event Name: ${{ github.event_name }}"
echo "PR Number: ${{ github.event.pull_request.number }}"Conditional Execution
- name: Notify on failure
if: failure() # Run only if previous step failed
run: curl -X POST webhook.site/...
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy.sh
- name: Run tests
if: contains(github.head_ref, 'test') # If branch name contains 'test'
run: npm testJob Dependencies & Ordering
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
build:
runs-on: ubuntu-latest
needs: lint # Wait for lint job to finish
steps:
- run: npm run build
test:
runs-on: ubuntu-latest
needs: [lint, build] # Wait for multiple jobs
steps:
- run: npm test
deploy:
runs-on: ubuntu-latest
needs: test
if: success() # Only run if test succeeded
steps:
- run: ./deploy.sh5. Matrix Strategy: Test Multiple Configurations
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [16, 18, 20, 22]
os: [ubuntu-latest, macos-latest]
include:
- node-version: 20
os: ubuntu-latest
experimental: true # Add custom property
exclude:
- node-version: 16
os: macos-latest # Don't test Node 16 on macOS
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
if: ${{ !matrix.experimental }} # Skip experimental configsResult: 7 parallel jobs (16+18+20+22 on Ubuntu, 18+20+22 on macOS, minus 16 on macOS)
6. Caching & Artifacts
Dependency Caching
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-Upload Artifacts
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
if: always() # Upload even if build failed
with:
name: build-${{ matrix.os }}-${{ matrix.node-version }}
path: dist/
retention-days: 5 # Delete after 5 daysDownload Artifacts
deploy:
needs: build
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: build-ubuntu-latest-20
path: ./dist
- run: npm run deploy7. Secrets & Variables: Managing Sensitive Data
Repository Secrets
Setting up: Settings → Secrets and variables → Actions → New repository secret
- name: Deploy
env:
API_KEY: ${{ secrets.PROD_API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
run: ./deploy.shRepository Variables (Non-sensitive)
- name: Build
env:
REGISTRY: ${{ vars.REGISTRY }} # ghcr.io
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
run: docker build -t $REGISTRY/myapp .Environment-specific Secrets
- Create GitHub Environment: Settings → Environments → New environment
- Add secrets specific to that environment
- Reference in workflow:
jobs:
deploy-production:
environment:
name: production
url: https://prod.example.com
steps:
- run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }} # Uses prod environment secretMasking Secrets in Logs
echo "::add-mask::${SENSITIVE_VALUE}"
echo "Using token: $SENSITIVE_VALUE" # Logs show: Using token: ***8. Community Actions: Popular & Useful
Check out code
- uses: actions/checkout@v4Setup languages
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- uses: actions/setup-go@v4
with:
go-version: '1.21'Publish results
- uses: actions/upload-artifact@v3
with:
name: test-results
path: junit.xml
- uses: actions/download-artifact@v3
with:
name: test-resultsCreate releases
- uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: falseDocker operations
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/build-push-action@v4
with:
push: true
tags: myregistry/myapp:latestDeploy to AWS
- uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: us-east-1
- run: aws s3 sync dist/ s3://my-bucket/9. Self-Hosted Runners
Use Case
- Private repositories
- Large workflows needing resources
- Custom environment requirements
- On-premises deployments
Setup
# 1. Go to repository Settings → Actions → Runners → New self-hosted runner
# 2. Download and run setup
./config.sh --url https://github.com/USER/REPO --token TOKEN
./run.shUse in Workflow
jobs:
build:
runs-on: self-hosted # Use self-hosted runner
# or with labels:
runs-on: [self-hosted, linux, x64]
steps:
- run: npm test10. Real-World Examples
Full CI/CD Pipeline for Node.js
name: Full CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
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
- run: npm run format:check
test:
runs-on: ubuntu-latest
needs: lint
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
build:
runs-on: ubuntu-latest
needs: test
outputs:
image-tag: ${{ steps.image.outputs.tag }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v2
- id: image
run: echo "tag=ghcr.io/${{ github.repository }}:${{ github.sha }}" >> $GITHUB_OUTPUT
- uses: docker/build-push-action@v4
with:
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.image.outputs.tag }}
deploy-staging:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
environment:
name: staging
url: https://staging.example.com
steps:
- uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: us-east-1
- run: aws ecs update-service --cluster staging --service myapp --force-new-deployment
deploy-production:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: production
url: https://example.com
steps:
- uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: us-east-1
- run: aws ecs update-service --cluster production --service myapp --force-new-deployment
- name: Notify deployment
run: curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d "Deployed to production"Security Scanning Workflow
name: Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * 0' # Weekly Sunday 2 AM
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: github/super-linter@v4
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dependency-check/Dependency-Check_Action@main
with:
project: 'myapp'
path: '.'
format: 'JSON'
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: trufflesecurity/trufflehog@main
with:
path: ./
base: main
head: HEAD11. Optimization: Speed & Cost
Caching Best Practices
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-Avoid Redundant Work
- name: Build
run: npm run build
- name: Test
needs: build # Run only after build succeeds
run: npm testParallel Jobs
strategy:
matrix:
test-suite: [unit, integration, e2e]
jobs:
test:
name: ${{ matrix.test-suite }} tests
runs-on: ubuntu-latest
steps:
- run: npm run test:${{ matrix.test-suite }}12. Debugging & Troubleshooting
Enable Debug Logging
# In workflow:
- name: Debug info
run: echo ${{ toJson(github) }}
# Or set secret: ACTIONS_STEP_DEBUG = trueCheck Job Status
- name: Print job status
if: always() # Run regardless of previous step
run: |
echo "Job status: ${{ job.status }}"
echo "Failure context: ${{ failure() }}"Use tee to Save Logs
- name: Build with logging
run: npm run build 2>&1 | tee build.log
- uses: actions/upload-artifact@v3
if: failure()
with:
name: build-logs
path: build.log13. Best Practices
✅ DO:
- Use specific action versions (e.g.,
@v4not@main) - Cache dependencies
- Run linting first (cheap, fast)
- Use matrix for testing multiple configs
- Mask sensitive output with
::add-mask:: - Set reasonable timeouts
- Require approval for production deployments
- Document your workflows with comments
- Use branch protection rules requiring CI to pass
❌ DON'T:
- Hardcode secrets in YAML
- Use
@latestfor actions (unpredictable) - Run expensive operations unnecessarily
- Have overly complex single jobs (split into smaller jobs)
- Ignore test failures in matrices
- Use
continue-on-error: truewithout good reason - Run the same workflow on every push and pull request
14. GitHub Actions Limits & Pricing
| Feature | Free Plan | Pro/Team | Enterprise |
|---|---|---|---|
| Workflow minutes/month | 2,000 | 3,000 | 50,000 |
| Concurrent jobs | 20 | 40 | 180 |
| Matrix builds | Unlimited | Unlimited | Unlimited |
| Storage | 500 MB | 2 GB | 50 GB |
| Self-hosted runners | Unlimited free | Unlimited free | Unlimited free |
Summary
GitHub Actions provides a powerful, integrated CI/CD solution:
- Free for public repos, generous limits for private
- Event-driven: Triggered by push, PR, schedule, or webhook
- Flexible: Runs tests, builds, deploys, releases
- Reusable: Community actions from GitHub Marketplace
- Secure: Built-in secret management
- Fast: Parallel job execution and caching
- Observable: Detailed logs and status checks
Start simple (lint → test → build), then expand to deployment strategies (canary, blue-green) as your team grows.