GitHub Actions: From Zero to Production CI/CD
Build robust CI/CD pipelines with GitHub Actions. Learn workflows, reusable actions, secrets management, and deployment strategies for production systems.
GitHub Actions has become the de facto CI/CD platform for modern development teams. This guide takes you from basic workflows to production-grade pipelines with caching, matrix builds, and secure deployments.
Understanding GitHub Actions
GitHub Actions uses a hierarchy: Workflows contain Jobs, which contain Steps. Workflows are triggered by Events.
# .github/workflows/ci.yml
name: CI Pipeline # Workflow name
on: # Events that trigger the workflow
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs: # Jobs run in parallel by default
test: # Job ID
runs-on: ubuntu-latest # Runner environment
steps: # Sequential steps
- uses: actions/checkout@v4
- name: Run tests
run: npm test
A Complete CI Pipeline
Here’s a production-ready CI pipeline with caching, linting, testing, and security scanning:
name: CI
on:
push:
branches: [main]
pull_request:
env:
NODE_VERSION: '20'
jobs:
# ─────────────────────────────────────────────────────────
# Lint and type-check
# ─────────────────────────────────────────────────────────
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run TypeScript type check
run: npm run type-check
# ─────────────────────────────────────────────────────────
# Unit tests with coverage
# ─────────────────────────────────────────────────────────
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# ─────────────────────────────────────────────────────────
# Security scanning
# ─────────────────────────────────────────────────────────
security:
name: Security Scan
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: '.'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Run npm audit
run: npm audit --audit-level=high
# ─────────────────────────────────────────────────────────
# Build
# ─────────────────────────────────────────────────────────
build:
name: Build
runs-on: ubuntu-latest
needs: [lint, test, security] # Wait for these jobs
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
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/
retention-days: 7
Matrix Builds
Test across multiple versions and platforms simultaneously:
jobs:
test:
name: Test (${{ matrix.os }} / Node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # Don't cancel other jobs if one fails
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18 # Skip Node 18 on Windows
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Caching Strategies
Caching dramatically speeds up workflows:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Cache node_modules
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
# Only install if cache miss
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci
# Cache Next.js build
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: |
.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*.ts', 'src/**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-
- name: Build
run: npm run build
Reusable Workflows
Create workflows that can be called from other workflows:
# .github/workflows/reusable-deploy.yml
name: Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
version:
required: true
type: string
secrets:
AWS_ACCESS_KEY_ID:
required: true
AWS_SECRET_ACCESS_KEY:
required: true
jobs:
deploy:
name: Deploy to ${{ inputs.environment }}
runs-on: ubuntu-latest
environment: ${{ inputs.environment }} # GitHub environment
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
run: |
echo "Deploying version ${{ inputs.version }} to ${{ inputs.environment }}"
# Your deployment commands here
Call it from another workflow:
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
version: ${{ github.ref_name }}
secrets:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
version: ${{ github.ref_name }}
secrets:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Composite Actions
Create custom actions that bundle multiple steps:
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Setup Node.js with caching and install dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
runs:
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
shell: bash
run: npm ci
- name: Cache build outputs
uses: actions/cache@v4
with:
path: |
.next/cache
node_modules/.cache
key: ${{ runner.os }}-build-${{ hashFiles('package-lock.json') }}
Use it in workflows:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
with:
node-version: '20'
- run: npm run build
Secrets and Environments
Using Secrets
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
# Secrets are masked in logs automatically
./deploy.sh
Environment Protection Rules
Configure environments in GitHub Settings → Environments:
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com
steps:
- name: Deploy
run: ./deploy.sh
Environment features:
- Required reviewers: Manual approval before deployment
- Wait timer: Delay before deployment starts
- Branch restrictions: Only allow deployments from specific branches
- Environment secrets: Secrets scoped to an environment
Docker Build and Push
Build and push Docker images with caching:
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: myorg/myapp
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
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
Deployment Strategies
Rolling Deployment to Kubernetes
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Update image
run: |
kubectl set image deployment/myapp \
myapp=myorg/myapp:${{ github.sha }} \
--record
- name: Wait for rollout
run: |
kubectl rollout status deployment/myapp --timeout=5m
Blue-Green Deployment
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to blue environment
run: |
kubectl apply -f k8s/deployment-blue.yaml
kubectl wait --for=condition=available deployment/myapp-blue
- name: Run smoke tests
run: |
./scripts/smoke-test.sh http://myapp-blue.internal
- name: Switch traffic to blue
run: |
kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'
- name: Scale down green
run: |
kubectl scale deployment/myapp-green --replicas=0
Workflow Optimization Tips
Conditional Jobs
jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
Skip CI
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
Timeout and Concurrency
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15 # Kill job if it takes too long
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Cancel old runs when new commit pushed
Debugging Workflows
Enable Debug Logging
Set these secrets in your repository:
ACTIONS_RUNNER_DEBUG:trueACTIONS_STEP_DEBUG:true
SSH Into Runner
- name: Setup tmate session
if: ${{ failure() }} # Only on failure
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
Key Takeaways
- Use caching aggressively — it saves time and money
- Create reusable workflows for common patterns
- Use environments for deployment protection
- Matrix builds catch cross-platform issues early
- Fail fast — don’t wait for slow tests if linting fails
- Set timeouts — runaway jobs are expensive
“The goal of CI is not to have green builds. It’s to have the confidence to deploy at any time.”