# GitHub Actions patterns

Loaded on demand from `../SKILL.md`.

## Sharing logic

| Need | Use |
|------|-----|
| A few steps repeated across jobs | composite action in `.github/actions/<name>/action.yml` |
| Whole jobs repeated across repos | reusable workflow with `on: workflow_call` |
| Logic that needs its own runtime or tests | JavaScript or container action in its own repo |

Reusable workflow:

```yaml
# .github/workflows/node-service.yml
on:
  workflow_call:
    inputs:
      path: { type: string, required: true }
    secrets:
      NPM_TOKEN: { required: false }
jobs:
  test:
    runs-on: ubuntu-24.04
    defaults: { run: { working-directory: "${{ inputs.path }}" } }
    steps:
      - uses: actions/checkout@<sha>
      - run: npm ci && npm test
```

```yaml
# caller
jobs:
  notes-service:
    uses: acme/platform/.github/workflows/node-service.yml@<sha>
    with: { path: services/notes }
    secrets: inherit
```

Composite action:

```yaml
# .github/actions/setup-pnpm/action.yml
name: setup-pnpm
inputs:
  node-version: { default: "22" }
runs:
  using: composite
  steps:
    - uses: pnpm/action-setup@<sha>
    - uses: actions/setup-node@<sha>
      with: { node-version: "${{ inputs.node-version }}", cache: pnpm }
    - run: pnpm install --frozen-lockfile
      shell: bash
```

Composite steps must declare `shell:` on every `run`.

## Passing data

- Step to step: `echo "version=$VERSION" >> "$GITHUB_OUTPUT"`, read as
  `steps.<id>.outputs.version`.
- Job to job: declare `outputs:` on the job and read `needs.<job>.outputs.x`.
- Files: `actions/upload-artifact` / `download-artifact` (v4 artifacts are
  immutable per name; use unique names per matrix leg).
- Summary for humans: append Markdown to `$GITHUB_STEP_SUMMARY`.

## Xcode on macOS runners

```yaml
ios:
  runs-on: macos-15
  timeout-minutes: 45
  steps:
    - uses: actions/checkout@<sha>
    - run: sudo xcode-select -s /Applications/Xcode_16.4.app
    - uses: actions/cache@<sha>
      with:
        path: ~/Library/Developer/Xcode/DerivedData/**/SourcePackages
        key: spm-${{ hashFiles('**/Package.resolved') }}
    - run: |
        set -o pipefail
        xcodebuild test \
          -scheme Notes -destination 'platform=iOS Simulator,name=iPhone 16' \
          -resultBundlePath TestResults.xcresult | xcbeautify
    - if: failure()
      uses: actions/upload-artifact@<sha>
      with: { name: xcresult, path: TestResults.xcresult }
```

- macOS minutes cost several times Linux minutes; run lint and unit tests of
  pure packages on Linux where possible.
- Check the runner image's installed Xcode list before selecting a version.
- Import signing certificates into a temporary keychain created in the job and
  delete it in an `if: always()` step.
