# Code Reference — GitHub Actions

_23 pages_

---
# GitHub Actions (/docs/github-actions)



# GitHub Actions [#github-actions]

CI/CD workflows.

Browse the sidebar for every cheat sheet in this section.

## Sections [#sections]

* [Examples](./examples)


---

# Artifacts (/docs/github-actions/artifacts)



# Artifacts [#artifacts]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Artifacts persist files between jobs or after a workflow finishes (logs, builds, test reports). Upload with `actions/upload-artifact`, download with `actions/download-artifact`. Retention is limited; use releases or external storage for long-term binaries.

## 🔧 Core concepts [#-core-concepts]

| Piece               | Role                         |
| ------------------- | ---------------------------- |
| `upload-artifact`   | Store paths from a job       |
| `download-artifact` | Restore into another job/run |
| `retention-days`    | Override default retention   |
| `name`              | Artifact identifier          |
| `path`              | File/glob to store           |
| `if-no-files-found` | `warn` / `error` / `ignore`  |

Artifacts are per-workflow-run. Jobs that need build outputs from another job must download explicitly—workspace is not shared.

## 💡 Examples [#-examples]

**Build → test jobs:**

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 7

  verify:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist
      - run: ls -R dist
```

**Multiple paths:**

```yaml
- uses: actions/upload-artifact@v4
  with:
    name: reports
    path: |
      coverage/
      test-results/
```

## ⚠️ Pitfalls [#️-pitfalls]

* Artifact v3/v4 APIs differ—match upload/download major versions.
* Uploading huge `node_modules` wastes storage—upload only build outputs.
* Name collisions across matrix cells—include `$\{\{ matrix.* \}\}` in the name.
* Expired artifacts disappear—don’t rely on them as a package registry.
* Sensitive files in artifacts are downloadable by users with permission—filter carefully.
* Compression and path layout differ; always `ls` after download in CI once.

## 🔗 Related [#-related]

* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [caching.md](/docs/github-actions/caching)
* [matrix.md](/docs/github-actions/matrix)
* [github\_pages.md](/docs/github-actions/github-pages)
* [docker.md](/docs/github-actions/docker)


---

# Caching (/docs/github-actions/caching)



# Caching [#caching]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Caching restores dependencies between runs with `actions/cache` or setup-action built-in caches (`setup-node`, `setup-python`). Good keys save minutes; bad keys cause stale builds. Caches are scoped per branch with restore fallbacks.

## 🔧 Core concepts [#-core-concepts]

| Piece          | Role                                 |
| -------------- | ------------------------------------ |
| `key`          | Exact cache identity                 |
| `restore-keys` | Prefix fallbacks                     |
| `path`         | Directories to save                  |
| Hit / miss     | Logged in step output                |
| Branch scope   | Default branch caches more shareable |
| Size limits    | Eviction when over quota             |

Prefer official setup actions’ `cache: true` / `cache-dependency-path` when available—they encode good defaults.

## 💡 Examples [#-examples]

**Node via setup-node:**

```yaml
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: npm
    cache-dependency-path: package-lock.json
- run: npm ci
```

**Explicit actions/cache:**

```yaml
- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-
```

**Go modules sketch:**

```yaml
- uses: actions/cache@v4
  with:
    path: |
      ~/.cache/go-build
      ~/go/pkg/mod
    key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
```

## ⚠️ Pitfalls [#️-pitfalls]

* Don’t cache build outputs that must be clean—cache package managers, not `dist`, unless intentional.
* Overly broad `restore-keys` can restore wrong versions and cause subtle bugs.
* Post-job save fails if the job was canceled early—understand when saves run.
* OS must be part of the key when paths differ across runners.
* Secrets in cached paths can persist—exclude credential dirs.
* Cache is not a secure artifact store or substitute for lockfiles.

## 🔗 Related [#-related]

* [setup\_node.md](/docs/github-actions/setup-node)
* [setup\_python.md](/docs/github-actions/setup-python)
* [artifacts.md](/docs/github-actions/artifacts)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [runners.md](/docs/github-actions/runners)


---

# Checkout (/docs/github-actions/checkout)



# Checkout [#checkout]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

`actions/checkout` clones your repository into the runner workspace. It is usually the first step. Configure depth, ref, submodules, and credentials for push or private dependencies.

## 🔧 Core concepts [#-core-concepts]

| Input                 | Role                                    |
| --------------------- | --------------------------------------- |
| `ref`                 | Branch, tag, or SHA                     |
| `fetch-depth`         | `0` = full history; `1` default shallow |
| `submodules`          | `true` / `recursive`                    |
| `token`               | PAT or `GITHUB_TOKEN`                   |
| `persist-credentials` | Leave creds for later git ops           |
| `path`                | Clone into subdirectory                 |
| `lfs`                 | Fetch Git LFS files                     |

Default checkout uses the triggering commit SHA for most events. For PRs, it checks out the merge commit unless configured otherwise.

## 💡 Examples [#-examples]

**Standard:**

```yaml
- uses: actions/checkout@v4
```

**Full history + tags (changelogs):**

```yaml
- uses: actions/checkout@v4
  with:
    fetch-depth: 0
    fetch-tags: true
```

**Push commits back:**

```yaml
- uses: actions/checkout@v4
  with:
    token: ${{ secrets.GH_PAT }}
    persist-credentials: true
- run: |
    git config user.name "github-actions[bot]"
    git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
    # … commit …
    git push
```

**Subdirectory:**

```yaml
- uses: actions/checkout@v4
  with:
    path: app
