Skip to main content

GitHub Actions

GitHub Actions is the CI/CD platform built into GitHub. It lets you automate build, test, and deployment workflows directly from your repository. For LLM applications, a well-designed pipeline ensures every push is validated, every image is scanned, and every deployment is reproducible.

Basic Workflow Structure

Every workflow lives in .github/workflows/ and has three core sections: triggers (on), jobs, and steps.

yaml
name: CI Pipeline

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

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

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
pip install uv
uv pip install --system -r requirements.txt

- name: Run tests
run: pytest --cov=app --cov-report=xml

- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Trigger on the right events

Use pull_request for validation and push to main for deployment. Avoid triggering on every push to every branch — it wastes minutes.

Matrix Builds

Matrix builds let you test across multiple Python versions, operating systems, or dependency sets in parallel:

yaml
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12"]
exclude:
- os: macos-latest
python-version: "3.10"

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt
- run: pytest

The fail-fast: false setting ensures all matrix combinations run even if one fails — critical for understanding the full compatibility picture.

Reusable Workflows

When multiple repositories share the same pipeline logic, use reusable workflows with workflow_call:

yaml
# .github/workflows/reusable-test.yml
name: Reusable Test

on:
workflow_call:
inputs:
python-version:
required: false
type: string
default: "3.12"
requirements-file:
required: false
type: string
default: "requirements.txt"

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- run: pip install -r ${{ inputs.requirements-file }}
- run: pytest --cov

Call it from any repository workflow:

yaml
jobs:
call-tests:
uses: my-org/.github/.github/workflows/reusable-test.yml@main
with:
python-version: "3.12"
secrets: inherit
Reusable vs. Composite Actions

Use reusable workflows (workflow_call) for multi-job pipelines. Use composite actions (action.yml) for sharing individual steps. Reusable workflows support secrets and matrices; composite actions are simpler and faster.

Secrets Management

Never hardcode API keys or credentials. Use GitHub Secrets and reference them with ${{ secrets.NAME }}:

Secret TypeScopeUse Case
Repository secretsSingle repoAPI keys, deploy tokens
Environment secretsPer environmentProduction DB passwords
Organization secretsAll repos in orgShared signing keys
yaml
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Requires environment protection approval
steps:
- uses: actions/checkout@v4
- name: Deploy to production
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
docker build -t myapp:prod .
docker push registry.example.com/myapp:prod
kubectl apply -f k8s/

Environment Protection Rules

Environments add a gate between staging and production. Configure them in Settings → Environments:

  • Required reviewers — require manual approval before deployment
  • Wait timer — add a delay (e.g., 5 minutes) for rollback window
  • Branch restrictions — only allow deploys from main
  • Environment secrets — separate credentials per environment
yaml
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "Deploying to staging..."

deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- run: echo "Deploying to production..."

Caching Strategies

Caching dramatically speeds up workflows. Cache dependencies, Docker layers, and build artifacts:

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

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

- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: docker-${{ runner.os }}-${{ hashFiles('Dockerfile') }}
restore-keys: |
docker-${{ runner.os }}-

- name: Build with cache
uses: docker/build-push-action@v5
with:
context: .
push: false
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
Cache key matters

Always include the lock file hash (requirements.txt, package-lock.json) in the cache key. Otherwise you'll restore stale dependencies.

Complete Production Pipeline

Here is a production-grade pipeline combining everything:

yaml
name: Production Deploy

on:
push:
branches: [main]

concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
uses: ./.github/workflows/reusable-test.yml
secrets: inherit

security-scan:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
scan-ref: .

deploy:
needs: [test, security-scan]
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy
run: kubectl rollout restart deployment/myapp

Key Takeaways

  • Use matrix builds to validate across Python versions and OS combinations
  • Extract shared logic into reusable workflows for DRY pipelines
  • Always use secrets for credentials — never hardcode or log them
  • Add environment protection rules for production deployments
  • Enable caching for dependencies and Docker layers to cut CI minutes