Matrix Builds and Deployment Gates¶
Matrix: one job definition, many parallel variants¶
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: ["18", "20"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
A matrix with two dimensions of two values each expands into 4
actual parallel jobs — every combination — without writing four
separate job blocks. This is the direct way to confirm something
works across every OS/version combination a project supports,
instead of testing only one and hoping the others behave the same.
Deployment environments and protection rules¶
jobs:
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- run: ./deploy.sh
Declaring environment: production on a job lets that environment
carry protection rules configured in the repository settings —
commonly, a required reviewer who must manually approve before the
job proceeds, even though the workflow already triggered and the
build job already passed. This is the concrete mechanism behind
"code is merged and CI is green, but a human still approves the
actual production deploy" — the gate lives in the deployment job's
environment configuration, not in the trigger or the build step.
Secrets¶
steps:
- run: ./deploy.sh
env:
API_KEY: ${{ secrets.PROD_API_KEY }}
Secrets configured in repository (or environment-specific) settings
are injected as environment variables at run time — never checked
into the workflow YAML itself, and GitHub automatically masks a
secret's value in logs if it ever appears in output, replacing it
with ***. Environment-scoped secrets (rather than repository-wide)
let a PROD_API_KEY only be accessible to jobs explicitly running
under the production environment — a staging deploy job simply
cannot read it, which is a real access boundary, not just an
organizational convention.
Concurrency: cancelling superseded runs¶
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
Without this, pushing three commits in quick succession to the same
branch queues three full deploy runs, potentially deploying an older
commit after a newer one if they finish out of order. concurrency
groups runs by a key (here, the branch ref) and cancel-in-progress
automatically cancels an already-running deploy for that same group
the moment a newer one starts — ensuring the last triggered run is
the one that actually completes and deploys, not whichever happened
to finish first.
Common pitfall¶
Relying on required-reviewer approval as the only safeguard before a
production deploy, without also gating on the actual build/test
results via needs:, can let a human approve a deploy whose
underlying build silently failed or was skipped — the approval gate
and the "did the code actually pass CI" gate are two separate
mechanisms, and forgetting to wire needs: build (or equivalent)
into the deploy job means the environment's protection rule is
guarding against the wrong risk, approving when to deploy without
actually confirming what's being deployed is sound.