```

## ⚠️ Pitfalls [#️-pitfalls]

* Shallow clones break `git describe` / some version tools—raise `fetch-depth`.
* `persist-credentials: true` + write token can allow unexpected pushes.
* Private submodules need a token with access.
* `pull_request_target` + checkout of PR head is a classic secret-exfiltration risk.
* LFS failures are silent if not enabled—set `lfs: true` when needed.
* Multiple checkouts need distinct `path` values.

## 🔗 Related [#-related]

* [permissions.md](/docs/github-actions/permissions)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [secrets\_env.md](/docs/github-actions/secrets-env)
* [composite\_actions.md](/docs/github-actions/composite-actions)
* [events\_triggers.md](/docs/github-actions/events-triggers)


---

# Composite Actions (/docs/github-actions/composite-actions)



# Composite Actions [#composite-actions]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Composite actions bundle multiple steps into a reusable `action.yml` with `runs.using: composite`. Unlike reusable workflows (job-level), composites plug in as a single step. Ideal for shared setup sequences within or across repos.

## 🔧 Core concepts [#-core-concepts]

| Piece                   | Role                                            |
| ----------------------- | ----------------------------------------------- |
| `action.yml`            | Metadata + inputs/outputs                       |
| `runs.using: composite` | Step bundle                                     |
| `runs.steps`            | Same shape as workflow steps                    |
| `inputs`                | Parameters from `with:`                         |
| `outputs`               | Mapped from step outputs                        |
| Location                | `org/repo/path@ref` or `./.github/actions/name` |

JavaScript and Docker actions are alternatives when you need richer logic or containers.

## 💡 Examples [#-examples]

**`.github/actions/setup-app/action.yml`:**

```yaml
name: Setup app
description: Checkout deps already assumed; install toolchains
inputs:
  node-version:
    required: true
    default: '22'
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: npm
    - run: npm ci
      shell: bash
```

**Use it:**

```yaml
steps:
  - uses: actions/checkout@v4
  - uses: ./.github/actions/setup-app
    with:
      node-version: '22'
  - run: npm test
```

**Output pattern:**

```yaml
outputs:
  version:
    value: ${{ steps.meta.outputs.version }}
runs:
  using: composite
  steps:
    - id: meta
      shell: bash
      run: echo "version=1.2.3" >> "$GITHUB_OUTPUT"
```

## ⚠️ Pitfalls [#️-pitfalls]

* Every `run` step in a composite **must** set `shell:`.
* Composites can’t directly grant extra `permissions`—caller workflow controls token.
* Nested `uses` of other actions is fine; keep versions pinned.
* Don’t confuse with reusable workflows—composites don’t create jobs.
* Local path actions require checkout first.
* Outputs must be declared and wired explicitly.

## 🔗 Related [#-related]

* [reusable\_workflows.md](/docs/github-actions/reusable-workflows)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [checkout.md](/docs/github-actions/checkout)
* [setup\_node.md](/docs/github-actions/setup-node)
* [expressions.md](/docs/github-actions/expressions)


---

# Docker (/docs/github-actions/docker)



# Docker [#docker]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

GitHub Actions can build/push images, run jobs inside containers, and use service containers (Postgres, Redis). Authenticate to GHCR or Docker Hub with secrets or `GITHUB_TOKEN`. Keep tags immutable for releases.

## 🔧 Core concepts [#-core-concepts]

| Pattern                    | Use                           |
| -------------------------- | ----------------------------- |
| `container:` on job        | Run all steps in an image     |
| `services:`                | Sidecar containers with ports |
| `docker build` / buildx    | Image builds                  |
| `docker/login-action`      | Registry auth                 |
| `docker/build-push-action` | Build + push                  |
| GHCR                       | `ghcr.io/owner/name`          |

Job containers still need checkout and careful volume/workspace handling. Services are reachable by hostname = service id.

## 💡 Examples [#-examples]

**Service container:**

```yaml
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      db:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
    steps:
      - uses: actions/checkout@v4
      - run: npm test
```

**Build and push GHCR:**

```yaml
permissions:
  contents: read
  packages: write
steps:
  - uses: actions/checkout@v4
  - 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 }}:latest
```

## ⚠️ Pitfalls [#️-pitfalls]

* `localhost` from a **job container** to a service may need the service name, not localhost—topology differs.
* Layer caching needs explicit buildx cache config.
* Publishing `:latest` only is fragile—also tag SHAs/versions.
* Rootless / permission issues writing workspace files from containers.
* Don’t bake secrets into images; use runtime env.
* Windows containers are a separate ecosystem from Linux images.

## 🔗 Related [#-related]

* [runners.md](/docs/github-actions/runners)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [secrets\_env.md](/docs/github-actions/secrets-env)
* [permissions.md](/docs/github-actions/permissions)
* [artifacts.md](/docs/github-actions/artifacts)


---

# Environments (/docs/github-actions/environments)



# Environments [#environments]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Environments (`environment: production`) gate deployments with protection rules: required reviewers, wait timers, and branch restrictions. Each environment can hold dedicated secrets and variables. Use them to separate staging and production credentials.

## 🔧 Core concepts [#-core-concepts]

| Feature                    | Role                        |
| -------------------------- | --------------------------- |
| `environment:`             | Bind job to named env       |
| `environment.name` / `url` | Name + deployment URL       |
| Protection rules           | Approvals, wait timer       |
| Deployment branches        | Limit which refs can deploy |
| Env secrets/vars           | Scoped credentials          |
| Deployment history         | Visible in repo UI          |

Environments are configured in repo **Settings → Environments**. Public repos have limitations on private env secrets for forks.

## 💡 Examples [#-examples]

**Simple environment:**

```yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
        env:
          TOKEN: ${{ secrets.DEPLOY_TOKEN }}
