CI/CD with GitHub Actions: Pipelines for Modern Web Projects
Introduction
GitHub Actions embeds CI/CD in your repository—workflows triggered by push, PR, schedule, or release. For web teams, it runs tests, lint, builds Docker images, deploys to cloud, and gates merges on quality checks.
This guide covers workflow structure, reusable actions, caching, matrix strategies, environment secrets, deployment patterns (staging/production), and monorepo considerations—practical YAML you can adapt today.
Table of Contents
- Workflow Basics and Triggers
- Jobs, Steps, and Runners
- Caching and Performance
- Matrix Builds and Reusable Workflows
- Docker Build and Registry Push
- Deployment Patterns
- Secrets and Environments
- Best Practices
- Common Mistakes
- Interview Questions
- Summary
- References
Workflow Basics and Triggers
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
concurrency cancels outdated PR runs—saves minutes and speeds feedback.
Jobs, Steps, and Runners
Jobs run in parallel by default; needs defines dependencies:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: echo "Deploying..."
Self-hosted runners for GPU, larger disks, or private network access. GitHub-hosted ubuntu-latest suits most Node projects.
Caching and Performance
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Docker layer cache:
- uses: docker/build-push-action@v6
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Pin action versions (@v4)—@main breaks silently.
Matrix Builds and Reusable Workflows
Test multiple Node versions:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [20, 22]
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
Reusable workflow:
# .github/workflows/reusable-test.yml
on:
workflow_call:
inputs:
node-version:
required: true
type: string
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci && npm test
Caller:
jobs:
call-test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "22"
Docker Build and Registry Push
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
Tag with github.sha for immutable deploys; promote same image to prod.
Deployment Patterns
Staging on PR preview:
deploy-preview:
if: github.event_name == 'pull_request'
environment: preview
steps:
- run: npx vercel deploy --token=${{ secrets.VERCEL_TOKEN }}
Production with approval:
deploy-prod:
needs: docker
environment:
name: production
url: https://app.example.com
runs-on: ubuntu-latest
steps:
- run: ./scripts/deploy.sh ${{ github.sha }}
GitHub environments add required reviewers and environment-scoped secrets.
Secrets and Environments
- Store secrets in Settings → Secrets and variables → Actions
- Never echo secrets in logs—GitHub masks known patterns
- Use OIDC for cloud deploy without long-lived keys:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubDeploy
aws-region: us-east-1
Principle of least privilege on IAM roles.
Best Practices
- Fast feedback—lint and unit tests first job.
- Pin action versions and update deliberately.
- Fail PR on test/lint—branch protection rules.
- Separate CI from CD—deploy only from main or release tags.
- Artifact passing between jobs for build outputs.
- Monitor workflow minutes—cache aggressively.
Common Mistakes
- No concurrency cancel—queued redundant runs.
npm installinstead ofnpm ci—non-reproducible builds.- Secrets in workflow YAML—use secrets store.
- Running full E2E on every commit without parallelism—10+ min CI.
- Deploy on PR merge without smoke test gate.
- Using
continue-on-erroreverywhere—hides real failures.
Interview Questions
- How do GitHub Actions workflows trigger?
- Difference between job and step?
- How to cache dependencies in Actions?
- What is a matrix build?
- OIDC vs stored AWS keys?
- How to gate production deploys?
- Reusable workflows vs composite actions?
- Strategies to speed up CI?
Monorepo CI Strategies
For Turborepo or Nx monorepos, run only affected packages:
- name: Detect changes
uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api:
- 'apps/api/**'
- 'packages/**'
- name: Test API
if: steps.filter.outputs.api == 'true'
run: npm run test --workspace=api
Path filters prevent 20-minute CI on documentation-only commits.
Security Scanning in Pipeline
- name: Run Trivy
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
severity: CRITICAL,HIGH
exit-code: 1
- name: npm audit
run: npm audit --audit-level=high
Balance strictness—new CVEs appear daily; pin policy to block only exploitable paths in your dependency tree.
Release Automation
Tag-driven releases:
on:
push:
tags: ["v*"]
jobs:
release:
steps:
- run: npm run build
- uses: softprops/action-gh-release@v2
with:
files: dist/**
Changelog generation from conventional commits keeps release notes accurate for internal and external consumers.
Branch Protection and Required Checks
Configure GitHub branch protection on main:
- Require PR before merge
- Require status checks:
test,lint,build - Require at least one approval from code owners
- Dismiss stale approvals on new commits
- Block force pushes
# CODEOWNERS
/apps/api/ @backend-team
/packages/shared/ @platform-team
CODEOWNERS auto-requests reviewers—reduces accidental merges without domain expert review.
Workflow Notifications and Failure Triage
Integrate Slack or Discord on failure:
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v2
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{"text": "CI failed on ${{ github.ref }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}
Failed main branch CI should page on-call—production deploy pipeline depends on green main.
Document common failure modes in team wiki: flaky integration tests, expired tokens, rate limits on registry push.
Caching npm in Monorepos
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
package-lock.json
apps/*/package-lock.json
packages/*/package-lock.json
Multiple lockfiles need explicit cache-dependency-path—otherwise cache misses every run and CI doubles in duration.
Rotate CI secrets on schedule—long-lived GITHUB_TOKEN scopes and cloud OIDC roles should be audited quarterly. Document which workflows use which credentials so compromise of one secret does not cascade across every deployment pipeline in the organization without a clear inventory for incident response teams to revoke access quickly and systematically.
Measure CI duration weekly and set team goals to keep PR feedback under ten minutes for the fast path—developer productivity correlates with merge frequency more than raw test count. Split workflows so documentation-only changes skip heavy integration suites entirely using path filters, while application changes still run the full matrix across supported Node versions and database service containers mirroring production topology as closely as staging infrastructure budgets allow without incurring cloud costs that finance teams reject during quarterly planning cycles when engineering headcount grows faster than infrastructure allocation for non-production environments used exclusively by automated test runners rather than human QA teams performing exploratory testing.
Summary
GitHub Actions provides triggers, jobs, caching, matrices, and environment-gated deployments in YAML beside your code. Structure workflows for fast PR feedback, immutable Docker tags, OIDC for cloud auth, and branch protection for quality gates. Well-designed CI/CD is the backbone of reliable releases.