DS DevShelfHub Projects · AI tools
Cheatsheets / GitHub Actions
Cheatsheet · Dev tooling

GitHub Actions Cheatsheet: Workflows, Jobs and Matrix Reference

By DevShelfHub

Workflows, triggers, jobs, steps, runners, contexts, secrets, matrix, caching, reusable workflows — the daily CI surface.

112 items 8 min Workflows Jobs Matrix

Start hereQuick start · 6 you’ll reach for daily

File.github/workflows/ci.yml
Triggeron: [push, pull_request]
Runnerruns-on: ubuntu-latest
Step- uses: actions/checkout@v4
Secret{{ secrets.X }}
Matrixstrategy.matrix.…

Target versions · paceVersions

Targets: workflow syntax v2 node20 runtime (default) actions/* ≥ v4

Snippets target the current syntax. Reach for actions/checkout@v4, actions/setup-*@v5, actions/cache@v4 — older versions either run on the deprecated Node 16 runtime or have known security issues. Pin third-party actions by full commit SHA, not branch / tag.

files · toolingSetup

.github/workflows/*.ymlWorkflow definitions. One file per workflow.
.github/actions/<name>/action.ymlRepo-local composite / JS action.
gh workflow list / run / viewCLI inspect + trigger.
gh run rerun <id>Re-run failed jobs from the CLI.
act -W .github/workflows/ci.ymlRun workflows locally in Docker.
actionlint .github/workflows/*.ymlStatic lint. Catches expressions + matrix bugs.
VS Code: GitHub Actions extensionInline schema + autocomplete.

top-level structureWorkflows

name: CIDisplay name in the UI.
on: push / pull_request / schedule / workflow_dispatchTrigger list.
permissions: { … }Token scope. Default to least privilege.
env: { KEY: val }Workflow-wide env. Job + step env: override.
defaults.run.shell / working-directoryApply to every run: step.
concurrency: { group, cancel-in-progress }Coalesce duplicate / superseded runs.
jobs: { id: { … } }Run in parallel by default. Use needs: for dependencies.
on.workflow_dispatch.inputsManually-triggerable inputs from the UI / CLI.
yaml
# .github/workflows/ci.yml — typical PR + push workflow
name: CI

on:
  push:
    branches: [main]
  pull_request:
    paths-ignore: ["docs/**", "**/*.md"]

permissions: { contents: read }

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        python: ["3.11", "3.12", "3.13"]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
          cache: pip
      - run: pip install -r requirements.txt
      - run: pytest -q --cov=src --cov-report=xml
      - uses: codecov/codecov-action@v4
        if: success()
        with:
          file: coverage.xml

when to runTriggers

on: pushAny branch unless filtered.
on: push: { branches: [main], tags: ['v*.*.*'] }Filtered.
on: push: { paths: ['src/**'] }Run only when matching files change.
on: push: { paths-ignore: ['docs/**'] }Skip docs-only changes.
on: pull_request: { types: [opened, synchronize] }Limit PR event types.
on: pull_request_targetCaution Runs with repo perms on forks. Security-sensitive.
on: schedule: { cron: '0 6 * * *' }Cron syntax (UTC).
on: workflow_dispatchManual trigger button.
on: workflow_callReusable workflow target.
on: workflow_run / repository_dispatchReact to other workflows / external API.
on: issues / issue_comment / release / deploymentMany more event types.

parallel units of workJobs

runs-on: ubuntu-latest / macos-14 / windows-latestPick a hosted runner.
runs-on: [self-hosted, linux, x64]Self-hosted runner labels.
needs: [job1, job2]Wait for upstream jobs.
if: github.event_name == 'push'Job-level conditional.
timeout-minutes: 15Cap runtime. Always set.
continue-on-error: trueJob fails → workflow still passes. Use sparingly.
outputs: { x: steps.s.outputs.value }Expose values to downstream jobs.
environment: { name, url }Bind a job to a protected environment (approval / secrets).
container: image: node:20Run job inside a container instead of the host runner.
services: { postgres: { image, ports, … } }Sidecar service containers.

inside a jobSteps

- uses: actions/checkout@v4Invoke a published action.
- run: echo hiShell command. Default bash (Linux) / pwsh (Windows).
- run: |
multi-line
script
Block scalar for multi-line scripts.
- name: "Friendly label"What shows up in the UI.
id: buildReference this step from steps.build.outputs.….
with: { input: value }Inputs for the action.
env: { KEY: val }Step-scoped env vars.
if: success() / failure() / always() / cancelled()Step-level conditions.
continue-on-error: trueStep fails but doesn’t fail the job.
working-directory: ./servicePer-step cwd.
shell: bash / pwsh / pythonOverride the shell.

where it runsRunners

ubuntu-latest / ubuntu-24.04 / ubuntu-22.04Pin a specific image when reproducibility matters.
macos-latest / macos-14 / macos-14-xlargeApple Silicon defaults. xlarge = paid larger runner.
windows-latest / windows-2022Windows server runner.
larger runners (paid)More vCPU / RAM / GPUs via Settings → Runners.
runs-on: [self-hosted, linux, x64, gpu]Custom labels on your own infra.
RUNNER_OS / RUNNER_TEMP / RUNNER_TOOL_CACHEBuilt-in env vars for portability.
setup-* actions populate tool cachesetup-python, setup-node, … cache toolchains.
runners installed software is documentedgithub.com/actions/runner-images. Don’t reinstall what’s already there.

reusable stepsActions

uses: actions/checkout@v4Official action by name + ref.
uses: org/repo/path/to/action@v1Sub-path inside a repo.
uses: ./.github/actions/myactionLocal composite action.
uses: docker://image:tagRun a public Docker image as an action.
Pin by commit SHAPreferred for third-party. Tags can move.
Action types: JavaScript / Composite / DockerJS = fastest. Composite = grouping. Docker = isolated env.
action.yml: inputs / outputs / runsSchema for an action you author.
marketplace.github.comDiscover community actions.

expression syntaxContexts & expressions

{{ github.* }}Event + repo info: github.ref, github.sha, github.actor.
{{ env.X }}Workflow / job / step env.
{{ secrets.X }}Secrets. Masked in logs (still risky in echo).
{{ vars.X }}Org / repo / environment variables (non-secret).
{{ steps.s.outputs.x }}Step output by id.
{{ needs.job.outputs.x }}Upstream-job output.
{{ matrix.python }}Matrix axes inside a job.
{{ runner.os }}Linux / macOS / Windows. Use to branch shell.
{{ inputs.x }}Workflow dispatch / reusable workflow inputs.
Functions: contains, startsWith, endsWith, format, fromJSON, toJSON, hashFiles, success, failure, cancelled, alwaysBuilt-in expression functions.
if: {{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}Boolean expressions; expression is implied on if:.

tokens, OIDC, least privilegeSecrets & permissions

permissions: { contents: read }Workflow-wide token scope. Default to read.
permissions: { id-token: write }Required to mint OIDC tokens for cloud auth.
{{ secrets.GITHUB_TOKEN }}Auto-issued repo token. Scope follows permissions:.
{{ secrets.MY_KEY }}User-defined repo / org / environment secret.
env: { MY_KEY: {{ secrets.MY_KEY }} }Preferred Inject as env, not on the command line.
OIDC → cloud: aws-actions/configure-aws-credentials@v4Keyless auth using federated identity.
environment: { name: production }Gate deploys behind required reviewers + secrets.
Mask sensitive values you compute: ::add-mask::Workflow command to add runtime secrets.
Never echo a secretEven masked, it can leak via base64 / chunking.
yaml
# .github/workflows/deploy.yml — tag-driven deploy with OIDC + environments
name: Deploy

on:
  push:
    tags: ["v*.*.*"]

permissions:
  id-token: write          # required for OIDC -> AWS / GCP
  contents: read

jobs:
  release:
    runs-on: ubuntu-latest
    environment:
      name: production     # requires manual approval if env is gated
      url: https://api.example.com

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123:role/gha-deploy
          aws-region:     us-east-1

      - name: Build + push image
        run: |
          docker build -t $IMAGE:$GITHUB_REF_NAME .
          docker push $IMAGE:$GITHUB_REF_NAME

      - name: Deploy
        run: ./scripts/deploy.sh $GITHUB_REF_NAME
        env:
          IMAGE: 123.dkr.ecr.us-east-1.amazonaws.com/app

fan out across versions / OSsMatrix

strategy.matrix.X: […]Fan out N runs per axis value.
strategy.matrix: { os: […], py: […] }Cross-product.
strategy.matrix.include: [{ os, py, extra }]Add specific combos.
strategy.matrix.excludeDrop specific combos.
strategy.fail-fast: falseDon’t cancel siblings on first failure.
strategy.max-parallel: 4Throttle concurrency.
{{ matrix.os }}Per-run value access.
jobs.test.name: "test ${{ matrix.os }} / py${{ matrix.py }}"Friendly per-run label in the UI.

speed up buildsCaching

uses: actions/cache@v4Manual cache by path + key.
key: deps-${{ hashFiles('**/requirements.txt') }}Hash inputs → cache key. Invalidates correctly.
restore-keys: deps-Prefix fallback for partial hits.
setup-python / setup-node / setup-go with `cache:`Built-in cache for stdlib package managers.
docker/build-push-action: cache-from / cache-to: type=ghaBuildKit cache backed by Actions.
Cache scoped per-branch + mainBranches read from main if no branch hit.
10 GB total cache per repoOld entries evicted LRU. Aim for keys that share well.
Artifacts: actions/upload-artifact@v4 / download-artifact@v4Persist per-run files (logs, binaries) for 90 days.

workflow_callReusable workflows

on: workflow_callMarks a workflow as callable.
inputs: { type: string / number / boolean }Typed inputs.
secrets: { X: { required: true } } / secrets: inheritExplicit pass-through, or inherit from caller.
outputs: { x: ${{ jobs.j.outputs.v }} }Surface workflow-level outputs to the caller.
uses: org/repo/.github/workflows/wf.yml@v1Call across repos. Pin by SHA / tag.
uses: ./.github/workflows/_wf.ymlSame-repo reusable workflow.
Composite action vs reusable workflowAction = group of steps; reusable workflow = whole jobs + env + permissions.
yaml
# .github/workflows/_test.yml — reusable: callable from other workflows
name: Test (reusable)

on:
  workflow_call:
    inputs:
      python:
        type: string
        default: "3.12"
    secrets:
      CODECOV_TOKEN:
        required: false
    outputs:
      coverage:
        description: "Coverage percent"
        value: ${{ jobs.test.outputs.cov }}

jobs:
  test:
    runs-on: ubuntu-latest
    outputs:
      cov: ${{ steps.cov.outputs.value }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: ${{ inputs.python }} }
      - run: pip install -r requirements.txt
      - run: pytest --cov=src
      - id: cov
        run: echo "value=$(coverage report --format=total)" >> "$GITHUB_OUTPUT"

# Caller: .github/workflows/ci.yml
# jobs:
#   tests:
#     uses: ./.github/workflows/_test.yml
#     with: { python: "3.13" }
#     secrets: inherit

lint · test · build · publishEnd-to-end · Release pipeline

Concurrency-gated workflow: lint, then a reusable test job, then a tag-only Docker build to GHCR with BuildKit cache. The shape most CI/CD-on-Actions setups converge on.

yaml
# .github/workflows/release.yml — lint -> test -> build -> publish, in one file.
name: Release

on:
  push:
    branches: [main]
    tags:     ["v*.*.*"]

permissions:
  contents: read
  packages: write          # to push to GHCR

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.13", cache: pip }
      - run: pip install ruff && ruff check .

  test:
    needs: lint
    uses: ./.github/workflows/_test.yml
    with: { python: "3.13" }
    secrets: inherit

  build:
    needs: test
    if: startsWith(github.ref, 'refs/tags/v')
    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/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
          cache-from: type=gha
          cache-to:   type=gha,mode=max

Best practiceGood to know

Default permissions: { contents: read }. The default token has broad write scope. Drop it at the workflow level; opt back into id-token: write or packages: write only on the job that needs it.
Pin third-party actions by SHA. Tags can be moved; SHAs can’t. Dependabot will open PRs to bump SHA + comment with the tag for review.
Wire concurrency to cancel stale PR runs. Two pushes to the same PR shouldn’t both run to completion. cancel-in-progress: true on pull_request saves real minutes.

Common trapsWatch out for

pull_request_target on forks is dangerous. It runs with full repo permissions on PRs from forks — including malicious code if you check out the head SHA. Don’t check out untrusted refs in that context.
Inline secrets in run: commands leak via process trees. run: curl -H "Authorization: Bearer ${{ secrets.X }}" may end up in logs / ps output. Set it as env: and reference $X.
Matrix expansion is implicit. Without fail-fast: false a single broken combo cancels its siblings — you lose visibility on whether the rest would have passed.

Go deeperSee also

GitHub Actions FAQ

What is GitHub Actions used for?

GitHub Actions is GitHub's built-in CI/CD platform. It runs automated workflows — tests, builds, deployments, and custom automations — triggered by repository events like push, pull_request, or schedule. Workflows are defined as YAML files in .github/workflows/.

What is the difference between a job and a step in GitHub Actions?

A job is a unit of work that runs on a single runner (virtual machine). Each job is composed of steps, which are individual shell commands or actions that run sequentially inside that runner. Jobs run in parallel by default; add needs: [other-job] to create dependencies between them.

How do I use secrets in GitHub Actions?

Add secrets in your repo or org settings under Settings > Secrets and variables > Actions. Reference them in your workflow as ${{ secrets.MY_SECRET }}. Secrets are masked in logs and not passed to workflows triggered by forks (pull_request from forks). Use environments for deployment secrets with required reviewers.

What is a matrix strategy in GitHub Actions?

A matrix strategy lets you run a job across multiple combinations of variables — for example, testing against Python 3.10, 3.11, and 3.12 at once. Define it with strategy.matrix, reference values with ${{ matrix.python-version }}, and use exclude or include to fine-tune which combinations run.

How does caching work in GitHub Actions?

Use actions/cache to save and restore directories (e.g., node_modules, pip cache, Maven .m2) between workflow runs. Specify a key (usually built from a hash of your lockfile) and restore-keys for fallback. Cache hits are restored before your steps run; a miss writes the cache at the end of the job.

What are reusable workflows in GitHub Actions?

Reusable workflows let you call one workflow from another using uses: org/repo/.github/workflows/deploy.yml@main. The called workflow defines inputs and secrets it accepts; the caller passes them. This avoids duplicating pipeline logic across multiple repos — useful for org-wide deploy or release patterns.