```

**Dynamic name:**

```yaml
environment: ${{ inputs.target }}
```

**With concurrency:**

```yaml
concurrency:
  group: deploy-production
  cancel-in-progress: false
jobs:
  deploy:
    environment: production
    …
```

## ⚠️ Pitfalls [#️-pitfalls]

* Job waits indefinitely for approval—communicate SLAs to reviewers.
* Environment secrets aren’t available until the job is approved (by design).
* Misnamed environments create new empty envs silently—match Settings names.
* Protection rules don’t replace proper authz in your deploy scripts.
* `url` is informational for the UI; it doesn’t verify the deployment.
* Combining matrix + environment can create many deployment records—name carefully.

## 🔗 Related [#-related]

* [secrets\_env.md](/docs/github-actions/secrets-env)
* [permissions.md](/docs/github-actions/permissions)
* [events\_triggers.md](/docs/github-actions/events-triggers)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [github\_pages.md](/docs/github-actions/github-pages)


---

# Events & Triggers (/docs/github-actions/events-triggers)



# Events & Triggers [#events--triggers]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

The `on` map selects which GitHub events start a workflow. Filter by branches, paths, tags, activity types, and cron schedules. Choose the narrowest trigger to save minutes and reduce noise.

## 🔧 Core concepts [#-core-concepts]

| Event                      | Typical use        |
| -------------------------- | ------------------ |
| `push`                     | CI on commits      |
| `pull_request`             | PR checks          |
| `workflow_dispatch`        | Manual run         |
| `schedule`                 | Cron (UTC)         |
| `release`                  | Publish on release |
| `workflow_call`            | Reusable callee    |
| `repository_dispatch`      | External webhook   |
| `issues` / `issue_comment` | Issue automation   |

Filters: `branches`, `branches-ignore`, `paths`, `paths-ignore`, `tags`, `types`. For PRs, `pull_request` runs in the merge context; `pull_request_target` is privileged—use carefully.

## 💡 Examples [#-examples]

**Push / PR with path filters:**

```yaml
on:
  push:
    branches: [main]
    paths: ['src/**', 'package.json']
  pull_request:
    paths: ['src/**']
```

**Manual + inputs:**

```yaml
on:
  workflow_dispatch:
    inputs:
      environment:
        description: Target
        required: true
        default: staging
        type: choice
        options: [staging, prod]
```

**Cron (every day 06:00 UTC):**

```yaml
on:
  schedule:
    - cron: '0 6 * * *'
```

## ⚠️ Pitfalls [#️-pitfalls]

* Scheduled workflows only run if the default branch contains the file and the repo is active.
* Cron is UTC; expect delays under load.
* `paths` filters don’t apply the way you expect for some events—read docs per event.
* `pull_request_target` + checkout of PR code is a security footgun.
* Fork PRs get a read-only `GITHUB_TOKEN` with restricted secrets.
* Duplicate runs: `push` + `pull_request` both fire for same change—use concurrency or conditionals.

## 🔗 Related [#-related]

* [workflow\_syntax.md](/docs/github-actions/workflow-syntax)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [secrets\_env.md](/docs/github-actions/secrets-env)
* [permissions.md](/docs/github-actions/permissions)
* [environments.md](/docs/github-actions/environments)


---

# Examples (/docs/github-actions/examples)



# Examples [#examples]

GitHub Actions notes in **Examples**.


---

# Node CI (/docs/github-actions/examples/node-ci)



# Node CI [#node-ci]

*Github Actions · Example / how-to*

***

## 📋 Overview [#-overview]

Run install, lint, typecheck, and tests for a Node project on pull requests with caching and a fixed Node version.

## 🔧 Core concepts [#-core-concepts]

| Piece                  | Role         |
| ---------------------- | ------------ |
| `actions/checkout`     | Fetch repo   |
| `actions/setup-node`   | Node + cache |
| Job steps              | Lint → test  |
| `pull_request` trigger | Gate merges  |

## 💡 Examples [#-examples]

**.github/workflows/node\_ci.yml:**

```yaml
name: Node CI

on:
  pull_request:
  push:
    branches: [main]

concurrency:
  group: node-ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: site
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
          cache-dependency-path: site/package-lock.json

      - name: Install
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Typecheck
        run: npm run typecheck

      - name: Test
        run: npm test --if-present
```

## ⚠️ Pitfalls [#️-pitfalls]

* `npm install` in CI drifts locks — prefer `npm ci`.
* Wrong `cache-dependency-path` disables caching silently.
* Fail fast: put cheap lint/typecheck before heavy e2e.

## 🔗 Related [#-related]

* [Path filters](/docs/github-actions/examples/path-filters)


---

# Path Filters (/docs/github-actions/examples/path-filters)



# Path Filters [#path-filters]

*Github Actions · Example / how-to*

***

## 📋 Overview [#-overview]

Skip or select jobs based on which paths changed using `paths` / `paths-ignore` filters (and optional path-filter actions).

## 🔧 Core concepts [#-core-concepts]

| Piece           | Role                            |
| --------------- | ------------------------------- |
| `on.push.paths` | Only run when paths match       |
| `paths-ignore`  | Skip doc-only changes           |
| Multiple jobs   | Different filters per area      |
| Fork PRs        | Filters still apply to PR files |

## 💡 Examples [#-examples]

**.github/workflows/path\_filters.yml:**

```yaml
name: Path filters

on:
  pull_request:
    paths-ignore:
      - "**/*.md"
      - "docs/**"
  push:
    branches: [main]
    paths:
      - "site/**"
      - ".github/workflows/path_filters.yml"

