A practical reference for building CI/CD pipelines with GitHub Actions.
Workflow Basics
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Hello"runs-on Values
runs-on: ubuntu-latest # Latest Ubuntu
runs-on: ubuntu-24.04 # Pin version
runs-on: windows-latest # Windows Server
runs-on: macos-latest # macOS ARM
runs-on: [self-hosted, linux, x64] # Self-hostedTriggers
on:
push:
branches: [main, develop]
tags: ['v*']
paths: ['src/**']
paths-ignore: ['docs/**', '*.md']
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
env:
type: choice
options: [staging, production]
required: true
schedule:
- cron: '0 6 * * 1' # Mon 06:00 UTC
release:
types: [published]Matrix Builds
jobs:
test:
strategy:
matrix:
node: [16, 18, 20]
os: [ubuntu-latest, windows-latest]
exclude:
- os: windows-latest
node: 16
include:
- os: macos-latest
node: 20
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm testStrategy Options
strategy:
fail-fast: false # Continue on failure
max-parallel: 3 # Limit concurrencyCommon Actions
Checkout
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0 # Full history
fetch-tags: true
lfs: true
submodules: recursive
token: ${{ secrets.GH_PAT }}
sparse-checkout: |
src/
package.jsonSetup Node / Python
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version-file: '.nvmrc'
cache: 'npm'
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'Cache Action
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-Secrets & Environment Variables
steps:
- run: echo "Deploy to ${{ secrets.HOST }}"
env:
API_KEY: ${{ secrets.API_KEY }}
REGION: ${{ vars.AWS_REGION }}
jobs:
deploy-prod:
environment: production # Requires approval
env:
URL: https://example.com
# Permissions:
permissions:
contents: read
pull-requests: write
packages: write
id-token: writeBuilt-in Context
- run: |
echo "Branch: ${{ github.ref_name }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Run ID: ${{ github.run_id }}"Artifacts
- uses: actions/upload-artifact@v4
with:
name: build-${{ github.sha }}
path: dist/
retention-days: 5
- uses: actions/download-artifact@v4
with:
pattern: build-*
path: ./artifacts
merge-multiple: trueDeploy to Cloudflare Pages
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-projectContainers in Jobs
Service Containers
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379Self-Hosted Runners
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64.tar.gz \
-L https://github.com/actions/runner/releases/download/v2.317.0/actions-runner-linux-x64-2.317.0.tar.gz
tar xzf actions-runner-linux-x64.tar.gz
./config.sh --url https://github.com/user/repo --token TOKEN --labels gpu,large
sudo ./svc.sh install && sudo ./svc.sh start# Ephemeral — self-destructs after one job
# ./config.sh --url ... --token ... --ephemeral
# Cleanup step
- if: runner.environment == 'self-hosted'
run: sudo rm -rf /tmp/*Conditional Steps
steps:
- if: github.ref == 'refs/heads/main'
run: echo "Deploying"
- if: failure()
run: echo "Failed"
- if: always()
run: echo "Cleanup"
- if: cancelled()
run: echo "Cancelled"
- continue-on-error: true
run: npm run lintReusable Workflows & Job Outputs
Caller Workflow
jobs:
ci:
uses: ./.github/workflows/test.yml
with:
node-version: 20
secrets: inherit
deploy:
needs: ci
uses: org/shared/.github/workflows/deploy.yml@v1
with:
environment: production
secrets:
CLOUDFLARE_TOKEN: ${{ secrets.CF_TOKEN }}Job Outputs & Dependencies
jobs:
version:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.v.outputs.tag }}
steps:
- id: v
run: echo "tag=v${{ github.run_number }}" >> $GITHUB_OUTPUT
release:
needs: [version, build]
env:
TAG: ${{ needs.version.outputs.tag }}
steps:
- run: echo "Releasing $TAG"
notify:
needs: [release]
if: always()
steps:
- run: echo "Done"Debugging
ACT (Local Workflow Runner)
brew install act # macOS
act # Run push workflow
act pull_request # Run PR workflow
act -j build # Specific job
act -s SECRET=*** # With secrets
act --debug # Debug outputtmate & Logging
- uses: mxschmitt/action-tmate@v3
if: failure()
with:
limit-access-to-actor: true- run: |
echo "::group::Install"
npm ci
echo "::endgroup::"
echo "::debug::debug message"Validation
yamllint .github/workflows/*.yml
actionlint .github/workflows/ci.yml
# Install: go install github.com/rhysd/actionlint/cmd/actionlint@latestStatus Badge
[](...)