Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

GitHub Actions

A powerful CI/CD platform integrated directly into GitHub repositories for automating build, test, and deployment workflows.

GitHub Actions Cheatsheet

A powerful CI/CD platform integrated directly into GitHub repositories for automating build, test, and deployment workflows.

Overview

GitHub Actions enables automation of software workflows directly from your GitHub repository. It uses YAML-based configuration files to define workflows that respond to events, run jobs on runners, and execute steps that can include marketplace actions or custom scripts.

RunnersGitHub Actions ArchitectureEvent TriggerWorkflowJob 1Job 2Job 3Step 1Step 2Step 3ActionRun CommandActionSteps...Steps...GitHub-hostedSelf-hostedRunnersGitHub Actions ArchitectureEvent TriggerWorkflowJob 1Job 2Job 3Step 1Step 2Step 3ActionRun CommandActionSteps...Steps...GitHub-hostedSelf-hosted

Workflow Syntax and Structure

Key Concepts

  • Workflow: Automated process defined in YAML, stored in .github/workflows/
  • Event: Trigger that starts a workflow (push, pull_request, schedule, etc.)
  • Job: Set of steps that execute on the same runner
  • Step: Individual task within a job (action or shell command)
  • Action: Reusable unit of code from marketplace or custom repository
  • Runner: Server that executes workflow jobs

Basic Workflow Structure

# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '20'

jobs:
  build:
    name: Build and Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

Workflow File Location

repository/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── cd.yml
│       └── scheduled-tasks.yml
└── src/

Common Triggers

Key Concepts

  • Push events: Triggered when code is pushed to specified branches
  • Pull request events: Triggered on PR actions (opened, synchronise, closed)
  • Schedule: Cron-based triggers for periodic execution
  • Manual triggers: workflow_dispatch for on-demand execution
  • Repository dispatch: API-triggered workflows
  • Workflow call: Reusable workflows triggered by other workflows

Common Patterns

on:
  # Push to specific branches
  push:
    branches:
      - main
      - 'releases/**'
    paths:
      - 'src/**'
      - '!src/**/*.md'
    tags:
      - 'v*'

  # Pull request events
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]
    paths-ignore:
      - '**.md'
      - 'docs/**'

  # Scheduled (cron syntax - UTC)
  schedule:
    - cron: '0 2 * * 1-5'  # Weekdays at 2 AM UTC
    - cron: '0 0 * * 0'    # Sundays at midnight

  # Manual trigger with inputs
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production
      debug_enabled:
        description: 'Enable debug logging'
        required: false
        type: boolean
        default: false

  # Trigger from another repository
  repository_dispatch:
    types: [deploy-trigger]

  # Release events
  release:
    types: [published, created]

  # Issue and PR comments
  issue_comment:
    types: [created]

Examples

Branch and Tag Filtering

on:
  push:
    branches:
      - main
      - 'feature/**'
      - '!feature/experimental-*'
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'

Conditional Workflow Based on Changed Files

on:
  push:
    paths:
      - 'backend/**'
      - 'shared/**'

jobs:
  build-backend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      # Build only backend when relevant files change

Job and Step Definitions

Key Concepts

  • Job dependencies: Define execution order with needs
  • Conditional execution: Use if expressions to control execution
  • Outputs: Pass data between jobs
  • Concurrency: Control parallel execution and cancellation
  • Timeout: Set maximum execution time
  • Continue on error: Allow workflow to continue despite failures

Job Configuration

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    timeout-minutes: 30

    # Job-level environment variables
    env:
      CI: true

    # Conditional execution
    if: github.event_name == 'push' || github.event.pull_request.draft == false

    steps:
      - uses: actions/checkout@v6
      - run: npm test

  build:
    name: Build Application
    runs-on: ubuntu-latest
    needs: test  # Wait for test job

    outputs:
      version: ${{ steps.version.outputs.value }}

    steps:
      - uses: actions/checkout@v6
      - id: version
        run: echo "value=$(cat VERSION)" >> $GITHUB_OUTPUT

  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: [test, build]  # Wait for multiple jobs

    # Use output from previous job
    env:
      APP_VERSION: ${{ needs.build.outputs.version }}

    steps:
      - run: echo "Deploying version $APP_VERSION"

