Skip to content

GitHub Actions

CI/CD and automation built into GitHub. Push code → Actions runs lint, tests, builds, deploys, or any script — driven by YAML files in .github/workflows/.

Table of Contents

Installation / Setup

No install — Actions is a GitHub feature. Create a workflow file:

mkdir -p .github/workflows
# add .github/workflows/ci.yml, commit, push

Runners: ubuntu-latest (Linux), windows-latest, macos-latest; larger/GPU runners are paid options.

Core Syntax

Workflow anatomy

name: CI                          # shown in the Actions tab
on:                               # triggers
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:              # manual "Run workflow" button

jobs:
  test:                           # job name (runs on one runner)
    runs-on: ubuntu-latest
    steps:                        # ordered commands
      - uses: actions/checkout@v4           # official action (clone repo)
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements-dev.txt
      - run: pytest
Element Meaning
on what triggers the run (push, PR, schedule, manual)
jobs independent units; parallel by default
steps commands/actions inside a job
uses reuse a published action (owner/repo@ref)
run shell command (default bash -e)
env / with inputs for steps
needs job ordering (needs: test)
if conditional execution

Triggers

on:
  push: { branches: [main] }        # any push to main
  pull_request: { types: [opened, synchronize] }
  schedule: [ { cron: "0 3 * * 1" } ]   # Monday 03:00 UTC
  workflow_dispatch: {}
  workflow_call: {}                 # reusable workflow
  release: { types: [published] }

Contexts & secrets

steps:
  - run: echo "ref=${{ github.ref }} sha=${{ github.sha }}"
  - run: deploy.sh ${{ github.ref_name }}
    env:
      API_TOKEN: ${{ secrets.API_TOKEN }}    # from repo Settings → Secrets
      VAR: ${{ vars.MY_VAR }}                # non-secret variables

Common Operations

Caching (deps stay fast)

- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${{ hashFiles('requirements.txt') }}
    restore-keys: pip-

Modern setup-python/setup-uv cache automatically (cache: pip / enable-cache: true).

Matrix builds (many configs, one job)

jobs:
  test:
    strategy:
      matrix:
        python-version: ["3.11", "3.12"]
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: ${{ matrix.python-version }} }
      - run: pytest

Upload/download artifacts

- uses: actions/upload-artifact@v4
  with:
    name: metrics
    path: reports/
- uses: actions/download-artifact@v4
  with:
    name: metrics

Publish a container image to GHCR

jobs:
  push:
    runs-on: ubuntu-latest
    permissions: { contents: read, packages: write }
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
      - run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

GITHUB_TOKEN is auto-created per run — no manual secret needed for GHCR.

Practical Examples

1. Lint + typecheck + test (the daily driver)

name: python-checks
on: [push, pull_request]
jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12", cache: pip }
      - run: pip install -e ".[dev]"
      - run: ruff check .
      - run: ruff format --check .
      - run: mypy src
      - run: pytest -q

2. Train-on-schedule with artifact upload

name: nightly-retrain
on:
  schedule: [{ cron: "0 2 * * *" }]
  workflow_dispatch: {}
jobs:
  train:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: python train.py --epochs 10
      - uses: actions/upload-artifact@v4
        with:
          name: model-ckpt
          path: artifacts/

3. Docker Compose test services

jobs:
  integration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_PASSWORD: postgres }
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s
    steps:
      - uses: actions/checkout@v4
      - run: pytest -m integration

Common Patterns

# Stop stale runs (only newest per branch)
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

# Reusable workflow
jobs:
  call:
    uses: ./.github/workflows/_tests.yml   # with: python-version: "3.12"

# Conditional steps
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  run: echo "on main"

# Path-triggered docs-only skip
- run: pytest
  if: always()      # run even if earlier step failed (cleanup)

Best Practices

  • Start minimal: checkout → setup-python → lint → pytest. Add stages as needed.
  • Pin action versions with tags/commits, not @main — supply-chain hygiene.
  • Use GITHUB_TOKEN + permissions least-privilege: declare permissions: explicitly.
  • Keep secrets in Settings, never in YAML or code; scope to the workflows that need them.
  • Cache dependency installs; measure and keep runs under ~10 minutes.
  • Make workflows idempotent so retries/manual runs are safe.
  • Test the workflow itself: use gh workflow run, act locally, and actions/checkout debugging (ACTIONS_STEP_DEBUG=true).
  • Put model training on schedules with versioned artifacts; keep CI fast and CD gated.

Common Mistakes

Mistake Consequence Fix
Secret hardcoded in YAML Leaked on push ${{ secrets.X }}
@main action refs Supply-chain risk, breakage Pin @v4 / commit SHA
Missing permissions Vague "denied" failures Set permissions: contents: read etc.
Reinstalling deps each run Slow pipelines Cache (lockfile-keyed)
Test step uses different Python than deploy Version skew Matrix + single canonical version
No workflow_dispatch Can't re-run manually Add it to scheduled workflows
Logging secrets in steps Token leaks into logs Redact; never echo ${{ secrets }}
Huge matrix (8 OS × Python) Wasted minutes/quota Target the needed combos

Troubleshooting

Symptom Cause Fix
Workflow not running Wrong path/name or trigger File must be .github/workflows/*.yml on default branch
The process ... failed with no detail Step error hidden Add set -x; check step logs
Error: Permission denied Token scopes Add permissions block
Cache miss every run Key changes per commit Key on the lockfile hash, not the commit
Job never starts Branch protection / concurrency Check queued runs; concurrency group
docker: not found No Docker on runner Use a Docker-enabled runner or docker/setup-buildx
Secrets *** masked but step fails Value wrong/empty Verify in Settings; test with a debug echo (redacted)

Official Documentation

Last Updated

2026-09-06 — Initial version.