GitHub Actions
Workflow Structure
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
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 test
Common Triggers
on:
push:
branches: [main, develop]
paths-ignore:
- "**.md"
- "docs/**"
pull_request:
branches: [main]
schedule:
- cron: "0 2 * * 1" # Monday 2am UTC
workflow_dispatch:
inputs:
environment:
description: "Deploy target"
required: true
type: choice
options: [staging, production]
release:
types: [published]
Matrix Builds
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Caching
# Automatic npm caching via setup-node
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
# Manual cache
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
Reusable Workflows
# .github/workflows/reusable-test.yml
name: Reusable Test
on:
workflow_call:
inputs:
node-version:
type: string
default: "20"
secrets:
NPM_TOKEN:
required: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm test
# Caller workflow
jobs:
test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Composite Actions
# .github/actions/setup-project/action.yml
name: Setup Project
description: Install deps and cache
inputs:
node-version:
default: "20"
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
shell: bash
Secrets and Environment Variables
steps:
- run: echo "Deploying..."
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Conditional Steps
steps:
- uses: actions/checkout@v4
- name: Lint
if: github.event_name == 'pull_request'
run: npm run lint
- name: Deploy
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: npm run deploy
Job Dependencies
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Linting..."
test:
needs: lint
runs-on: ubuntu-latest
steps:
- run: echo "Testing..."
deploy:
needs: [lint, test]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: echo "Deploying..."
Artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
Environments
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "Deploy to staging..."
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- run: echo "Deploy to production..."
Useful Patterns
# Skip duplicate runs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Auto-merge dependabot PRs
- if: github.actor == 'dependabot[bot]'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}