Skip to content

feat: workflow authoring standards + actionlint/zizmor gate #25

@ms280690

Description

@ms280690

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

  • .pre-commit-config.yaml in sparkgeo/github-actions runs both actionlint and zizmor
  • workflow-lint.yml CI gate blocks PRs that fail either linter
  • All existing workflow files in this repo pass both linters
  • Standards documented in CONTRIBUTING.md (cross-ref README issue)
  • All new workflow issues reference this issue as a prerequisite

Metadata

Metadata

Assignees

Labels

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions