GitHub Actions Workflows
Overview¶
This document provides comprehensive GitHub Actions workflow templates for CI/CD, testing, building, and deployment across all languages and frameworks covered in this style guide.
Python CI/CD Workflow¶
name: Python CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Lint with ruff
run: ruff check src tests
- name: Check formatting with black
run: black --check src tests
- name: Type check with mypy
run: mypy src
- name: Run tests with pytest
run: |
pytest --cov --cov-report=xml --cov-report=term
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Bandit security scan
run: |
pip install bandit
bandit -r src -f json -o bandit-report.json
- name: Run Safety dependency check
run: |
pip install safety
safety check --json
build:
needs: [test, security]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Build package
run: |
pip install build
python -m build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
Node.js / TypeScript CI/CD Workflow¶
name: Node.js CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run type-check
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build
path: dist/
Terraform CI/CD Workflow¶
name: Terraform CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TF_VERSION: 1.6.0
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init -backend=false
- name: Terraform Validate
run: terraform validate
- name: Run tflint
uses: terraform-linters/setup-tflint@v4
with:
tflint_version: latest
- name: TFLint
run: tflint --recursive
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
soft_fail: true
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: terraform
soft_fail: true
plan:
needs: [validate, security]
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Terraform Init
run: terraform init
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Upload plan
uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplan
apply:
needs: [validate, security]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Terraform Init
run: terraform init
- name: Terraform Apply
run: terraform apply -auto-approve
Docker Build and Push Workflow¶
name: Docker Build and Push
on:
push:
branches: [main]
tags:
- 'v*'
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
security-scan:
needs: build
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@v0.34.1
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
Release Workflow¶
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
pip install build twine
- name: Build package
run: python -m build
- name: Check distribution
run: twine check dist/*
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish-pypi:
needs: build
runs-on: ubuntu-latest
environment: release
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
create-release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Generate changelog
id: changelog
run: |
echo "## What's Changed" > CHANGELOG.md
git log --pretty=format:"- %s (%h)" $(git describe --tags --abbrev=0 HEAD^)..HEAD >> CHANGELOG.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
body_path: CHANGELOG.md
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Reusable Workflow - Test¶
## .github/workflows/reusable-test.yml
name: Reusable Test Workflow
on:
workflow_call:
inputs:
python-version:
required: true
type: string
working-directory:
required: false
type: string
default: '.'
outputs:
coverage:
description: "Test coverage percentage"
value: ${{ jobs.test.outputs.coverage }}
jobs:
test:
runs-on: ubuntu-latest
outputs:
coverage: ${{ steps.coverage.outputs.percentage }}
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: 'pip'
- name: Install dependencies
working-directory: ${{ inputs.working-directory }}
run: |
pip install -e ".[dev]"
- name: Run tests
working-directory: ${{ inputs.working-directory }}
run: |
pytest --cov --cov-report=term --cov-report=json
- name: Extract coverage
id: coverage
working-directory: ${{ inputs.working-directory }}
run: |
COVERAGE=$(jq '.totals.percent_covered' coverage.json)
echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT
Using Reusable Workflow¶
## .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test-python-310:
uses: ./.github/workflows/reusable-test.yml
with:
python-version: "3.10"
test-python-311:
uses: ./.github/workflows/reusable-test.yml
with:
python-version: "3.11"
Multi-Language Monorepo Workflow¶
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
terraform: ${{ steps.filter.outputs.terraform }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'frontend/**'
backend:
- 'backend/**'
terraform:
- 'terraform/**'
test-frontend:
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run tests
working-directory: frontend
run: npm test
test-backend:
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: 'pip'
cache-dependency-path: backend/requirements.txt
- name: Install dependencies
working-directory: backend
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests
working-directory: backend
run: pytest
validate-terraform:
needs: detect-changes
if: needs.detect-changes.outputs.terraform == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Format Check
working-directory: terraform
run: terraform fmt -check
- name: Terraform Validate
working-directory: terraform
run: |
terraform init -backend=false
terraform validate
Security Scanning Workflow¶
name: Security Scanning
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday
jobs:
codeql:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
strategy:
matrix:
language: [python, javascript]
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v3
secret-scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: TruffleHog Secret Scan
uses: trufflesecurity/trufflehog@v3.93.4
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: auto
Deployment Workflow¶
name: Deploy to Production
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster production-cluster \
--service web-service \
--force-new-deployment
- name: Wait for deployment
run: |
aws ecs wait services-stable \
--cluster production-cluster \
--services web-service
- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v1.25.0
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "Deployment to production: ${{ job.status }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Deployment Status: *${{ job.status }}*\nCommit: ${{ github.sha }}\nAuthor: ${{ github.actor }}"
}
}
]
}
Best Practices¶
Workflow Optimization¶
## Cache dependencies
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
## Use matrix strategy for multiple versions
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
fail-fast: false # Continue other jobs if one fails
## Conditional job execution
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
## Job dependencies
needs: [test, lint, security]
Security Best Practices¶
## Use environment protection rules
environment:
name: production
url: https://example.com
## Minimal permissions
permissions:
contents: read
packages: write
## Use secrets for sensitive data
env:
API_KEY: ${{ secrets.API_KEY }}
## Pin action versions
uses: actions/checkout@v4 # Not @main
Artifact Management¶
## Upload artifacts
- uses: actions/upload-artifact@v4
with:
name: test-results
path: |
test-results/
coverage/
retention-days: 30
## Download artifacts
- uses: actions/download-artifact@v4
with:
name: test-results
path: test-results/
Composite Actions¶
Custom Action Example¶
## .github/actions/setup-python-env/action.yml
name: 'Setup Python Environment'
description: 'Set up Python with caching and install dependencies'
inputs:
python-version:
description: 'Python version to use'
required: true
cache-key:
description: 'Cache key prefix'
required: false
default: 'pip'
runs:
using: 'composite'
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: ${{ inputs.cache-key }}
- name: Install dependencies
shell: bash
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
Using Composite Action¶
steps:
- uses: actions/checkout@v4
- name: Setup Python environment
uses: ./.github/actions/setup-python-env
with:
python-version: "3.10"
References¶
Official Documentation¶
Best Practices¶
Useful Actions¶
- actions/checkout
- actions/setup-python
- actions/setup-node
- docker/build-push-action
- hashicorp/setup-terraform
Status: Active