jobs:
  site:
    if: github.event_name == 'pull_request' || contains(join(github.event.commits.*.modified, ','), 'site/') || true
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
          cache-dependency-path: site/package-lock.json
      - run: npm ci
        working-directory: site
      - run: npm run lint
        working-directory: site

  python:
    runs-on: ubuntu-latest
    # Alternative: dorny/paths-filter for multi-job path detection
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            python:
              - 'Python/**'
              - '**/requirements*.txt'
      - if: steps.filter.outputs.python == 'true'
        run: echo "Run Python checks here"
```

**Simple docs skip:**

```yaml
on:
  pull_request:
    paths-ignore:
      - "**/*.md"
```

## ⚠️ Pitfalls [#️-pitfalls]

* Workflow file changes should be included in `paths` or the workflow never updates.
* `paths-ignore` on the whole workflow skips all jobs — use job-level filters for mixed repos.
* Required checks that never run can block merges — configure branch protection carefully.

## 🔗 Related [#-related]

* [Node CI](/docs/github-actions/examples/node-ci)


---

# Expressions (/docs/github-actions/expressions)



# Expressions [#expressions]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Expressions (`$\{\{ \}\}`) evaluate values for `if`, `env`, `with`, and names. Contexts expose github metadata, secrets, vars, matrix, needs, and steps outputs. Functions like `contains`, `hashFiles`, and `success()` power conditionals.

## 🔧 Core concepts [#-core-concepts]

| Context            | Contents                              |
| ------------------ | ------------------------------------- |
| `github`           | Event, ref, sha, actor, repository    |
| `env`              | Environment variables                 |
| `secrets` / `vars` | Config                                |
| `matrix`           | Strategy values                       |
| `needs`            | Upstream job results/outputs          |
| `steps`            | Step outputs                          |
| `runner`           | OS, arch, temp                        |
| `inputs`           | `workflow_dispatch` / `workflow_call` |

Common functions: `success()`, `failure()`, `cancelled()`, `always()`, `contains()`, `startsWith()`, `format()`, `toJSON()`, `hashFiles()`, `fromJSON()`.

## 💡 Examples [#-examples]

**Conditionals:**

```yaml
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
if: failure()
if: always() && needs.build.result == 'success'
```

**hashFiles + format:**

```yaml
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
name: Build ${{ format('{0} ({1})', matrix.os, matrix.node) }}
```

**FromJSON for dynamic matrices:**

```yaml
strategy:
  matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
```

**Step output consumption:**

```yaml
if: steps.changes.outputs.app == 'true'
```

## ⚠️ Pitfalls [#️-pitfalls]

* Inside `if:`, the `$\{\{ \}\}` wrapper is optional but mixing quotes is error-prone.
* Secrets are not available in some contexts (e.g. `hashFiles` inputs)—don’t try to hash secrets.
* `github.head_ref` is empty outside PRs—guard with event checks.
* Boolean strings: `'false'` is truthy as a non-empty string—compare carefully.
* `hashFiles` returns empty if no match—keys may collide unexpectedly.
* Multi-line expressions and YAML parsing—prefer single-line `if` when possible.

## 🔗 Related [#-related]

* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [matrix.md](/docs/github-actions/matrix)
* [secrets\_env.md](/docs/github-actions/secrets-env)
* [workflow\_syntax.md](/docs/github-actions/workflow-syntax)
* [events\_triggers.md](/docs/github-actions/events-triggers)


---

# GitHub Pages (/docs/github-actions/github-pages)



# GitHub Pages [#github-pages]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Deploy static sites to GitHub Pages with Actions using `actions/upload-pages-artifact` and `actions/deploy-pages`. Configure the repo to deploy from **GitHub Actions** (not the classic branch). Grant `pages: write` and `id-token: write`.

## 🔧 Core concepts [#-core-concepts]

| Piece                       | Role                                |
| --------------------------- | ----------------------------------- |
| Pages source                | Settings → Pages → GitHub Actions   |
| `upload-pages-artifact`     | Package static files                |
| `deploy-pages`              | Publish artifact                    |
| `environment: github-pages` | Standard env + URL output           |
| Custom domain               | Repo Pages settings + DNS           |
| Project vs user site        | `/\{repo\}` vs `username.github.io` |

Build with any static generator (Next export, Vite, Hugo, MkDocs). Only static assets are served.

## 💡 Examples [#-examples]

**Classic static deploy:**

```yaml
name: Deploy 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@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci && npm run build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: dist
  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4
```

## ⚠️ Pitfalls [#️-pitfalls]

* Forgetting `id-token: write` breaks OIDC-based Pages deploy.
* Wrong artifact `path` uploads empty/wrong site—verify build output dir.
* SPA routing needs `404.html` tricks or platform rules—Pages is static only.
* Base path for project sites (`/repo-name/`) must match bundler `base` config.
* Custom domains require both DNS and Pages settings; HTTPS provisioning takes time.
* Don’t commit secrets into static JS—anything in `dist` is public.

## 🔗 Related [#-related]

* [permissions.md](/docs/github-actions/permissions)
* [artifacts.md](/docs/github-actions/artifacts)
* [environments.md](/docs/github-actions/environments)
* [setup\_node.md](/docs/github-actions/setup-node)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)


---

# Glossary (/docs/github-actions/glossary)



# Glossary [#glossary]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Alphabetical glossary of core GitHub Actions terms for workflows, jobs, runners, secrets, and reusable automation.

## 🔧 Core concepts [#-core-concepts]

| Term              | Definition                                                                            |
| ----------------- | ------------------------------------------------------------------------------------- |
| Action            | A reusable unit of work published for use as a step (`uses:`).                        |
| Artifact          | A file set uploaded from a job and downloadable later in the workflow.                |
| Cache             | Stored dependencies keyed for faster restores across runs.                            |
| Checkout          | The common first step that clones the repository onto the runner.                     |
| Composite action  | An action defined as a sequence of steps in an `action.yml`.                          |
| Concurrency       | Controls that cancel or queue overlapping workflow runs.                              |
| Context           | Built-in objects such as `github`, `env`, `secrets`, and `steps` used in expressions. |
| Environment       | A named deployment target with protection rules and secrets.                          |
| Event             | A repository activity that can trigger a workflow, such as `push`.                    |
| Expression        | A `$\{\{ \}\}` expression evaluated for conditions, inputs, and contexts.             |
| Job               | A set of steps that runs on the same runner.                                          |
| Matrix            | A strategy that expands one job into many variants of inputs.                         |
| Needs             | A job dependency declaring that another job must succeed first.                       |
| OIDC              | Federated auth letting workflows obtain cloud tokens without long-lived keys.         |
| Permission        | Token scopes limiting what `GITHUB_TOKEN` may do.                                     |
| Reusable workflow | A callable workflow invoked by other workflows with `uses:`.                          |
| Runner            | The machine (GitHub-hosted or self-hosted) that executes a job.                       |
| Secret            | An encrypted value exposed to workflows as an environment variable.                   |
| Service container | A Docker service (e.g. Postgres) attached to a job for tests.                         |
| Step              | A single command or action invocation inside a job.                                   |
| Trigger           | The `on:` configuration that starts a workflow for matching events.                   |
| Workflow          | A YAML automation file under `.github/workflows/`.                                    |
| Workflow call     | The event that allows one workflow to be invoked by another.                          |
| Workflow dispatch | A manual trigger that can accept typed inputs.                                        |
| Workflow run      | One execution instance of a workflow.                                                 |

## 💡 Examples [#-examples]

**Workflow trigger and job:**

```yaml
name: CI
on:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - run: npm ci && npm test
```

**Matrix strategy:**

```yaml
strategy:
  matrix:
    node: [20, 22]
    os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
```

**Secrets and expressions:**

```yaml
env:
  API_KEY: ${{ secrets.API_KEY }}
if: ${{ github.ref == 'refs/heads/main' }}
```

## ⚠️ Pitfalls [#️-pitfalls]

* Confusing **job** (runner-scoped) with **step** (sequential unit inside a job).
* Mixing **secrets** (encrypted) with ordinary **env** vars committed in YAML.
* Treating **cache** as an artifact store — caches are best-effort and can miss.
* Equating **reusable workflows** with **composite actions** — different call sites and capabilities.
* Assuming **matrix** failures always fail-fast the same way — `fail-fast` is configurable.

## 🔗 Related [#-related]

* [workflow\_syntax](/docs/github-actions/workflow-syntax)
* [jobs\_steps](/docs/github-actions/jobs-steps)
* [events\_triggers](/docs/github-actions/events-triggers)
* [matrix](/docs/github-actions/matrix)
* [secrets\_env](/docs/github-actions/secrets-env)
* [caching](/docs/github-actions/caching)


---

# Jobs & Steps (/docs/github-actions/jobs-steps)



# Jobs & Steps [#jobs--steps]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Jobs run in parallel by default on separate runners. Steps within a job run sequentially and share the workspace. Use `needs` for dependencies, `if` for conditions, and map outputs between jobs.

## 🔧 Core concepts [#-core-concepts]

| Key                 | Role                  |
| ------------------- | --------------------- |
| `jobs.<id>.runs-on` | Runner label          |
| `needs`             | Job dependencies      |
| `if`                | Conditional execution |
| `steps[].uses`      | Run an action         |
| `steps[].run`       | Shell command         |
| `steps[].name`      | UI label              |
| `steps[].id`        | Reference outputs     |
| `outputs`           | Job-level outputs     |
| `continue-on-error` | Soft-fail step        |
| `timeout-minutes`   | Cap duration          |

Each step can set `working-directory`, `env`, and `shell`. Failures cancel dependent jobs unless configured otherwise.

## 💡 Examples [#-examples]

**Dependent jobs + outputs:**

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - id: meta
        run: echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT"

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying ${{ needs.build.outputs.version }}"
```

**Multi-line script:**

```yaml
- name: Build
  run: |
    npm ci
    npm run build
```

## ⚠️ Pitfalls [#️-pitfalls]

* Jobs don’t share filesystem—pass data via artifacts or outputs.
* `needs` + `if: always()` patterns are easy to get wrong for cleanup jobs.
* Step outputs must be written to `$GITHUB_OUTPUT` (not deprecated `set-output`).
* Matrix jobs expand into many jobs—watch billing and `fail-fast`.
* `working-directory` doesn’t apply to `uses:` the same way—check action docs.
* Shell differences: bash on Linux/macOS, PowerShell default on Windows.

## 🔗 Related [#-related]

* [runners.md](/docs/github-actions/runners)
* [matrix.md](/docs/github-actions/matrix)
* [artifacts.md](/docs/github-actions/artifacts)
* [expressions.md](/docs/github-actions/expressions)
* [workflow\_syntax.md](/docs/github-actions/workflow-syntax)


---

# Matrix (/docs/github-actions/matrix)



# Matrix [#matrix]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

A strategy matrix expands one job into many combinations (OS, language version, flags). Use `include` / `exclude` to refine, `fail-fast` to control cancellation, and `max-parallel` to limit concurrency.

