Examples
Five end-to-end CI/CD examples: a GitHub Actions matrix build, a Docker build + push, a Terraform plan + apply, an OIDC-authenticated deploy, and a release pipeline. Paste-ready.
Five CI/CD recipes
EXAMPLE
# 1) Test matrix on push + PR
# .github/workflows/test.yml
name: test
on:
push: { branches: [main] }
pull_request: { branches: [main] }
jobs:
test:
strategy:
fail-fast: false
matrix:
node: ['18', '20', '22']
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node }}, cache: 'npm' }
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --reporter=junit
- if: always()
uses: actions/upload-artifact@v4
with: { name: junit-${{ matrix.os }}-${{ matrix.node }}, path: junit.xml }
# 2) Docker build + push to GHCR
# .github/workflows/docker.yml
name: docker
on:
push: { branches: [main], tags: ['v*'] }
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=long
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# 3) Terraform plan on PR, apply on merge
# .github/workflows/terraform.yml
name: terraform
on:
pull_request:
paths: ['infra/**']
push:
branches: [main]
paths: ['infra/**']
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with: { role-to-assume: ${{ secrets.TF_ROLE_ARN }}, aws-region: ap-southeast-2 }
- uses: hashicorp/setup-terraform@v3
- working-directory: infra
run: |
terraform init -input=false
terraform plan -no-color -out plan.bin
terraform show -no-color plan.bin > plan.txt
- uses: actions/github-script@v7
with: |
const fs = require('fs');
const body = '### Plan\n```\n' + fs.readFileSync('infra/plan.txt', 'utf8') + '\n```';
github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });
apply:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with: { role-to-assume: ${{ secrets.TF_ROLE_ARN }}, aws-region: ap-southeast-2 }
- uses: hashicorp/setup-terraform@v3
- working-directory: infra
run: |
terraform init -input=false
terraform apply -auto-approve -input=false
# 4) OIDC to AWS (no static keys)
# Set up the OIDC trust once in AWS:
# aws iam create-open-id-connect-provider \
# --url https://token.actions.githubusercontent.com \
# --client-id-list sts.amazonaws.com
# Then create a role with a trust policy that limits to your repo:
# { "Federated": "...oidc...", "Action": "sts:AssumeRoleWithWebIdentity",
# "Condition": { "StringEquals": {
# "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
# "token.actions.githubusercontent.com:sub": "repo:owner/repo:ref:refs/heads/main"
# } } }
# 5) Tag-triggered release pipeline (see cicd/tags lesson)
# .github/workflows/release.yml — triggered on push of v*.*.* tag
# Builds + signs + uploads + creates GitHub release + deploys.
# ===== Patterns to internalise =====
# - Run lint + typecheck + test on every PR, matrix-style
# - Cache aggressively (actions/setup-node cache, Docker buildx --cache-to gha)
# - OIDC for AWS / GCP / Azure (no static keys in secrets)
# - Plan on PR, apply on merge for infra
# - Separate workflow files per concern; reuse via composite actions
# ===== Pitfalls =====
# - Storing AWS keys in secrets (use OIDC)
# - Running tests against a single Node version
# - Forgetting to upload coverage / test reports
# - Long workflows (> 30 min) without parallelism or caching
# - Manual tag pushes (use release-please instead)
Why it matters
OIDC + a per-environment role assumption + plan-on-PR + apply-on-merge is the modern CI/CD shape. No static keys, no manual approvals on tiny changes, and the diff between staging and production is visible in the PR before any apply lands. Once you have it, "we deployed" stops being scary.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# A complete pipeline: lint → test → e2e → build → publish → deploy → notify.Try it Yourself »
Discussion
Loading…