Back to Cheatsheets
Cheatsheet

GitHub Actions CI/CD Cheatsheet

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-hosted

Triggers

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 test

Strategy Options

strategy:
  fail-fast: false                           # Continue on failure
  max-parallel: 3                            # Limit concurrency

Common 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.json

Setup 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: write

Built-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: true

Deploy 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-project

Containers 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:6379

Self-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 lint

Reusable 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 output

tmate & 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@latest

Status Badge

[![CI](https://github.com/user/repo/actions/workflows/ci.yml/badge.svg)](...)