| name | github-actions-cicd |
| description | Set up GitHub Actions CI/CD pipelines for testing, building, and deploying to Kubernetes. Use when implementing continuous integration and deployment for Phase 5. (project) |
| allowed-tools | Bash, Write, Read, Glob, Edit, Grep |
GitHub Actions CI/CD Skill
Quick Start
- Read Phase 5 Constitution -
constitution-prompt-phase-5.md - Create workflows directory -
.github/workflows/ - Set up CI pipeline - Test, lint, build on PR
- Set up CD pipeline - Deploy to staging/production
- Configure secrets - Docker registry, K8s credentials
- Add branch protection - Require CI to pass
Workflow Structure
.github/
└── workflows/
├── ci.yaml # Test & build on PR
├── cd-staging.yaml # Deploy to staging
├── cd-production.yaml # Deploy to production
└── security.yaml # Security scanning
CI Pipeline (Pull Requests)
Create .github/workflows/ci.yaml:
name: CI Pipeline
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ${{ github.repository }}
jobs:
# Backend Tests
backend-test:
name: Backend Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./backend
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: todo_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.13
- name: Install dependencies
run: uv sync --frozen
- name: Run linting
run: uv run ruff check .
- name: Run type checking
run: uv run mypy src/
- name: Run tests
env:
DATABASE_URL: postgresql://test:test@localhost:5432/todo_test
BETTER_AUTH_SECRET: test-secret
GEMINI_API_KEY: test-key
run: uv run pytest tests/ -v --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./backend/coverage.xml
flags: backend
# Frontend Tests
frontend-test:
name: Frontend Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./frontend
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
run: npm ci
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run tests
run: npm test -- --coverage
- name: Build
run: npm run build
env:
NEXT_PUBLIC_API_URL: http://localhost:8000
# Build Docker Images
build-images:
name: Build Images
runs-on: ubuntu-latest
needs: [backend-test, frontend-test]
if: github.event_name == 'push'
permissions:
contents: read
packages: write
strategy:
matrix:
service:
- name: backend
context: ./backend
dockerfile: ./backend/Dockerfile
- name: frontend
context: ./frontend
dockerfile: ./frontend/Dockerfile
- name: notification-service
context: ./services/notification
dockerfile: ./services/notification/Dockerfile
- name: recurring-service
context: ./services/recurring
dockerfile: ./services/recurring/Dockerfile
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
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_PREFIX }}/${{ matrix.service.name }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ${{ matrix.service.context }}
file: ${{ matrix.service.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Helm Chart Validation
helm-lint:
name: Helm Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v3.14.0
- name: Lint charts
run: helm lint ./helm/evolution-todo
- name: Template validation
run: |
helm template evolution-todo ./helm/evolution-todo \
--values ./helm/evolution-todo/values.yaml \
--debug
CD Pipeline (Staging)
Create .github/workflows/cd-staging.yaml:
name: CD Staging
on:
push:
branches: [develop]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ${{ github.repository }}
CLUSTER_NAME: todo-staging
NAMESPACE: todo-staging
jobs:
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig
run: doctl kubernetes cluster kubeconfig save ${{ env.CLUSTER_NAME }}
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v3.14.0
- name: Deploy with Helm
run: |
helm upgrade --install evolution-todo ./helm/evolution-todo \
--namespace ${{ env.NAMESPACE }} \
--create-namespace \
--values ./helm/evolution-todo/values-staging.yaml \
--set global.image.tag=${{ github.sha }} \
--set backend.env.DATABASE_URL=${{ secrets.STAGING_DATABASE_URL }} \
--set backend.env.GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} \
--set backend.env.BETTER_AUTH_SECRET=${{ secrets.BETTER_AUTH_SECRET }} \
--wait \
--timeout 10m
- name: Verify deployment
run: |
kubectl rollout status deployment/backend -n ${{ env.NAMESPACE }}
kubectl rollout status deployment/frontend -n ${{ env.NAMESPACE }}
- name: Run smoke tests
run: |
FRONTEND_URL=$(kubectl get svc frontend -n ${{ env.NAMESPACE }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -f http://$FRONTEND_URL/health || exit 1
CD Pipeline (Production)
Create .github/workflows/cd-production.yaml:
name: CD Production
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Version to deploy'
required: true
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ${{ github.repository }}
CLUSTER_NAME: todo-production
NAMESPACE: todo-app
jobs:
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Determine version
id: version
run: |
if [ "${{ github.event_name }}" == "release" ]; then
echo "version=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
else
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig
run: doctl kubernetes cluster kubeconfig save ${{ env.CLUSTER_NAME }}
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v3.14.0
- name: Deploy with Helm
run: |
helm upgrade --install evolution-todo ./helm/evolution-todo \
--namespace ${{ env.NAMESPACE }} \
--values ./helm/evolution-todo/values-production.yaml \
--set global.image.tag=${{ steps.version.outputs.version }} \
--set backend.env.DATABASE_URL=${{ secrets.PROD_DATABASE_URL }} \
--set backend.env.GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} \
--set backend.env.BETTER_AUTH_SECRET=${{ secrets.BETTER_AUTH_SECRET }} \
--set kafka.brokers=${{ secrets.REDPANDA_BROKERS }} \
--wait \
--timeout 15m
- name: Verify deployment
run: |
kubectl rollout status deployment/backend -n ${{ env.NAMESPACE }} --timeout=5m
kubectl rollout status deployment/frontend -n ${{ env.NAMESPACE }} --timeout=5m
- name: Notify success
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Production deployment successful: ${{ steps.version.outputs.version }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- name: Rollback on failure
if: failure()
run: |
helm rollback evolution-todo -n ${{ env.NAMESPACE }}
Security Scanning
Create .github/workflows/security.yaml:
name: Security Scanning
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday
jobs:
trivy-scan:
name: Trivy Vulnerability Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner (repo)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
dependency-review:
name: 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@v4
with:
fail-on-severity: high
Required Secrets
Configure in GitHub Settings → Secrets and variables → Actions:
| Secret | Description | Used In |
|---|---|---|
DIGITALOCEAN_ACCESS_TOKEN |
DO API token | CD pipelines |
STAGING_DATABASE_URL |
Staging Neon DB URL | CD Staging |
PROD_DATABASE_URL |
Production Neon DB URL | CD Production |
GEMINI_API_KEY |
Google AI API key | All deployments |
BETTER_AUTH_SECRET |
Auth secret | All deployments |
REDPANDA_BROKERS |
Redpanda Cloud brokers | Production |
SLACK_WEBHOOK |
Slack notifications | CD Production |
Branch Protection Rules
Configure in GitHub Settings → Branches → Branch protection rules:
# For main branch
- Require pull request before merging
- Require status checks to pass:
- backend-test
- frontend-test
- helm-lint
- Require branches to be up to date
- Require signed commits (optional)
- Include administrators
Verification Checklist
-
.github/workflows/directory created - CI pipeline runs on PRs
- Docker images build and push to GHCR
- Helm charts lint successfully
- Staging deployment works
- Production deployment works
- All secrets configured
- Branch protection rules set
- Security scanning enabled
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Tests fail | Missing env vars | Add to workflow env |
| Push denied | No GHCR permission | Check packages: write |
| Helm deploy fails | Bad values | Run helm template locally |
| K8s auth fails | Bad kubeconfig | Check doctl token |