Toolmingo
Guides7 min read

GitHub Actions Cheat Sheet: CI/CD Workflows Explained

A complete GitHub Actions reference — workflow syntax, triggers, jobs, secrets, matrix builds, caching, artifacts, and ready-to-copy examples for Node.js, Python, Docker, and more.

GitHub Actions automates your build, test, and deploy pipelines directly in your repository. This reference covers the full workflow syntax with copy-ready examples.

Quick reference

Concept Syntax
Trigger on push on: push
Trigger on PR on: pull_request
Scheduled run on: schedule: - cron: '0 9 * * 1'
Manual trigger on: workflow_dispatch
Use an action uses: actions/checkout@v4
Run a command run: npm test
Set env var env: NODE_ENV: test
Use a secret ${{ secrets.MY_SECRET }}
Job output ${{ needs.build.outputs.version }}
Matrix build strategy: matrix: node: [18, 20, 22]
Upload artifact uses: actions/upload-artifact@v4
Cache deps uses: actions/cache@v4

Workflow file anatomy

Every workflow is a YAML file in .github/workflows/.

name: CI                        # Workflow name (shown in UI)

on:                             # What triggers the workflow
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:                           # One or more jobs (run in parallel by default)
  test:                         # Job ID (your name)
    runs-on: ubuntu-latest      # Runner OS

    steps:                      # Ordered list of steps
      - uses: actions/checkout@v4         # Check out source code

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

Triggers (on)

Common triggers

on:
  push:
    branches: [main]
    paths:
      - 'src/**'          # Only trigger when src/ changes
      - '!src/**/*.md'    # Ignore markdown changes

  pull_request:
    types: [opened, synchronize, reopened]
    branches: [main]

  schedule:
    - cron: '0 9 * * 1'   # Every Monday at 9am UTC

  workflow_dispatch:        # Manual trigger with optional inputs
    inputs:
      environment:
        description: 'Deploy target'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]

  workflow_call:            # Called by another workflow (reusable)
    inputs:
      version:
        type: string
        required: true

Trigger shorthand

on: [push, pull_request]   # Multiple events, simple form

Jobs

Sequential jobs with needs

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."

  test:
    runs-on: ubuntu-latest
    needs: build            # Runs after build completes
    steps:
      - run: echo "Testing..."

  deploy:
    runs-on: ubuntu-latest
    needs: [build, test]    # Runs after both complete
    if: github.ref == 'refs/heads/main'   # Conditional job
    steps:
      - run: echo "Deploying..."

Job outputs

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get-version.outputs.value }}
    steps:
      - id: get-version
        run: echo "value=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying version ${{ needs.build.outputs.version }}"

Contexts and expressions

# Contexts available in expressions ${{ }}
${{ github.event_name }}     # Event that triggered the workflow
${{ github.ref }}            # Branch/tag ref (refs/heads/main)
${{ github.sha }}            # Commit SHA
${{ github.actor }}          # User who triggered the workflow
${{ github.repository }}     # owner/repo
${{ github.workspace }}      # Path to checked-out repo
${{ runner.os }}             # Linux, macOS, Windows
${{ env.MY_VAR }}            # Environment variable
${{ secrets.MY_SECRET }}     # Secret value
${{ vars.MY_VAR }}           # Repository variable (non-secret)
${{ job.status }}            # success, failure, cancelled
${{ steps.STEP_ID.outputs.VALUE }}   # Step output
${{ needs.JOB_ID.outputs.VALUE }}    # Job output

Conditionals

steps:
  - name: Deploy to production
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: ./deploy.sh

  - name: Notify on failure
    if: failure()            # Built-ins: success(), failure(), always(), cancelled()
    run: ./notify.sh

  - name: Skip draft PRs
    if: github.event.pull_request.draft == false
    run: npm test

Secrets and variables

