GitHub Actions — Complete Guide

Comprehensive reference for building CI/CD pipelines with GitHub Actions.

Why / When to Use

Use when automating build, test, deploy, or any recurring workflow on a GitHub-hosted repository. Supports GitHub-hosted and self-hosted runners — the latter useful for accessing local LLM proxies (LiteLLM, DeepSeek).

Core Concepts

Repository layout:

.github/
└── workflows/
    ├── ci.yml        ← each .yml file is one workflow
    ├── deploy.yml
    └── release.yml
TermMeaning
WorkflowAutomated process defined in a .yml file
Trigger (on)What starts the workflow
JobGroup of steps running on the same machine
StepIndividual command or action
ActionReusable unit (marketplace or custom)
RunnerMachine that executes jobs

Core Concept / Commands

1. Basic CI — test on every push

# .github/workflows/ci.yml
name: CI
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm install
      - run: npm test

2. Multiple jobs with dependencies

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install && npm test
 
  build:
    runs-on: ubuntu-latest
    needs: test          # only runs if test passes
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
 
  deploy:
    runs-on: ubuntu-latest
    needs: build         # only runs if build passes
    steps:
      - run: echo "Deploying..."

3. Using secrets

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
        run: ./deploy.sh

Add secrets: Repo → Settings → Secrets and variables → Actions

4. Matrix strategy — test multiple versions

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm install && npm test

Runs 9 jobs (3 OS × 3 Node versions) in parallel.

5. Docker build and push

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: myuser/myapp:latest

6. Scheduled jobs (cron)

on:
  schedule:
    - cron: '0 2 * * *'   # every day at 2am UTC
  workflow_dispatch:         # also allow manual trigger

7. Conditional steps

steps:
  - run: npm test
 
  - name: Notify on failure
    if: failure()
    run: curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d '{"text":"Build failed!"}'
 
  - name: Deploy only on main
    if: github.ref == 'refs/heads/main'
    run: npm run deploy
 
  - name: Skip on draft PR
    if: github.event.pull_request.draft == false
    run: npm run heavy-tests

8. Reusable workflow

# .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm test
# Caller workflow
jobs:
  call-test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '20'

9. Self-hosted runner

jobs:
  build:
    runs-on: self-hosted     # uses your own machine
    steps:
      - uses: actions/checkout@v4
      - run: npm install && npm test

Setup:

# Repo → Settings → Actions → Runners → New self-hosted runner
./config.sh --url https://github.com/user/repo --token YOUR_TOKEN
./run.sh

Key use case: self-hosted runner can call a local LiteLLM/DeepSeek proxy directly, enabling AI-powered CI steps without egressing tokens externally.

10. Full real-world pipeline

name: Full Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
env:
  NODE_VERSION: '20'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
      - run: npm ci && npm run lint
 
  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
      - run: npm ci && npm test
 
  build-docker:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
 
  deploy:
    runs-on: self-hosted
    needs: build-docker
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Pull and restart
        run: |
          docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          docker compose up -d

Key Options / Variants

SyntaxMeaning
on: pushTrigger on push
needs: jobnameWait for another job
if: failure()Run only on failure
${{ secrets.X }}Use a secret
${{ github.ref }}Current branch ref
runs-on: self-hostedUse your own machine
strategy.matrixRun multiple combinations
workflow_dispatchAllow manual trigger

Gotchas

  • workflow_dispatch is required for manual runs from the GitHub UI
  • Matrix jobs run in parallel — add max-parallel to throttle if needed
  • GITHUB_TOKEN is auto-generated per workflow run; no manual secret needed for GHCR pushes
  • Self-hosted runners persist state between runs (node_modules, Docker cache) — useful for speed but can cause stale-cache bugs

Source

Conversation “Multiple GitHub repositories in one workspace” — 2026-05-19 (Claude Code project)