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
- Core Syntax
- Common Operations
- Practical Examples
- Common Patterns
- Best Practices
- Common Mistakes
- Troubleshooting
- Related Cheat Sheets
- Official Documentation
Installation / Setup¶
No install — Actions is a GitHub feature. Create a workflow file:
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: declarepermissions: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,actlocally, andactions/checkoutdebugging (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) |
Related Cheat Sheets¶
- ci-cd.md — the pipeline concepts Actions implements
- devops.md — practices & environment model
- github.md — PRs, releases, Pages basics
- docker.md · docker-compose.md — build & service images
- MLOps — retraining/deploy workflows
- Home · Central Navigation Index
Official Documentation¶
- GitHub Actions docs
- Workflow syntax reference
- Marketplace (official actions)
- act — run Actions locally
Last Updated¶
2026-09-06 — Initial version.