GitHub Actions Deep Dive: Workflows, Matrix Builds, and Secrets
GitHub Actions Deep Dive: Workflows, Matrix Builds, and Secrets
GitHub Actions looks simple until you need matrix builds, reusable workflows, and environment protection rules. This is the deeper tour — the syntax and features you reach for after the quickstart.
Table of Contents
- Anatomy: where things live
- Contexts and expressions
- Matrix builds: test everything at once
- Job dependencies, artifacts, and outputs
- Secrets and environments
- Caching: the speed knob
- Starter workflows and reuse
- Pitfalls worth knowing
Anatomy: where things live
Workflows are YAML files in .github/workflows/. That's the hard rule — anywhere else and GitHub won't see them. A workflow has events that trigger it (on:), jobs that run in parallel by default, and steps inside each job that run sequentially on one runner.
name: ci
on:
push:
branches: [main]
pull_request:
workflow_dispatch: # manual button
schedule:
- cron: "0 6 * * 1" # Monday 06:00 UTC
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
Contexts and expressions
${{ }} expressions pull from contexts — the runtime data model:
${{ github.sha }} # commit that triggered
${{ github.ref_name }} # branch/tag name
${{ github.actor }} # who triggered it
${{ github.event_name }} # push, pull_request, ...
${{ runner.os }} # Linux/Windows/macOS
${{ secrets.AWS_KEY }} # stored secrets
${{ vars.APP_ENV }} # repo/environment variables
${{ job.status }} # running status
Print the whole event payload when debugging: run: echo '${{ toJSON(github) }}'.
Matrix builds: test everything at once
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
Four jobs, two lines of matrix. fail-fast: false keeps other combos running when one fails.
Job dependencies, artifacts, and outputs
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- id: v
run: echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with: { name: dist, path: dist/ }
deploy:
needs: build # waits for build
environment: production # gates + secrets scoping
steps:
- uses: actions/download-artifact@v4
with: { name: dist }
- run: echo "deploying ${{ needs.build.outputs.version }}"
Secrets and environments
Settings → Secrets and variables → Actions: repo secrets for everything, environment secrets scoped to deploy targets. Attach protection rules to an environment: — required reviewers, restricted branches — and a deploy job pauses for approval before running.
For cloud auth, skip static keys entirely: use OIDC federation with permissions: id-token: write and assume a short-lived role. No secrets in GitHub at all.
Caching: the speed knob
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm # one line: restores node_modules by lockfile hash
For anything else, actions/cache with a key derived from the dependency lockfile. Uncached CI is slow CI.
Starter workflows and reuse
GitHub suggests ready-made workflows in Actions → New workflow (the actions/starter-workflows repo powers this). For organization-wide patterns, reusable workflows (on: workflow_call) let one canonical pipeline serve every repo.
Pitfalls worth knowing
- Workflows only run on the branch/tag rules you give on: — a typo silently disables CI.
- pull_request from forks runs with read-only tokens and no secrets (by design, for security).
- Each job gets a fresh VM — pass state via artifacts, not the filesystem.
- Pin actions to major versions (@v4) or full SHAs for supply-chain safety.
Start from a starter workflow, then grow into matrix builds and OIDC deploys as the need arrives — Actions rewards incremental adoption.
Related Articles
- CI/CD Pipelines Explained: From Commit to Production
- GitHub Actions vs GitLab CI vs Jenkins: How to Choose
Last Updated: October 2026 Author: CloudOpsGuide Team Difficulty: Intermediate Estimated Reading Time: 13 minutes