Job Dependencies Diagram

Job DependencieslintdeploytestbuildJob Dependencieslintdeploytestbuild

Step Configuration

steps:
  # Using an action
  - name: Checkout repository
    uses: actions/checkout@v6
    with:
      fetch-depth: 0
      token: ${{ secrets.PAT }}

  # Running a command
  - name: Install dependencies
    run: npm ci
    working-directory: ./frontend
    shell: bash

  # Multi-line command
  - name: Build and test
    run: |
      npm run build
      npm run test:coverage
      npm run lint

  # Conditional step
  - name: Deploy to production
    if: github.ref == 'refs/heads/main' && success()
    run: ./deploy.sh

  # Continue on error
  - name: Run optional checks
    run: npm run optional-lint
    continue-on-error: true

  # Set environment variables for subsequent steps
  - name: Set build info
    run: |
      echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
      echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV

  # Use outputs
  - name: Get version
    id: get-version
    run: echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT

  - name: Use version
    run: echo "Version is ${{ steps.get-version.outputs.version }}"

Concurrency Control

# Cancel in-progress runs for same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

# Or per job
jobs:
  deploy:
    concurrency:
      group: deploy-${{ github.ref }}
      cancel-in-progress: false  # Don't cancel deploys

Using Actions from the Marketplace

