Detector spec (from hypatia#333 Pattern 6)
Pattern 6 — Read-only check workflow missing concurrency: block
Severity: low (runner-cost waste on rapid-push PRs)
Detection (Hypatia.Rules.WorkflowAudit):
- For each workflow file:
- If
on: includes pull_request or push, AND no top-level concurrency: block, AND permissions: is read-all or only has read scopes, flag.
- The read-only condition is the safety gate —
cancel-in-progress: true is only safe when the workflow doesn't publish/mutate.
Worked examples (this session):
affinescript/.github/workflows/{ci,semgrep,stdlib-naming,spark-theatre-gate,workflow-linter}.yml all lacked concurrency blocks. Fix: affinescript#379 added the estate-standard {group: workflow-ref, cancel-in-progress: true} pattern.
Remediation guidance to emit:
Read-only check workflows should add the canonical block:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Skip for workflows that publish (release, JSR, npm) — superseding a publish is unsafe.
Implementation pointers
- Detection algorithm: For each workflow, if
on: includes pull_request/push AND no top-level concurrency: AND permissions: is read-all or read-only scopes, flag. Read-only condition is the safety gate.
- Real-world example:
affinescript/.github/workflows/{ci,semgrep,stdlib-naming,spark-theatre-gate,workflow-linter}.yml all lacked concurrency blocks.
- Landed fix (reference): affinescript#379 (added estate-standard
{group: workflow-ref, cancel-in-progress: true} pattern).
- Rule statement: Read-only check workflows should add the canonical
concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true } block. Skip for workflows that publish (release, JSR, npm) — superseding a publish is unsafe.
Acceptance
Source cohort: hypatia#333.
Detector spec (from hypatia#333 Pattern 6)
Pattern 6 — Read-only check workflow missing
concurrency:blockSeverity: low (runner-cost waste on rapid-push PRs)
Detection (
Hypatia.Rules.WorkflowAudit):on:includespull_requestorpush, AND no top-levelconcurrency:block, ANDpermissions:isread-allor only hasreadscopes, flag.cancel-in-progress: trueis only safe when the workflow doesn't publish/mutate.Worked examples (this session):
affinescript/.github/workflows/{ci,semgrep,stdlib-naming,spark-theatre-gate,workflow-linter}.ymlall lacked concurrency blocks. Fix: affinescript#379 added the estate-standard{group: workflow-ref, cancel-in-progress: true}pattern.Remediation guidance to emit:
Implementation pointers
on:includespull_request/pushAND no top-levelconcurrency:ANDpermissions:isread-allorread-only scopes, flag. Read-only condition is the safety gate.affinescript/.github/workflows/{ci,semgrep,stdlib-naming,spark-theatre-gate,workflow-linter}.ymlall lacked concurrency blocks.{group: workflow-ref, cancel-in-progress: true}pattern).concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }block. Skip for workflows that publish (release, JSR, npm) — superseding a publish is unsafe.Acceptance
lib/rules/<name>.exif Elixir, or matching the repo's rule DSL)Source cohort: hypatia#333.