# Repository secrets (Settings → Secrets and variables → Actions)
steps:
  - name: Deploy
    env:
      API_KEY: ${{ secrets.API_KEY }}
      DB_URL: ${{ secrets.DATABASE_URL }}
    run: ./deploy.sh

# Repository variables (non-secret, visible in logs)
  - name: Set environment
    env:
      APP_URL: ${{ vars.APP_URL }}
    run: echo "Deploying to $APP_URL"

# Environment secrets (scoped to deployment environments)
  - name: Production deploy
    environment: production    # Environment must be configured in repo settings
    env:
      PROD_KEY: ${{ secrets.PROD_KEY }}
    run: ./deploy-prod.sh

# Set an env var for subsequent steps
  - name: Set version
    run: echo "VERSION=1.2.3" >> $GITHUB_ENV

  - name: Use version
    run: echo "Building $VERSION"    # Available in next steps

Matrix builds

Run the same job across multiple configurations:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false        # Don't cancel others if one fails
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [18, 20, 22]
        exclude:
          - os: macos-latest
            node: 18          # Skip this combination
        include:
          - os: ubuntu-latest
            node: 22
            experimental: true   # Add extra data to specific combo

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test

Caching dependencies

# Node.js — cache node_modules
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

# Python — cache pip
- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}

# Setup actions have built-in caching (preferred)
- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'            # Automatically caches npm

- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

Artifacts

Share files between jobs or download after a run:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: npm run build

      - uses: actions/upload-artifact@v4
        with:
          name: dist-files
          path: dist/
          retention-days: 7     # Default: 90 days

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist-files
          path: dist/

      - run: ./deploy.sh dist/

Common workflow recipes

Node.js CI

name: Node.js CI

on: [push, pull_request]

jobs:
  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 run build --if-present
      - run: npm test

Python CI

name: Python CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'

      - run: pip install -r requirements.txt
      - run: pip install pytest
      - run: pytest

Docker build and push

name: Docker Build

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: myuser/myapp
          tags: |
            type=semver,pattern={{version}}
            type=sha

      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Deploy only on main

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    concurrency:
      group: production
      cancel-in-progress: false    # Never cancel a deploy in progress

    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

Reusable workflows

Define once, call from multiple workflows:

# .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'

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 && npm test
# .github/workflows/ci.yml — calls the reusable workflow
jobs:
  call-test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '22'
    secrets: inherit    # Pass all secrets through

Common mistakes

Mistake Problem Fix
uses: actions/checkout (no version) Breaking changes with @main Always pin: @v4
Storing secrets in env at top level Secrets leak into all jobs/steps Scope env to the step that needs it
run: npm install instead of npm ci Non-reproducible installs Use npm ci in CI
No fail-fast: false in matrix One failure cancels all matrix jobs Add fail-fast: false
if: always() on deploy step Deploys even when tests fail Use if: success() or omit (default)
Hardcoded branch names in conditions Breaks on branch renames Use github.event.repository.default_branch
Large artifacts without retention-days Storage costs accumulate Set retention-days: 7 for temp files

FAQ

How do I skip a workflow run?
Include [skip ci] or [ci skip] in your commit message. GitHub also has native skip: add skip-checks: true to the commit.

How do I run a step only on failure?
Use if: failure(). Steps run with success() by default. For cleanup that always runs: if: always().

Can I pass secrets to reusable workflows?
Yes — add secrets: inherit to pass all secrets, or list them individually under secrets:.

How do I debug a failing workflow?
Enable debug logging by setting secret ACTIONS_STEP_DEBUG to true in your repository. Also try tmate for an interactive SSH session.

What is $GITHUB_OUTPUT vs set-output?
set-output (old) was deprecated. Use echo "key=value" >> $GITHUB_OUTPUT instead. Same for env vars: echo "VAR=value" >> $GITHUB_ENV.

How do I run a workflow manually with parameters?
Use on: workflow_dispatch with inputs. A "Run workflow" button appears in the Actions tab. Parameters are available via ${{ github.event.inputs.PARAM_NAME }}.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools