Skip to content
Commitpedia
Log in Join & edit

GitHub Actions cheat sheet: triggers, matrix, secrets, caching

The GitHub Actions syntax you reach for most, in copy-ready blocks: event triggers and filters, build matrices, secrets and permissions, dependency caching and artifacts, concurrency, job outputs and conditions.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of GitHub Actions cheat sheet: triggers, matrix, secrets, caching: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
5 min

Workflows live in .github/workflows/*.yml. Each has triggers (on), one or more jobs that run on separate runners in parallel unless linked with needs, and each job has steps that run in sequence on one runner. Everything below is current syntax; pin third-party actions to a major version (@v4) or, for stricter supply-chain control, to a full commit SHA.

Triggers

on:
  push:
    branches: [main, "release/**"]
    tags: ["v*"]
    paths-ignore: ["docs/**", "*.md"]
  pull_request:
    types: [opened, synchronize, reopened]
  schedule:
    - cron: "30 2 * * 1-5"
  workflow_dispatch:
    inputs:
      environment:
        description: Target environment
        type: choice
        options: [staging, production]
        default: staging
  workflow_call:
  • branches, tags, paths and their -ignore variants filter push and pull_request; globs like ** are allowed.
  • schedule uses POSIX cron in UTC and only runs on the default branch. Scheduled workflows are disabled after 60 days without repository activity.
  • workflow_dispatch adds a Run workflow button; inputs are read as ${{ inputs.environment }}.
  • workflow_call makes the workflow reusable from others with uses: owner/repo/.github/workflows/name.yml@main.
  • pull_request_target runs in the context of the base branch with write permissions; never check out and execute code from the pull request under it.

Matrix

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      max-parallel: 4
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [20, 22]
        include:
          - os: ubuntu-latest
            node: 24
            experimental: true
        exclude:
          - os: windows-latest
            node: 20
    continue-on-error: ${{ matrix.experimental == true }}
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - run: npm ci
      - run: npm test
  • Every combination of the lists runs as its own job; include adds extra combinations or extra keys, exclude removes some.
  • fail-fast: false keeps the other jobs running after one fails; the default cancels them.
  • continue-on-error at job level marks the job as passed even if it fails, useful for experimental versions.

Secrets, variables and permissions

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v5
      - name: Deploy
        env:
          API_TOKEN: ${{ secrets.API_TOKEN }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: ./deploy.sh
  • Secrets are set under Settings > Secrets and variables > Actions at repository, environment or organization level and read as ${{ secrets.NAME }}; they are masked in logs. Non-secret settings go in Variables and are read as ${{ vars.NAME }}.
  • Pass secrets to commands through env, not by splicing them into run strings, so they do not appear in the shell trace.
  • secrets.GITHUB_TOKEN is created per run. The top-level permissions block sets its scopes for every job; a job-level block overrides it. Start with contents: read and add only what a job needs.
  • id-token: write enables OpenID Connect so cloud providers can trust the run without long-lived keys.
  • environment: production binds the job to an environment with its own secrets, required reviewers and deployment branch rules.
  • Secrets are not passed to workflows triggered by pull_request from forks, and the token is read-only there.

Caching and artifacts

steps:
  - uses: actions/checkout@v5

  - uses: actions/cache@v4
    with:
      path: ~/.cargo/registry
      key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
      restore-keys: |
        cargo-${{ runner.os }}-

  - run: cargo test

  - uses: actions/upload-artifact@v4
    if: always()
    with:
      name: test-report
      path: target/report/
      retention-days: 7
  • actions/cache restores path when key matches exactly and falls back to the newest entry matching a restore-keys prefix; the cache is saved at the end of the job if the exact key was not found. Caches are evicted after 7 days without use and the repository total is capped at 10 GB.
  • hashFiles hashes lock files so the key changes when dependencies change.
  • The setup-node, setup-python, setup-go and setup-java actions have a cache input that handles the package manager cache without a separate cache step.
  • Artifacts (upload-artifact and download-artifact) move files between jobs in a run and keep them for download; caches are for speeding up future runs.

Concurrency, outputs, conditions and dependencies

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    outputs:
      version: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v5
      - id: meta
        run: echo "version=$(git describe --tags --always)" >> "$GITHUB_OUTPUT"
      - run: echo "BUILD_DIR=dist" >> "$GITHUB_ENV"
      - run: echo "### Built ${{ steps.meta.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"

  release:
    needs: build
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - run: echo "releasing ${{ needs.build.outputs.version }}"
  • concurrency with cancel-in-progress stops an older run of the same workflow on the same branch when a new push arrives.
  • timeout-minutes kills a hung job; the default is 360 minutes.
  • Steps write outputs by appending name=value to $GITHUB_OUTPUT; jobs expose them under outputs and dependants read needs.JOB.outputs.NAME. $GITHUB_ENV sets environment variables for later steps, $GITHUB_STEP_SUMMARY renders Markdown on the run page.
  • needs orders jobs; if accepts expressions with github.event_name, github.ref, startsWith, contains, success(), failure() and always().
  • Useful contexts: github.sha, github.actor, github.repository, runner.os, runner.temp, steps.ID.outcome.

Services and containers

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: test
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v5
      - run: psql postgresql://postgres:test@localhost:5432/postgres -c 'select 1'

services starts side containers for the job and waits for their health checks; the job reaches them on localhost when it runs on the VM directly, or by service name when the job itself runs in a container.

Debugging

  • Re-run a failed run with Re-run jobs > Enable debug logging, or set the repository secret ACTIONS_STEP_DEBUG to true.
  • Print the whole event payload once with run: echo '${{ toJSON(github.event) }}' to see which fields are available.
  • Run workflows locally with nektos/act before pushing.
  • gh run list, gh run view --log and gh run rerun from the GitHub CLI do the same as the web interface from a terminal.

Official documentation

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

Repos in the wild

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +7,565 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?