CI/CD for the Web: A GitHub Actions Masterclass
From first workflow to production: triggers, jobs, caching, matrices, reusable workflows, environments, and the security hardening that keeps GitHub Actions pipelines safe in 2026.
Published on • August 11, 2026
AI Assistant

“It works on my machine” is the confession behind every un-shipped release. Manual deploys rot: a step is skipped, a cache is stale, a migration runs twice. CI/CD exists to make delivery deterministic and reversible, and GitHub Actions is the most widely used tool for the job in 2026 — triggering tens of millions of runs a day.
In this post, you will learn how to design production-grade pipelines: trigger them correctly, structure jobs and steps, cache dependencies, fan out with matrices, share logic via reusable workflows and composite actions, gate deploys with environments, and — non-negotiably in 2026 — harden the whole thing against supply-chain attacks.
Key technologies: GitHub Actions YAML, the actions/* family (checkout, setup-node, cache, upload-artifact, configure-pages, upload-pages-artifact, deploy-pages), CodeQL, and GitHub Environments.
Prerequisites
- A GitHub repository with write access, and a local clone.
- Git 2.40+, and a Node.js web project (examples use Node 22 LTS).
- Nothing to install — GitHub’s hosted
ubuntu-latestrunners are included.
Anatomy of a workflow
A workflow is YAML in .github/workflows/. The minimal CI pipeline:
name: CI
on: [push, pull_request]
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run lint && npm test
Two details matter here. permissions: contents: read grants least privilege — the default GITHUB_TOKEN is far too permissive. concurrency cancels superseded runs for the same branch, so you never pay for a build you’re about to replace. Beyond push and pull_request, reach for workflow_dispatch (manual triggers with typed inputs), schedule with cron for nightly audit jobs, and paths filters so docs-only commits skip CI entirely.
Matrices, caching, and artifacts
A matrix fans one job definition into a Cartesian product — Node 22 and 24 across three OSes, six runners from one job:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['22', '24']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit
fail-fast: false keeps all combinations running even when one fails. setup-node’s cache: 'npm' handles the download cache; use actions/cache@v4 for bigger wins like Playwright browsers. Artifacts move files between jobs and outlive the run (actions/upload-artifact@v4, often with if: always() so reports ship even after a failure). Job outputs pass small values across the needs boundary via $GITHUB_OUTPUT.
Reusable workflows and composite actions
Duplicated YAML rots. A reusable workflow is a full workflow called like a job, declared with workflow_call:
# .github/workflows/node-ci.yml
name: Node CI
on:
workflow_call:
inputs:
node-version:
type: string
default: '22'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
Call it from any workflow (secrets must be passed explicitly or with secrets: inherit):
jobs:
ci:
uses: ./.github/workflows/node-ci.yml
with:
node-version: '24'
A composite action packages several steps into a single reusable step — it lives in action.yml, declares runs: using: composite, and collapses checkout + setup + install into one uses: line. Pin reusable workflows and composite actions by tag or commit SHA — never @main — or callers drift silently when the shared definition changes.
Environments, approvals, and secrets
Environments gate deploys with protection rules: required reviewers, wait timers, and secret isolation. A deploy job declares environment: production, and GitHub pauses the run until an approved reviewer says yes — then injects only that environment’s secrets. Secrets live in settings, never in YAML, and ${{ secrets.FOO || 'fallback' }} degrades gracefully in forked PR builds. Prefer the OIDC pattern — aws-actions/configure-aws-credentials@v4 with role-to-assume — over storing long-lived cloud keys.
Hardening your pipeline in 2026
The pipeline is part of your attack surface. The non-negotiables: pin third-party actions to SHAs (or at least major tags); grant minimal permissions; never run untrusted PR code with your secrets in a pull_request_target job; and scan dependencies on every run — npm audit --audit-level=high plus github/codeql-action/init@v3 and analyze@v3 for JavaScript/TypeScript. Least-privilege permissions, pinned actions, and CodeQL are the baseline posture reviewers expect in 2026.
Putting It All Together
A runnable pipeline for a static Astro/Next-style site that lints, tests, builds, and deploys to GitHub Pages — full gist: https://gist.github.com/redlinesoft/cicd-node-deploy-pipeline
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run lint && npm test
- run: npm run build
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
environment: github-pages
steps:
- id: deployment
uses: actions/deploy-pages@v4
Expected output: a build job passing lint, tests, and npm run build, then a deploy job publishing dist/ and reporting the Pages URL. A failing test halts the pipeline before configure-pages runs — nothing broken ships.
Conclusion & Next Steps
You can trigger workflows precisely, structure jobs with dependencies and matrices, cache aggressively, share logic via reusable workflows and composite actions, gate deploys with environments, and defend the pipeline itself. Next: add a nightly schedule audit job, wire OIDC for cloud deploys, and turn on the Security tab’s secret scanning with a test PR.
References / Sources
- GitHub Actions documentation. https://docs.github.com/en/actions
- GitHub Docs: reusable workflows. https://docs.github.com/en/actions/sharing-automations/reusing-workflows
- GitHub Docs: security hardening for GitHub Actions. https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
- GitHub Docs: deploying with GitHub Pages and Actions. https://docs.github.com/en/pages/getting-started-with-github-pages/using-github-pages-with-github-actions