# GitHub Actions Quick Reference

From Modern Git Academy — https://moderngitacademy.com/

Verified against actions/checkout v7 documentation, September 2026. Action major versions
change; check current documentation before pinning.

---

## Minimal workflow

```yaml
name: CI
on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read          # start read-only, widen per job

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version-file: '.nvmrc'
          cache: npm
      - run: npm ci
      - run: npm test
```

## Permissions

```yaml
permissions:
  contents: read          # clone
  pull-requests: write    # comment on PRs
  id-token: write         # OIDC — cloud auth without stored keys
  packages: write         # publish
```

**Set the organisation default to read-only and let jobs request more.** It is one setting
and it shrinks the blast radius of every workflow at once.

Job-level `permissions:` beats workflow-level. Grant write to the one job that needs it.

## Triggers

| Trigger | Fires on |
| --- | --- |
| `push` | Commits pushed |
| `pull_request` | PR opened/updated — **no secrets on fork PRs** |
| `pull_request_target` | PR, but with base-repo context ⚠️ |
| `workflow_dispatch` | Manual |
| `schedule` | Cron |
| `workflow_call` | Called by another workflow |

⚠️ **`pull_request_target` runs with secrets and write permissions in the base repository's
context.** Checking out and executing PR head code under it hands your credentials to anyone
who can open a pull request. If you use it, do not check out untrusted code.

## Checkout tuning

```yaml
- uses: actions/checkout@v7
  with:
    fetch-depth: 1            # default; 0 = full history
    filter: blob:none         # skip file contents until needed
    sparse-checkout: |        # only these paths
      apps/web
      libs/core
    persist-credentials: false  # if no later git commands need auth
```

`fetch-depth: 0` copied from a template is the most common source of CI waste. But if a tool
needs a **merge base** (diff-based linting, coverage deltas), depth 1 will not have it —
pair `fetch-depth: 0` with `filter: blob:none` for full history at a fraction of the transfer.

## Secrets

```yaml
env:
  TOKEN: ${{ secrets.MY_TOKEN }}
```

- Repository secrets are readable by **every workflow in the repository**.
- **Environment secrets** can require reviewers and restrict branches — use these for
  anything production.
- Prefer **OIDC** over stored cloud keys: exchange the job's identity for short-lived
  credentials and store nothing.

## Reusable workflows

```yaml
jobs:
  build:
    uses: example-org/workflows/.github/workflows/node.yml@v3
    with:
      node-version-file: '.nvmrc'
    secrets: inherit          # or name individual secrets — narrower
```

One change reaches every caller. Reference a **version**, not the default branch.

## Security essentials

- [ ] Pin third-party actions to a **full commit SHA**, not a tag. Tags move.
- [ ] Never run fork PR code with secrets or write permissions.
- [ ] Never interpolate untrusted text (`${{ github.event.pull_request.title }}`) directly
      into a `run:` block — pass it through `env:` instead.
- [ ] Self-hosted runners: never on public repositories.
- [ ] Disable workflows approving pull requests.

---

Full lessons: https://moderngitacademy.com/github-actions/
