GitHub Actions transforms your repository into a powerful CI/CD platform. Build, test, and deploy code directly from GitHub without managing external services. This guide covers everything from basic workflows to advanced deployment strategies.
GitHub Actions Fundamentals
┌─────────────────────────────────────────────────────────────────────────┐
│ GitHub Actions Architecture │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ TRIGGER WORKFLOW RESULT │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ ┌──────────┐ ┌──────────────────────┐ ┌──────────┐ │
│ │ Events │──────────────│ .github/workflows/ │────│ Artifacts│ │
│ │ • push │ │ │ │ Logs │ │
│ │ • PR │ │ ┌────────────────┐ │ │ Status │ │
│ │ • cron │ │ │ Job 1 │ │ └──────────┘ │
│ │ • manual │ │ │ ┌────────────┐ │ │ │
│ └──────────┘ │ │ │ Step 1 │ │ │ ┌──────────┐ │
│ │ │ │ Step 2 │ │ │────│ Deploy │ │
│ │ │ │ Step 3 │ │ │ └──────────┘ │
│ │ │ └────────────┘ │ │ │
│ │ └────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────┐ │ │
│ │ │ Job 2 │ │ │
│ │ └────────────────┘ │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key Concepts
| Concept | Description |
|---|---|
| Workflow | Automated process defined in YAML in .github/workflows/ |
| Event | Trigger that starts a workflow (push, PR, schedule) |
| Job | Set of steps running on same runner, jobs run in parallel by default |
| Step | Individual task within a job (action or shell command) |
| Action | Reusable unit from Marketplace or custom |
| Runner | Server executing workflows (GitHub-hosted or self-hosted) |
| Artifact | Files produced by workflow, persist after completion |
Basic CI Workflow
Start with a simple workflow that runs on every push and pull request:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
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 linter
run: npm run lint
- name: Run tests
run: npm test
- name: Build
run: npm run build
Workflow Anatomy
name: CI # Workflow name (shown in Actions tab)
on: # Triggers
push:
branches: [main] # Only main branch
paths: # Only when these files change
- 'src/**'
- 'package.json'
pull_request:
types: [opened, synchronize] # Specific PR events
jobs:
job-name: # Unique job identifier
runs-on: ubuntu-latest # Runner type
timeout-minutes: 10 # Job timeout
steps:
- uses: actions/checkout@v4 # Use an action
with: # Action inputs
fetch-depth: 0
- name: Run command # Step name
run: echo "Hello" # Shell command
env: # Environment variables
MY_VAR: value
Matrix Builds
Test across multiple versions, operating systems, or configurations:
name: Matrix CI
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # Don't cancel other jobs if one fails
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
exclude: # Skip specific combinations
- os: windows-latest
node-version: 18
include: # Add specific combinations
- os: ubuntu-latest
node-version: 20
coverage: true
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
- name: Upload coverage
if: matrix.coverage
uses: codecov/codecov-action@v4
Caching Dependencies
Speed up workflows by caching downloaded dependencies:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Option 1: Built-in cache in setup actions
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Automatic npm caching
# Option 2: Manual cache control
- name: Cache node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
Cache Strategies by Language
| Language | Cache Path | Key Pattern |
|---|---|---|
| Node.js | ~/.npm or node_modules | hashFiles('package-lock.json') |
| Python | ~/.cache/pip | hashFiles('requirements.txt') |
| Go | ~/go/pkg/mod | hashFiles('go.sum') |
| Rust | ~/.cargo, target | hashFiles('Cargo.lock') |
| Java | ~/.m2/repository | hashFiles('pom.xml') |
Working with Secrets
Store sensitive data securely and access in workflows:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Access secrets
- name: Deploy to production
env:
API_KEY: ${{ secrets.PRODUCTION_API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
./deploy.sh
# GITHUB_TOKEN is automatic
- name: Create release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v1.0.0
Secret Best Practices
- Never echo secrets - They're masked, but don't risk it
- Use OIDC for cloud providers - Avoid long-lived credentials
- Scope appropriately - Use environment secrets for production
- Rotate regularly - Update secrets periodically
- Audit access - Review who can access secrets
Artifacts and Outputs
Share data between jobs and preserve build outputs:
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: version
run: echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT
- name: Build
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build-files
path: dist/
retention-days: 5
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: build-files
path: dist/
- name: Deploy version ${{ needs.build.outputs.version }}
run: ./deploy.sh dist/
Deployment Workflows
Deploy to Cloud Providers
# Deploy to AWS
jobs:
deploy-aws:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
- name: Deploy to S3
run: aws s3 sync ./dist s3://my-bucket/
# Deploy to Azure
jobs:
deploy-azure:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: my-app
package: ./dist
# Deploy to Vercel
jobs:
deploy-vercel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
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'
Environment Protection
Use environments for production deployments with approvals:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # No protection needed
steps:
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com # Shows link in GitHub UI
steps:
- run: ./deploy.sh production
Configure environment protection in Settings > Environments:
- Required reviewers
- Wait timer
- Branch restrictions
- Deployment branches
Reusable Workflows
Create workflows that can be called from other workflows:
# .github/workflows/reusable-build.yml
name: Reusable Build
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '20'
environment:
required: true
type: string
secrets:
npm-token:
required: false
outputs:
artifact-name:
description: "Name of uploaded artifact"
value: ${{ jobs.build.outputs.artifact-name }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-name: ${{ steps.upload.outputs.artifact-name }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.npm-token }}
- run: npm run build
- id: upload
uses: actions/upload-artifact@v4
with:
name: build-${{ inputs.environment }}
path: dist/
Call reusable workflow:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
environment: production
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Deploying ${{ needs.build.outputs.artifact-name }}"
Composite Actions
Create custom actions combining multiple steps:
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Setup Node.js and install dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
runs:
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
shell: bash
run: npm ci
- name: Verify installation
shell: bash
run: npm --version && node --version
Use composite action:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup project
uses: ./.github/actions/setup-project
with:
node-version: '20'
- run: npm test
Advanced Patterns
Conditional Jobs
jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'frontend/**'
backend:
- 'backend/**'
build-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Building frontend"
build-backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Building backend"
Scheduled Workflows
name: Nightly Build
on:
schedule:
- cron: '0 2 * * *' # Run at 2 AM UTC daily
workflow_dispatch: # Allow manual trigger
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build:full
- run: npm run test:e2e
Concurrency Control
name: Deploy
on:
push:
branches: [main]
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true # Cancel previous runs
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
Common Workflow Patterns
Pull Request Checks
name: PR Checks
on:
pull_request:
types: [opened, synchronize, reopened]
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
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: codecov/codecov-action@v4
build:
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
Release Workflow
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
files: dist/*
generate_release_notes: true
Debugging Workflows
Enable Debug Logging
Set repository secrets:
ACTIONS_RUNNER_DEBUG:trueACTIONS_STEP_DEBUG:true
Interactive Debugging
- name: Setup tmate session
if: failure()
uses: mxschmitt/action-tmate@v3
timeout-minutes: 15
Local Testing with act
# Install act
brew install act
# Run workflow locally
act push
# Run specific job
act -j build
# Use specific event
act pull_request
# Pass secrets
act -s GITHUB_TOKEN=xxx
Related Guides
- Git & GitHub Complete Guide - Overview of all Git topics
- GitHub Actions Security - Secure your workflows
- GitHub Webhooks - External integrations
- Git Branching Strategies - Branch workflows
Conclusion
GitHub Actions provides powerful CI/CD capabilities directly in your repository. Start with basic workflows for testing and linting, then expand to deployment pipelines, matrix builds, and reusable workflows. Key practices:
- Cache dependencies for faster builds
- Use OIDC for cloud authentication
- Protect environments for production
- Create reusable workflows for consistency
- Enable debug logging when troubleshooting
For security best practices, see our GitHub Actions Security Guide.