Key Concepts

  • Official actions: Maintained by GitHub (actions/*)
  • Verified creators: Trusted third-party actions
  • Version pinning: Use specific versions for stability
  • Fork for security: Fork critical actions to your organisation

Common Actions

steps:
  # Checkout code
  - uses: actions/checkout@v6
    with:
      fetch-depth: 0
      submodules: recursive

  # Setup language runtimes
  - uses: actions/setup-node@v6
    with:
      node-version: '20'
      cache: 'npm'

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

  - uses: actions/setup-go@v6
    with:
      go-version: '1.22'

  # Docker actions
  - uses: docker/setup-buildx-action@v4
  - uses: docker/build-push-action@v7
    with:
      push: true
      tags: ghcr.io/${{ github.repository }}:latest

  # GitHub script for API calls
  - uses: actions/github-script@v8
    with:
      script: |
        await github.rest.issues.createComment({
          owner: context.repo.owner,
          repo: context.repo.repo,
          issue_number: context.issue.number,
          body: 'Build completed!'
        })

Using Actions from Different Sources

steps:
  # Public action from marketplace
  - uses: actions/checkout@v6

  # Action from a different repository
  - uses: owner/repo@v1

  # Action from a specific branch
  - uses: owner/repo@main

  # Action from a specific commit SHA (most secure)
  - uses: owner/repo@a1b2c3d4e5f6

  # Local action in same repository
  - uses: ./.github/actions/my-action

  # Docker Hub action
  - uses: docker://alpine:3.23

Creating a Local Composite Action

# .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'

outputs:
  cache-hit:
    description: 'Whether cache was hit'
    value: ${{ steps.cache.outputs.cache-hit }}

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v6
      with:
        node-version: ${{ inputs.node-version }}

    - name: Cache dependencies
      id: cache
      uses: actions/cache@v5
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}

    - name: Install dependencies
      if: steps.cache.outputs.cache-hit != 'true'
      run: npm ci
      shell: bash

Secrets and Environment Variables

Key Concepts

  • Secrets: Encrypted values for sensitive data (tokens, passwords)
  • Variables: Non-sensitive configuration values
  • Environment-specific: Secrets/variables scoped to deployment environments
  • Masking: Automatic masking of secret values in logs

Configuration Levels

# Repository-level environment variable
env:
  GLOBAL_VAR: 'available-to-all-jobs'

jobs:
  build:
    runs-on: ubuntu-latest

    # Job-level environment variable
    env:
      JOB_VAR: 'available-to-all-steps'

    steps:
      - name: Use variables
        # Step-level environment variable
        env:
          STEP_VAR: 'only-this-step'
          SECRET_TOKEN: ${{ secrets.API_TOKEN }}
        run: |
          echo "Global: $GLOBAL_VAR"
          echo "Job: $JOB_VAR"
          echo "Step: $STEP_VAR"

Using Secrets

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Login to registry
        env:
          REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: echo "$REGISTRY_TOKEN" | docker login -u user --password-stdin

      - name: Deploy with token
        run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

      # GitHub token (automatically provided)
      - name: Create release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh release create v1.0.0

Deployment Environments

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: ./deploy.sh
        env:
          API_URL: ${{ vars.API_URL }}  # Environment variable
          API_KEY: ${{ secrets.API_KEY }}  # Environment secret

  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://myapp.com
    steps:
      - run: ./deploy.sh
        env:
          API_URL: ${{ vars.API_URL }}
          API_KEY: ${{ secrets.API_KEY }}

Dynamic Secrets Masking

steps:
  - name: Generate token
    id: token
    run: |
      TOKEN=$(./generate-token.sh)
      echo "::add-mask::$TOKEN"
      echo "token=$TOKEN" >> $GITHUB_OUTPUT

  - name: Use token
    run: curl -H "Authorization: Bearer ${{ steps.token.outputs.token }}" https://api.example.com

Matrix Builds

Key Concepts

  • Matrix strategy: Run job with multiple configurations in parallel
  • Include/exclude: Add or remove specific combinations
  • Fail-fast: Control whether to cancel other jobs on failure
  • Max-parallel: Limit concurrent jobs

Basic Matrix

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [22, 24]
        os: [ubuntu-latest, windows-latest, macos-latest]

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

Advanced Matrix Configuration

jobs:
  test:
    strategy:
      fail-fast: false  # Continue other jobs if one fails
      max-parallel: 4   # Limit concurrent jobs
      matrix:
        os: [ubuntu-latest, windows-latest]
        python: ['3.10', '3.11', '3.12']

        # Include additional configurations
        include:
          - os: ubuntu-latest
            python: '3.12'
            coverage: true
          - os: macos-latest
            python: '3.12'

        # Exclude specific combinations
        exclude:
          - os: windows-latest
            python: '3.10'

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python }}

      - name: Run tests
        run: pytest

      - name: Upload coverage
        if: matrix.coverage
        run: ./upload-coverage.sh

Dynamic Matrix

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - id: set-matrix
        run: |
          # Generate matrix from files or API
          echo 'matrix={"service":["api","web","worker"]}' >> $GITHUB_OUTPUT

  build:
    needs: prepare
    strategy:
      matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building ${{ matrix.service }}"

Caching Dependencies

Key Concepts

  • Cache action: Store and restore files between workflow runs
  • Cache key: Unique identifier for cache entries
  • Restore keys: Fallback keys for partial matches
  • Cache scope: Branch-level isolation with fallback to default branch

Common Caching Patterns

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      # Node.js with npm
      - uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'  # Built-in caching

      # Manual cache for custom paths
      - name: Cache node modules
        uses: actions/cache@v5
        with:
          path: |
            node_modules
            ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      # Python with pip
      - uses: actions/cache@v5
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      # Go modules
      - uses: actions/cache@v5
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

      # Gradle
      - uses: actions/cache@v5
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

Conditional Caching

steps:
  - name: Cache dependencies
    id: cache
    uses: actions/cache@v5
    with:
      path: node_modules
      key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}

  - name: Install dependencies
    if: steps.cache.outputs.cache-hit != 'true'
    run: npm ci

Cache Limits and Best Practices

# Good: Specific, content-based keys
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

# Good: Multiple restore keys for fallback
restore-keys: |
  ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
  ${{ runner.os }}-node-
  ${{ runner.os }}-

# Avoid: Generic keys that rarely change
key: ${{ runner.os }}-deps  # Bad - won't invalidate properly

Artifacts Management

Key Concepts

  • Artifacts: Files preserved after workflow completion
  • Retention: Configurable storage duration (default 90 days)
  • Upload/download: Share files between jobs or for later retrieval
  • Compression: Automatic compression for efficiency

Uploading Artifacts

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Build
        run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v7
        with:
          name: build-output
          path: |
            dist/
            !dist/**/*.map
          retention-days: 7

      - name: Upload test results
        if: always()  # Upload even on failure
        uses: actions/upload-artifact@v7
        with:
          name: test-results
          path: test-results/
          if-no-files-found: warn  # warn, error, or ignore

Downloading Artifacts

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      artifact-name: ${{ steps.artifact.outputs.name }}
    steps:
      - run: npm run build
      - uses: actions/upload-artifact@v7
        id: artifact
        with:
          name: build-${{ github.sha }}
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v8
        with:
          name: build-${{ github.sha }}
          path: dist/

      - name: Deploy
        run: ./deploy.sh dist/

Sharing Artifacts Between Jobs

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: npm run build
      - uses: actions/upload-artifact@v7
        with:
          name: app
          path: dist/

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: app
      - run: npm test

  deploy:
    needs: [build, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: app
      - run: ./deploy.sh

Artifacts Flow Diagram

Artifacts FlowuploaddownloaddownloadBuild JobArtifacts StorageTest JobDeploy JobArtifacts FlowuploaddownloaddownloadBuild JobArtifacts StorageTest JobDeploy Job

GHCR (GitHub Container Registry)

Key Concepts

  • GHCR: GitHub's container registry at ghcr.io
  • Authentication: Use GITHUB_TOKEN or PAT
  • Visibility: Public or private packages
  • Permissions: Linked to repository or organisation

Building and Pushing to GHCR

name: Build and Push Container

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

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v6

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Login to GHCR
        uses: docker/login-action@v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Multi-Architecture Builds

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      packages: write

    steps:
      - uses: actions/checkout@v6

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Login to GHCR
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

Using GHCR Images in Workflows

jobs:
  test:
    runs-on: ubuntu-latest

    # Use container from GHCR
    container:
      image: ghcr.io/owner/custom-runner:latest
      credentials:
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    services:
      postgres:
        image: ghcr.io/owner/postgres-test:latest
        credentials:
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
        env:
          POSTGRES_PASSWORD: test

    steps:
      - uses: actions/checkout@v6
      - run: npm test

Best Practices for CI/CD Pipelines

Security Best Practices

# Pin actions to full SHA for security
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3  # v6.0.0

# Minimal permissions
permissions:
  contents: read

jobs:
  deploy:
    permissions:
      contents: read
      packages: write

# Use environments for production deployments
jobs:
  deploy:
    environment:
      name: production
      url: https://myapp.com
    # Requires manual approval if configured

Performance Optimisation

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # Shallow clone for faster checkout
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1

      # Use caching
      - uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'

      # Parallelise where possible
      - name: Run tests
        run: npm test -- --parallel

Reusable Workflows

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      DEPLOY_KEY:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v6
      - run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

# Calling workflow
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
    secrets:
      DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}

Workflow Organisation

# Use meaningful names
name: CI - Build, Test, and Lint

# Group related jobs
jobs:
  lint:
    name: Code Quality
    # ...

  test:
    name: Unit Tests
    # ...

  integration:
    name: Integration Tests
    needs: [lint, test]
    # ...

  deploy:
    name: Deploy to Production
    needs: integration
    if: github.ref == 'refs/heads/main'
    # ...

Troubleshooting Common Issues

Key Concepts

  • Workflow logs: Detailed execution logs in Actions tab
  • Debug logging: Enhanced logging with secrets
  • Status checks: Required checks for branch protection
  • API limits: Rate limiting for GitHub API calls

Common Issues and Solutions

Workflow Not Triggering

# Check: Workflow file must be on default branch for some events
# Check: Paths filter might be excluding your changes
# Check: Branch protection might require status checks

# Debug: Use workflow_dispatch to test manually
on:
  push:
    branches: [main]
  workflow_dispatch:  # Add for manual testing

Permission Denied Errors

# Solution: Add explicit permissions
permissions:
  contents: read
  packages: write
  issues: write
  pull-requests: write

# For GITHUB_TOKEN in forks
# Note: Secrets aren't available in PRs from forks by default

Cache Not Restoring

# Check key matches exactly
- uses: actions/cache@v5
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    # Ensure lockfile exists and is committed

# Debug: Check cache-hit output
    id: cache
- run: echo "Cache hit: ${{ steps.cache.outputs.cache-hit }}"

Job Timeout

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 60  # Reduce from default 360 to fail fast

    steps:
      - name: Long running step
        timeout-minutes: 30  # Step-level timeout
        run: ./long-process.sh

Secrets Not Available

# Secrets aren't available in:
# - Forked PRs (security)
# - Reusable workflows (unless passed explicitly)
# - workflow_run triggered workflows

# Solution for reusable workflows:
on:
  workflow_call:
    secrets:
      MY_SECRET:
        required: true

Debugging Workflows

Enable Debug Logging

# Set repository secrets:
# ACTIONS_RUNNER_DEBUG = true
# ACTIONS_STEP_DEBUG = true

# Or use workflow_dispatch input
on:
  workflow_dispatch:
    inputs:
      debug:
        type: boolean
        default: false

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      ACTIONS_STEP_DEBUG: ${{ inputs.debug }}

Debugging Techniques

steps:
  # Print context information
  - name: Dump GitHub context
    env:
      GITHUB_CONTEXT: ${{ toJson(github) }}
    run: echo "$GITHUB_CONTEXT"

  # Print all environment variables
  - name: Print environment
    run: env | sort

  # Interactive debugging with tmate
  - name: Setup tmate session
    if: failure()
    uses: mxschmitt/action-tmate@v3
    with:
      limit-access-to-actor: true

  # Add continue-on-error for investigation
  - name: Failing step
    continue-on-error: true
    run: ./might-fail.sh

  - name: Check previous result
    run: echo "Previous step outcome: ${{ steps.previous.outcome }}"

Workflow Run Logs

# Download logs via API
- name: Download logs
  run: |
    gh run view ${{ github.run_id }} --log
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Common Debug Scenarios

# Debug: Check if file exists
- run: |
    if [ -f "package-lock.json" ]; then
      echo "File exists"
      ls -la package-lock.json
    else
      echo "File not found"
      ls -la
    fi

# Debug: Check working directory
- run: |
    echo "Current directory: $(pwd)"
    echo "Contents:"
    ls -la

# Debug: Network connectivity
- run: |
    curl -v https://registry.npmjs.org

# Debug: Check installed tools
- run: |
    node --version
    npm --version
    which node

Using Self-Hosted Runners

Key Concepts

  • Self-hosted runners: Your own machines running GitHub Actions jobs
  • Labels: Identify runner capabilities and target specific runners
  • Runner groups: Organise and control access to runners
  • Security: Ensure proper isolation and access control

Registering a Self-Hosted Runner

# Download and configure runner
# (check Settings → Actions → Runners → New runner for the current version and command)
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.334.0.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.334.0/actions-runner-linux-x64-2.334.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.334.0.tar.gz

# Configure
./config.sh --url https://github.com/owner/repo --token TOKEN

# Run
./run.sh

# Or install as service
sudo ./svc.sh install
sudo ./svc.sh start

Using Self-Hosted Runners in Workflows

jobs:
  build:
    # Use self-hosted runner
    runs-on: self-hosted

    steps:
      - uses: actions/checkout@v6
      - run: ./build.sh

  gpu-training:
    # Use runner with specific labels
    runs-on: [self-hosted, linux, gpu]

    steps:
      - uses: actions/checkout@v6
      - run: python train.py

  macos-build:
    # Multiple labels for precise targeting
    runs-on: [self-hosted, macOS, ARM64]

    steps:
      - uses: actions/checkout@v6
      - run: xcodebuild

Runner Labels and Groups

jobs:
  # Target specific runner group
  deploy:
    runs-on:
      group: production-runners
      labels: [self-hosted, linux]

    steps:
      - run: ./deploy.sh

Self-Hosted Runner Security

# Limit to specific repositories
# Configure in: Settings > Actions > Runner groups

# Use ephemeral runners for better isolation
# ./config.sh --ephemeral

# Run in container for isolation
jobs:
  build:
    runs-on: self-hosted
    container:
      image: node:20
    steps:
      - uses: actions/checkout@v6
      - run: npm test

Auto-Scaling Runners

# Using Actions Runner Controller (ARC) for Kubernetes
# Deploy runner scale set that auto-scales based on demand

# Example workflow targeting ARC runners
jobs:
  build:
    runs-on: arc-runner-set
    steps:
      - uses: actions/checkout@v6
      - run: ./build.sh

Quick Reference

Component Syntax Description
Workflow file .github/workflows/*.yml Location for workflow definitions
Trigger on push on: push Run on code push
Trigger on PR on: pull_request Run on pull request
Manual trigger on: workflow_dispatch Allow manual execution
Job definition jobs: <name>: Define a job
Runner runs-on: ubuntu-latest Specify execution environment
Use action uses: actions/checkout@v6 Use marketplace action
Run command run: npm test Execute shell command
Environment variable env: KEY: value Set environment variable
Secret ${{ secrets.NAME }} Access secret value
Matrix strategy: matrix: Run multiple configurations
Cache actions/cache@v5 Cache dependencies
Artifact upload actions/upload-artifact@v7 Store workflow output
Artifact download actions/download-artifact@v8 Retrieve stored output
Job dependency needs: [job1, job2] Wait for jobs to complete
Conditional if: github.ref == 'refs/heads/main' Conditional execution
Output echo "key=value" >> $GITHUB_OUTPUT Set step output
Set env var echo "KEY=value" >> $GITHUB_ENV Set for subsequent steps
Permissions permissions: contents: read Set GITHUB_TOKEN scope
Concurrency concurrency: group: ${{ github.ref }} Control parallel runs
Timeout timeout-minutes: 30 Set maximum run time

Common Issues and Solutions

Issue Cause Solution
Workflow not running File not on default branch Merge workflow file to main/master first
Permission denied Insufficient GITHUB_TOKEN permissions Add explicit permissions: block
Cache miss Key doesn't match Check hashFiles() path and file existence
Secret is empty Wrong secret name or scope Verify secret exists in correct scope
Resource not accessible Missing permissions Add required permission (issues, packages, etc.)
Job stuck in queue No available runners Check runner status and labels
Matrix job failing Invalid combination Use exclude: to remove problematic combos
Artifact not found Wrong name or expired Check artifact name and retention period
Docker login fails Invalid credentials Verify GITHUB_TOKEN has packages: write
Workflow cancelled Concurrency settings Adjust cancel-in-progress setting
Step timeout Default timeout exceeded Add explicit timeout-minutes
Fork PR secrets empty Security restriction Expected behaviour; use pull_request_target carefully
Self-hosted runner offline Runner service stopped Restart runner service
Rate limit exceeded Too many API calls Add delays or reduce API usage
Bad credentials Expired or invalid token Regenerate PAT or check GITHUB_TOKEN scope

Related Topics

The following topics would complement this GitHub Actions cheatsheet:

  1. Docker and Container Basics - Understanding containerisation is essential for building and deploying container images with GitHub Actions

  2. YAML Syntax and Best Practices - Deep knowledge of YAML helps write cleaner, more maintainable workflow files

  3. Git Branching Strategies - Understanding branching models (GitFlow, trunk-based) helps design effective CI/CD triggers

  4. Shell Scripting (Bash) - Most workflow steps involve shell commands; strong scripting skills improve automation

  5. Kubernetes Deployments - Many CI/CD pipelines deploy to Kubernetes; understanding K8s concepts enables better deployment workflows

  6. Security Best Practices for CI/CD - Covers secrets management, supply chain security, and secure deployment patterns