Context
Track: A — Workflow authoring standards
Scope: The sparkgeo/github-actions repo itself — enforced on every PR that adds or modifies a reusable workflow
Parent: (see GitHub Actions platform security parent)
This issue is a dependency for every other workflow implementation issue in this repo. All reusable workflows must comply with the standards defined here before merging.
The problem
Without automated enforcement, security properties of workflow YAML degrade over time — a well-intentioned PR adds ${{ github.event.pull_request.title }} directly into a run: block, or references an action at @main instead of a pinned SHA. Manual review catches some; automation catches all.
Standards every workflow in this repo must meet
| Standard |
Rule |
Why |
| SHA pinning |
All uses: references pinned to full 40-char commit SHA with # version comment |
Mutable tags can be force-pushed with malicious commits |
| Minimal permissions |
permissions: block at workflow or job level; default contents: read |
GITHUB_TOKEN historically overpermissioned by default |
| No persistent credentials |
actions/checkout always has persist-credentials: false unless explicitly required |
Downstream steps can read .git/config and steal the token |
No context interpolation in run: |
${{ github.event.* }} and ${{ github.head_ref }} must be assigned to an env: var first |
Direct interpolation enables shell injection via crafted PR titles/branch names |
No pull_request_target without threat model |
Any workflow using pull_request_target or workflow_run must include a documented threat model in its header comment |
These triggers run in the base repo context with full secret access |
Explicit shell: |
All run: steps declare shell: bash or shell: pwsh explicitly |
Implicit shell selection can vary across runner OS |
Correct pattern — context interpolation
# WRONG — direct interpolation
- run: echo "Branch: ${{ github.head_ref }}"
# CORRECT — intermediate env var
- env:
HEAD_REF: ${{ github.head_ref }}
run: echo "Branch: $HEAD_REF"
Correct pattern — SHA pinning
# WRONG
- uses: actions/checkout@v4
# CORRECT
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Tools
actionlint — workflow syntax and type checking
- Validates workflow YAML structure, expression types, and common mistakes
- Catches undefined context references, invalid event filters, shell script errors via
shellcheck integration
- Native
pre-commit hook available
# .pre-commit-config.yaml (on sparkgeo/github-actions repo itself)
repos:
- repo: https://github.com/rhysd/actionlint
rev: v1.x.x
hooks:
- id: actionlint
zizmor — security-specific workflow analysis
- Purpose-built for GitHub Actions security: flags injection vectors, dangerous triggers, missing permission scopes, unpinned actions
- Complements
actionlint (syntax) with security-specific rules
- SARIF output → GitHub Security tab
# .pre-commit-config.yaml
repos:
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.x.x
hooks:
- id: zizmor
Reusable workflow — workflow-lint.yml
CI gate that runs on every PR touching .github/workflows/:
name: Workflow Lint
on:
pull_request:
paths:
- '.github/workflows/**'
jobs:
actionlint:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: rhysd/actionlint@main # pin SHA when stable version tagged
- uses: woodruffw/zizmor-action@main
with:
sarif: true
Acceptance criteria
Context
Track: A — Workflow authoring standards
Scope: The
sparkgeo/github-actionsrepo itself — enforced on every PR that adds or modifies a reusable workflowParent: (see GitHub Actions platform security parent)
This issue is a dependency for every other workflow implementation issue in this repo. All reusable workflows must comply with the standards defined here before merging.
The problem
Without automated enforcement, security properties of workflow YAML degrade over time — a well-intentioned PR adds
${{ github.event.pull_request.title }}directly into arun:block, or references an action at@maininstead of a pinned SHA. Manual review catches some; automation catches all.Standards every workflow in this repo must meet
uses:references pinned to full 40-char commit SHA with# versioncommentpermissions:block at workflow or job level; defaultcontents: readGITHUB_TOKENhistorically overpermissioned by defaultactions/checkoutalways haspersist-credentials: falseunless explicitly required.git/configand steal the tokenrun:${{ github.event.* }}and${{ github.head_ref }}must be assigned to anenv:var firstpull_request_targetwithout threat modelpull_request_targetorworkflow_runmust include a documented threat model in its header commentshell:run:steps declareshell: bashorshell: pwshexplicitlyCorrect pattern — context interpolation
Correct pattern — SHA pinning
Tools
actionlint— workflow syntax and type checkingshellcheckintegrationpre-commithook availablezizmor— security-specific workflow analysisactionlint(syntax) with security-specific rulesReusable workflow —
workflow-lint.ymlCI gate that runs on every PR touching
.github/workflows/:Acceptance criteria
.pre-commit-config.yamlinsparkgeo/github-actionsruns bothactionlintandzizmorworkflow-lint.ymlCI gate blocks PRs that fail either linterCONTRIBUTING.md(cross-ref README issue)