GitHub Actions¶
Master GitHub Actions for CI/CD automation.
Workflow Basics¶
Workflow File Structure¶
# .github/workflows/ci.yml
name: CI # Workflow name (shown in GitHub UI)
on: # Trigger events
push:
branches: [main, develop]
pull_request:
branches: [main]
env: # Global environment variables
NODE_VERSION: "20"
PYTHON_VERSION: "3.12"
jobs: # Jobs to run
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Hello"
Trigger Events¶
on:
# Push to specific branches
push:
branches:
- main
- 'release/**'
paths:
- 'src/**'
- '!src/**/*.md' # Ignore markdown files
# Pull requests
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
# Scheduled runs (cron syntax)
schedule:
- cron: '0 0 * * *' # Daily at midnight UTC
# Manual trigger
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy to'
required: true
default: 'staging'
type: choice
options:
- staging
- production
# Triggered by another workflow
workflow_call:
inputs:
config:
required: true
type: string
secrets:
token:
required: true
Jobs and Steps¶
Job Configuration¶
jobs:
build:
name: Build Application
runs-on: ubuntu-latest
# Job-level environment
environment:
name: staging
url: https://staging.example.com
# Timeout (default 360 minutes)
timeout-minutes: 30
# Conditionally run
if: github.event_name == 'push'
# Concurrency control
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
Step Types¶
steps:
# Use an action
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Run a command
- run: bun install
# Run multiple commands
- run: |
bun install
bun run build
bun test
# Named step with condition
- name: Deploy
if: github.ref == 'refs/heads/main'
run: ./deploy.sh
# Use specific shell
- name: Run PowerShell
shell: pwsh
run: Write-Host "Hello"
Job Dependencies¶
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Linting"
test:
runs-on: ubuntu-latest
steps:
- run: echo "Testing"
build:
needs: [lint, test] # Runs after lint AND test
runs-on: ubuntu-latest
steps:
- run: echo "Building"
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Deploying"
Matrix Builds¶
Basic Matrix¶
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pytest
Matrix with Include/Exclude¶
strategy:
matrix:
python-version: ["3.11", "3.12"]
os: [ubuntu-latest, macos-latest]
# Add specific combinations
include:
- python-version: "3.12"
os: ubuntu-latest
experimental: true
# Remove specific combinations
exclude:
- python-version: "3.11"
os: macos-latest
Fail-Fast Control¶
Caching¶
Dependency Caching¶
# Python with pip
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip' # Built-in caching
# Node with Bun
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: '20'
Custom Caching¶
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: |
~/.cache
node_modules
.next/cache
key: ${{ runner.os }}-build-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-build-
Docker Layer Caching¶
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Secrets and Variables¶
Using Secrets¶
steps:
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.sh
- name: Login to Docker
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | \
docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
Environment-Specific Secrets¶
jobs:
deploy:
environment: production # Uses production secrets
steps:
- run: ./deploy.sh
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }} # production value
Variables (Non-Sensitive)¶
# Repository or organization variables
env:
DEPLOYMENT_REGION: ${{ vars.DEPLOYMENT_REGION }}
APP_NAME: ${{ vars.APP_NAME }}
Reusable Workflows¶
Create Reusable Workflow¶
# .github/workflows/reusable-build.yml
name: Reusable Build
on:
workflow_call:
inputs:
environment:
required: true
type: string
node-version:
required: false
type: string
default: '20'
secrets:
npm-token:
required: true
outputs:
image-tag:
description: "Built image tag"
value: ${{ jobs.build.outputs.tag }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Build
env:
NPM_TOKEN: ${{ secrets.npm-token }}
run: bun run build
- id: tag
run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT
Use Reusable Workflow¶
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
environment: production
node-version: '20'
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Deploying ${{ needs.build.outputs.image-tag }}"
Composite Actions¶
Create Composite Action¶
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Setup Python and Node environment'
inputs:
python-version:
description: 'Python version'
required: false
default: '3.12'
node-version:
description: 'Node version'
required: false
default: '20'
runs:
using: 'composite'
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: 'pip'
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install dependencies
shell: bash
run: |
pip install -e ".[dev]"
bun install
Use Composite Action¶
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
with:
python-version: '3.12'
node-version: '20'
- run: pytest && bun test
Services (Containers)¶
Database Services¶
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Run tests
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
run: pytest
Outputs and Artifacts¶
Job Outputs¶
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Deploying version ${{ needs.build.outputs.version }}"
Artifacts¶
# Upload
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 5
# Download
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
Expressions and Contexts¶
Contexts¶
steps:
- run: |
echo "Repository: ${{ github.repository }}"
echo "Branch: ${{ github.ref_name }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Event: ${{ github.event_name }}"
echo "Runner OS: ${{ runner.os }}"
echo "Job status: ${{ job.status }}"
Conditional Expressions¶
steps:
# Run on specific branch
- if: github.ref == 'refs/heads/main'
run: echo "On main"
# Run on pull request
- if: github.event_name == 'pull_request'
run: echo "PR"
# Run on success
- if: success()
run: echo "Previous step succeeded"
# Run on failure
- if: failure()
run: echo "Previous step failed"
# Always run (even if canceled)
- if: always()
run: echo "Cleanup"
# Complex condition
- if: |
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
!contains(github.event.head_commit.message, '[skip ci]')
run: ./deploy.sh
Best Practices¶
| Practice | Implementation |
|---|---|
| Pin action versions | uses: actions/checkout@v4 |
| Use caching | Reduce build times |
| Fail fast | Quick checks first |
| Use environments | Control deployments |
| Store secrets securely | Never hardcode |
| Use matrix builds | Test multiple configs |
| Limit concurrency | Prevent resource waste |
| Add timeouts | Prevent hanging jobs |