## 🔧 Core concepts [#-core-concepts]

| Key                    | Role                                      |
| ---------------------- | ----------------------------------------- |
| `strategy.matrix`      | Axes of values                            |
| `$\{\{ matrix.os \}\}` | Reference a cell                          |
| `include`              | Add/override combos                       |
| `exclude`              | Remove combos                             |
| `fail-fast`            | Cancel siblings on failure (default true) |
| `max-parallel`         | Cap simultaneous matrix jobs              |

Matrix context is available in `runs-on`, `env`, step `name`, and `if`. Nested objects in matrix are allowed.

## 💡 Examples [#-examples]

**OS × Node:**

```yaml
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [20, 22]
        exclude:
          - os: windows-latest
            node: 20
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm test
```

**Include extra job:**

```yaml
strategy:
  matrix:
    shard: [1, 2, 3]
    include:
      - shard: 1
        extra_flag: '--coverage'
```

## ⚠️ Pitfalls [#️-pitfalls]

* Combinatorial explosion burns minutes—keep axes small.
* `fail-fast: true` can hide sibling failures when debugging flakes.
* Expression errors in matrix values fail the whole job graph.
* Windows vs bash shell differences across OS matrix cells.
* Job names collide in UI—set dynamic `name:` including matrix values.
* Reusable workflows have limits on matrix passthrough—check docs.

## 🔗 Related [#-related]

* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [runners.md](/docs/github-actions/runners)
* [setup\_node.md](/docs/github-actions/setup-node)
* [setup\_python.md](/docs/github-actions/setup-python)
* [expressions.md](/docs/github-actions/expressions)


---

# Permissions (/docs/github-actions/permissions)



# Permissions [#permissions]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

`permissions` limits what `GITHUB_TOKEN` can do. Default token permissions have tightened over time—set least privilege explicitly at workflow or job level. Extra access for packages, PRs, or Pages must be granted deliberately.

## 🔧 Core concepts [#-core-concepts]

| Scope examples    | Values                              |
| ----------------- | ----------------------------------- |
| `contents`        | `read` / `write` / `none`           |
| `actions`         | Manage artifacts/workflows metadata |
| `checks`          | Status checks                       |
| `pull-requests`   | Comment / label PRs                 |
| `id-token`        | `write` for OIDC                    |
| `pages`           | GitHub Pages deploy                 |
| `packages`        | GHCR / packages                     |
| `security-events` | Code scanning uploads               |

Workflow-level permissions apply to all jobs unless a job overrides. `permissions: \{\}` denies all (then grant per job).

## 💡 Examples [#-examples]

**Read-only CI:**

```yaml
permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
```

**PR bot job:**

```yaml
jobs:
  comment:
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              ...context.issue,
              body: 'Thanks for the PR!'
            })
```

**OIDC to cloud:**

```yaml
permissions:
  id-token: write
  contents: read
```

## ⚠️ Pitfalls [#️-pitfalls]

* Missing `pull-requests: write` causes cryptic API 403s when commenting.
* Elevating `contents: write` on untrusted PR workflows is dangerous.
* Org/repo settings can restrict maximum token permissions—workflow can’t exceed them.
* Fork PRs still have reduced secret access regardless of permissions block.
* `actions/checkout` with persist-credentials + write token can push—disable when unused.
* Document why each write scope exists in comments.

## 🔗 Related [#-related]

* [secrets\_env.md](/docs/github-actions/secrets-env)
* [workflow\_syntax.md](/docs/github-actions/workflow-syntax)
* [checkout.md](/docs/github-actions/checkout)
* [github\_pages.md](/docs/github-actions/github-pages)
* [environments.md](/docs/github-actions/environments)


---

# Reusable Workflows (/docs/github-actions/reusable-workflows)



# Reusable Workflows [#reusable-workflows]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Reusable workflows let one workflow call another with `workflow_call` / `uses: org/repo/.github/workflows/file.yml@ref`. Share CI patterns across repos while keeping callers thin. Pass inputs, secrets, and read outputs from called jobs.

## 🔧 Core concepts [#-core-concepts]

| Piece                             | Role                       |
| --------------------------------- | -------------------------- |
| `on.workflow_call`                | Define a callable workflow |
| `inputs` / `secrets`              | Contract for callers       |
| `uses: ./.github/workflows/x.yml` | Same-repo call             |
| `uses: org/repo/...@v1`           | Cross-repo call            |
| `with:` / `secrets:`              | Pass values                |
| `jobs.*.outputs`                  | Return data to caller      |

Called workflows run as jobs from the caller’s perspective (`needs` works). Nesting depth and some features (e.g. certain matrix cases) have limits.

## 💡 Examples [#-examples]

**Callee (`_ci.yml`):**

```yaml
name: Reusable CI
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '22'
    secrets:
      NPM_TOKEN:
        required: false

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      - run: npm test
```

**Caller:**

```yaml
jobs:
  call-ci:
    uses: ./.github/workflows/_ci.yml
    with:
      node-version: '20'
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
```

## ⚠️ Pitfalls [#️-pitfalls]

* Caller must explicitly pass secrets—`secrets: inherit` only when appropriate.
* Version-pin `@ref` for cross-repo reuse; floating branches can break you.
* Not everything is supported inside reusable workflows (check current limits).
* Debugging spans two files—add clear job names.
* Permissions are evaluated carefully across caller/callee—set both as needed.
* Avoid deep nesting; prefer one reusable layer.

## 🔗 Related [#-related]

* [composite\_actions.md](/docs/github-actions/composite-actions)
* [workflow\_syntax.md](/docs/github-actions/workflow-syntax)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [secrets\_env.md](/docs/github-actions/secrets-env)
* [expressions.md](/docs/github-actions/expressions)


