2026-07-22: Actionlint workflow gate (#612), appeal-issue close-out (#659, #660, #661) โ
What shipped โ
- Pinned actionlint gate (#612).
tooling/scripts/lint.actionlint.mjslints.github/workflows/*.ymlwith actionlint v1.7.12, the version confirmed during the #607 incident to flag the exact empty$expression that broke workflow parsing on dev. Wired three ways:npm run lint:actionlint, the validate orchestrator (scopeworkflows, next to Workflow Consistency), and a CI step in the Lint & Security job. - Supply-chain posture. No new third-party GitHub Action and no unverified binary download: the script auto-installs through the Go module proxy, which verifies checksums against the sum.golang.org transparency log. CI's ubuntu runners have Go preinstalled. A dev machine without Go gets a warn-and-skip (CI is the enforcement backstop);
CI=truemakes a missing binary a hard failure. Shellcheck integration is disabled (-shellcheck=) to keep the gate scoped to Actions expressions, per the issue's keep-scope-tight note. - One real finding fixed. actionlint flagged
release-prod.yml's workflow_dispatch choice input containing an empty-string option (''). Replaced with anonesentinel and taught the consuming guard to treatnonelike unset. Behavior is identical for both trigger paths (workflow_run supplies no inputs; dispatch defaults tonone).
Verified โ
- Clean run over all current workflows passes.
- A probe workflow containing the #607 empty-
$class fails the gate and names the file. - No-binary local run warn-skips (exit 0); no-binary CI run hard-fails (exit 1).
Issue close-outs (stale-complete, no code needed) โ
Scoping the appeal-hardening theme (#659, #660) found ALL functional checklist items already shipped on dev by the Stage C appeal build; both issues predate it and went stale. Closed with per-item mapping comments on the issues (docRequested end-to-end, 45-day aging floor server + admin, appeal per-number cap consumed only after proven OTP + confirmed restriction). Notables recorded there: the verify-otp path deliberately counts attempts BEFORE checkOtp as its brute-force bound (10/10min), and the claim-enum item was superseded by the moderator's action + docRequested carrying the classification. #661 (sync-skills strict YAML) was likewise already built (validateFrontmatter + non-zero exit); closed with mapping.
Deferred (recorded) โ
- Aging-floor badge on the admin appeals queue ROW (drawer already carries the signal at the decision point); noted in #660's close-out comment.
- Script-wiring audit for
lint:actionlint: grouped (Linting), described (scripts-info); no curated extension button on purpose, consistent with sibling CI-gate lints.