---

# Runners (/docs/github-actions/runners)



# Runners [#runners]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Runners execute jobs. GitHub-hosted runners (`ubuntu-latest`, `windows-latest`, `macos-latest`) are ephemeral VMs. Self-hosted runners persist on your hardware—isolate them carefully. Labels select where a job lands.

## 🔧 Core concepts [#-core-concepts]

| Label                       | Notes                         |
| --------------------------- | ----------------------------- |
| `ubuntu-latest`             | Default Linux; version floats |
| `ubuntu-24.04` / `22.04`    | Pin OS image                  |
| `windows-latest`            | PowerShell default shell      |
| `macos-latest`              | Scarcer / costlier            |
| `[self-hosted, linux, x64]` | Custom labels                 |

| Topic      | Detail                                           |
| ---------- | ------------------------------------------------ |
| Tool cache | Preinstalled SDKs on hosted images               |
| Specs      | CPU/RAM vary by label (larger runners available) |
| Networking | Outbound internet; inbound limited               |
| Ephemeral  | Hosted disk resets each job                      |

Image software lists live in `actions/runner-images`.

## 💡 Examples [#-examples]

**Hosted job:**

```yaml
jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - run: lsb_release -a
      - run: node -v || true
```

**Self-hosted:**

```yaml
jobs:
  build:
    runs-on: [self-hosted, linux, gpu]
    steps:
      - uses: actions/checkout@v4
      - run: ./build.sh
```

**Larger runners (org feature):**

```yaml
runs-on: ubuntu-latest-4-cores
```

## ⚠️ Pitfalls [#️-pitfalls]

* `*-latest` moves—pin versions for reproducibility when it matters.
* Self-hosted runners can retain secrets/workspace—use ephemeral runners or strict cleanup.
* Never attach self-hosted runners to public repos without hardening (fork PR risk).
* macOS minutes cost more—use only when required.
* Custom container jobs (`container:`) change the environment vs bare VM.
* Software may be missing—use setup actions instead of assuming host tools.

## 🔗 Related [#-related]

* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [docker.md](/docs/github-actions/docker)
* [matrix.md](/docs/github-actions/matrix)
* [setup\_node.md](/docs/github-actions/setup-node)
* [setup\_python.md](/docs/github-actions/setup-python)


---

# Secrets & Environment Variables (/docs/github-actions/secrets-env)



# Secrets & Environment Variables [#secrets--environment-variables]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Use repository/org/environment **secrets** for sensitive values and **variables** for non-sensitive config. Map them into `env` at workflow, job, or step scope. Secrets are masked in logs but not a full security boundary—avoid echoing them.

## 🔧 Core concepts [#-core-concepts]

| Source              | Access                         |
| ------------------- | ------------------------------ |
| Secrets             | `$\{\{ secrets.NAME \}\}`      |
| Vars                | `$\{\{ vars.NAME \}\}`         |
| `GITHUB_TOKEN`      | Auto-provided job token        |
| `env:`              | Static or expression-based env |
| Environment secrets | Bound via `environment:`       |
| Org secrets         | Shared with selected repos     |

Never commit `.env` with real credentials. Prefer OIDC/`token` exchange over long-lived cloud keys when possible.

## 💡 Examples [#-examples]

**Wire secrets to env:**

```yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    env:
      APP_ENV: ${{ vars.APP_ENV }}
      API_KEY: ${{ secrets.API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
```

**Step-level only:**

```yaml
- name: Publish
  env:
    NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
  run: npm publish
```

**Set env dynamically:**

```yaml
- run: echo "SHA_SHORT=${GITHUB_SHA::8}" >> "$GITHUB_ENV"
```

## ⚠️ Pitfalls [#️-pitfalls]

* Fork PRs usually cannot read repo secrets.
* Masking is best-effort—partial prints / base64 may leak.
* `pull_request_target` can expose secrets to untrusted code if you check out PR SHA unsafely.
* Empty secrets fail differently than missing vars—validate early.
* Don’t store huge blobs in secrets; use OIDC or encrypted storage.
* `GITHUB_TOKEN` permissions are limited by workflow `permissions:` and defaults.

## 🔗 Related [#-related]

* [permissions.md](/docs/github-actions/permissions)
* [environments.md](/docs/github-actions/environments)
* [events\_triggers.md](/docs/github-actions/events-triggers)
* [expressions.md](/docs/github-actions/expressions)
* [workflow\_syntax.md](/docs/github-actions/workflow-syntax)


---

# Setup Node (/docs/github-actions/setup-node)



# Setup Node [#setup-node]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

`actions/setup-node` installs a Node.js version, optionally adds registry auth, and can cache npm/yarn/pnpm dependencies. Pair with `npm ci` for reproducible CI installs.

## 🔧 Core concepts [#-core-concepts]

| Input                   | Role                                        |
| ----------------------- | ------------------------------------------- |
| `node-version`          | e.g. `22`, `22.11.0`, `lts/*`               |
| `node-version-file`     | `.nvmrc` / `.node-version` / `package.json` |
| `cache`                 | `npm` / `yarn` / `pnpm`                     |
| `cache-dependency-path` | Lockfile path(s)                            |
| `registry-url`          | Scoped registry                             |
| `scope`                 | npm scope for auth                          |

Matrix Node versions to catch compatibility issues. Prefer lockfiles committed to the repo.

## 💡 Examples [#-examples]

**npm CI:**

```yaml
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
  with:
    node-version-file: '.nvmrc'
    cache: npm
- run: npm ci
- run: npm test
```

**Matrix:**

```yaml
strategy:
  matrix:
    node: [20, 22]
steps:
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}
      cache: npm
```

**Publish with auth:**

```yaml
- uses: actions/setup-node@v4
  with:
    node-version: 22
    registry-url: 'https://registry.npmjs.org'
- run: npm publish
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

## ⚠️ Pitfalls [#️-pitfalls]

* `npm install` mutates lockfiles—use `npm ci` in CI.
* Cache key misses when lockfile path is wrong (monorepos need `cache-dependency-path`).
* pnpm/yarn need their own install steps and often `packageManager` / corepack.
* Global tools installed in one job aren’t in another—reinstall or use artifacts.
* Windows path lengths / script shells differ—test matrix cells.
* Don’t commit `.npmrc` with tokens; inject via env/secrets.

## 🔗 Related [#-related]

* [setup\_python.md](/docs/github-actions/setup-python)
* [caching.md](/docs/github-actions/caching)
* [matrix.md](/docs/github-actions/matrix)
* [checkout.md](/docs/github-actions/checkout)
* [secrets\_env.md](/docs/github-actions/secrets-env)


---

# Setup Python (/docs/github-actions/setup-python)



# Setup Python [#setup-python]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

`actions/setup-python` installs Python, exposes `python`/`pip`, and can cache pip dependencies. Use version files or matrices for multi-version testing. Prefer `pip install -r` with hashed requirements or lock tools (pip-tools, poetry, uv) for reproducibility.

## 🔧 Core concepts [#-core-concepts]

| Input                   | Role                        |
| ----------------------- | --------------------------- |
| `python-version`        | `3.12`, `3.12.x`            |
| `python-version-file`   | `.python-version`           |
| `cache`                 | `pip` / `pipenv` / `poetry` |
| `cache-dependency-path` | Requirements/lock paths     |
| `architecture`          | `x64` / `arm64`             |

Virtualenvs are optional on clean runners; still useful for clarity. Tox/nox/pytest fit naturally after setup.

## 💡 Examples [#-examples]

**pip + cache:**

```yaml
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: pip
    cache-dependency-path: requirements*.txt
- run: pip install -r requirements.txt
- run: pytest -q
```

**Matrix:**

```yaml
strategy:
  matrix:
    python: ['3.11', '3.12', '3.13']
steps:
  - uses: actions/setup-python@v5
    with:
      python-version: ${{ matrix.python }}
```

**Poetry sketch:**

```yaml
- uses: actions/setup-python@v5
  with:
    python-version-file: '.python-version'
    cache: poetry
- run: pipx install poetry
- run: poetry install --no-interaction
- run: poetry run pytest
```

## ⚠️ Pitfalls [#️-pitfalls]

* System Python on the image ≠ your setup-python version—always run setup first.
* Cache won’t help if dependency files aren’t hashed correctly.
* Compiled wheels differ by OS—matrix Linux/macOS/Windows when shipping binaries.
* `PYTHONPATH` / editable installs can hide packaging issues—test the sdist/wheel occasionally.
* Secrets for private indexes belong in env, not committed `pip.conf`.
* Match black/ruff/mypy versions to local tooling via pins.

## 🔗 Related [#-related]

* [setup\_node.md](/docs/github-actions/setup-node)
* [caching.md](/docs/github-actions/caching)
* [matrix.md](/docs/github-actions/matrix)
* [checkout.md](/docs/github-actions/checkout)
* [runners.md](/docs/github-actions/runners)


---

# Workflow Syntax (/docs/github-actions/workflow-syntax)



# Workflow Syntax [#workflow-syntax]

*GitHub Actions · Reference cheat sheet*

***

## 📋 Overview [#-overview]

Workflows live in `.github/workflows/*.yml` and define when automation runs (`on`), what it does (`jobs`), and how steps execute. YAML indentation matters. Start from a minimal workflow, then add permissions, concurrency, and reuse.

## 🔧 Core concepts [#-core-concepts]

| Key           | Role                                  |
| ------------- | ------------------------------------- |
| `name`        | UI label                              |
| `on`          | Events / schedules                    |
| `permissions` | Token scopes                          |
| `env`         | Workflow-level env                    |
| `defaults`    | Default `run` shell/working-directory |
| `concurrency` | Cancel/queue overlapping runs         |
| `jobs`        | Map of job IDs                        |
| `run-name`    | Dynamic run title                     |

Top-level `jobs.<id>` contains `runs-on`, `steps`, `needs`, `strategy`, `environment`, etc. Comments use `#`.

## 💡 Examples [#-examples]

**Minimal CI:**

```yaml
name: CI
on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test
        run: npm test
```

**Defaults:**

```yaml
defaults:
  run:
    shell: bash
    working-directory: ./app
```

## ⚠️ Pitfalls [#️-pitfalls]

* Tabs break YAML—use spaces only.
* Unquoted `on: on` / special values may parse oddly; quote strings when unsure.
* Workflow must be on the default branch to run `pull_request` from forks with some constraints—know fork PR token limits.
* `actions/checkout` and other actions should be pinned to versions/SHAs for supply-chain safety.
* Invalid expressions fail at runtime—validate with act or a dry PR.
* Large monolithic workflows are hard to debug—split jobs.

## 🔗 Related [#-related]

* [events\_triggers.md](/docs/github-actions/events-triggers)
* [jobs\_steps.md](/docs/github-actions/jobs-steps)
* [permissions.md](/docs/github-actions/permissions)
* [expressions.md](/docs/github-actions/expressions)
* [reusable\_workflows.md](/docs/github-actions/reusable-workflows)

