Skip to content

feat: standards-check.yml, the deterministic required check (W1) - #162

Merged
twistedmelonman merged 6 commits into
mainfrom
claude/feat-standards-check-019HDRKL
Sep 8, 2026
Merged

feat: standards-check.yml, the deterministic required check (W1)#162
twistedmelonman merged 6 commits into
mainfrom
claude/feat-standards-check-019HDRKL

Conversation

@twistedmelonman

@twistedmelonman twistedmelonman commented Sep 8, 2026

Copy link
Copy Markdown
Member

What this is

The deterministic, secret-free standards check that becomes the fleet's required check, replacing the CI judgment reviewer outright (dev-env#60, #154; decided 2026-09-08).

It is a reusable workflow. It installs pinned, checksum-verified linters, then runs standards/run-standards.sh from this repo at the SHA of the workflow file itself — so the script, the configs, and the workflow always agree, and a consumer can never drift onto a mismatched pair.

Unlike the judgment reviewer it replaces, it consumes no secrets, calls no model, and returns the same verdict for the same tree every time.

The caller stub

Consumers add one file. Name the job standards-check, and the required check is standards-check / run-standards-check:

name: Standards Check
on:
  pull_request:
    types: [opened, synchronize, ready_for_review, reopened]
permissions:
  contents: read
jobs:
  standards-check:
    uses: smartwatermelon/github-workflows/.github/workflows/standards-check.yml@standards-check-v1

self-standards-check.yml in this PR is that stub pointed at a local path, so a PR that changes the check dogfoods itself.

Inputs

Input Type Default Effect
shellcheck boolean true shellcheck -S info over shell files
yamllint boolean true yamllint over YAML files
actionlint boolean true actionlint over .github/workflows
zizmor boolean true zizmor over .github/workflows
markdownlint boolean true markdownlint-cli2 over Markdown
node_floor_check boolean true Fail on Node pins below node_floor
node_floor string "22" Lowest supported Node major

Setting a linter's input to false skips both its install and its run.

Pinned tools

Every version is pinned here and bumped by PR. The three binaries are additionally SHA256-verified before install.

Tool Version Verification
shellcheck 0.11.0 SHA256
actionlint 1.7.12 SHA256
zizmor 1.30.0 SHA256
yamllint 1.38.0 pinned via pipx
markdownlint-cli2 0.23.2 pinned via npm

Running it locally

The same script CI runs:

bash standards/run-standards.sh --repo /path/to/repo

Design decisions worth review

Three deviations from the drafted design, each verified rather than assumed:

  1. job.workflow_sha, not github.job_workflow_sha. The draft used the latter. It is absent from the current contexts reference and evaluates to an empty string (Property job_workflow_sha in context github is not available actions/runner#2417). Empty is the dangerous value: actions/checkout treats an empty ref as the default branch, so the check would have gone green having linted a different commit than the one under review. A separate step now resolves the SHA and exits 2 when it is empty, so a runner-side regression fails loudly instead of silently linting the wrong tree.

  2. A scoped actionlint.yaml ignore. actionlint 1.7.12 has not added job.workflow_sha to its context schema, so it reports the property as undefined. The ignore is scoped to that one message in that one file, and the scoping was validated against a known-bad case — an unrelated bogus property in the same file is still reported.

  3. SKIP_LIST split per linter. The single concatenated expression was 346 columns, over the shared 120-column yamllint limit. Widening that limit would have weakened it fleet-wide, so the list is now one env var per linter joined in the shell. The join was exercised for the all-empty, partial, all-set, and trailing-element cases.

Also included: a fix for two review findings from the earlier commits — run-standards.sh reported a clean pass over any directory git could not read (it now exits 2), and the test fixtures inherited the user's global git template instead of being hermetic.

Validation

Read from the run log, not inferred from the green check.

standards-check / run-standards-check passed in 16s.

All three checksums verified:

/tmp/shellcheck.tar.xz: OK
/tmp/actionlint.tar.gz: OK
/tmp/zizmor.tar.gz: OK

All six linters ran (skip list: '' — nothing skipped, every input defaulted true):

== shellcheck
== yamllint
== actionlint
== zizmor
== markdownlint
== node-floor
Node floor 22: OK
standards-check: all enabled linters clean

The standards checkout resolved the right commit. The log reports:

standards SHA: efab72221e5933ffe4b6f65cb4ac61d0267b916f

That commit is Merge 535b88f into 39d1b15 — the PR head merged into main, confirmed by its two parents and by the PR's own potentialMergeCommit. For a pull_request trigger that is the correct ref: it carries this PR's content tested against the merge target. It is not origin/main (39d1b15), which rules out the silent default-branch fallback that the empty-SHA guard exists to prevent.

This is the end-to-end confirmation that job.workflow_sha populates at runtime where github.job_workflow_sha would have been empty.

Advances #154

https://claude.ai/code/session_019HDRKLQNv82SEBd4zGpcXf

Claude Code Bot added 6 commits September 8, 2026 11:43
Adds the canonical markdownlint and yamllint configs (mirroring the
dotfiles copies at ~/.config), a check-node-floor.sh that rejects Node
pins below a given major, and a tests/ harness to run known-bad fixtures.

check-node-floor.sh scans .nvmrc/.node-version, package.json
engines.node, and workflow node-version / node-version-file pins. Named
aliases (lts/*, node, latest) float and are accepted; ${{ }} expressions
cannot be resolved statically and get a notice instead of an error.

Claude-Session: https://claude.ai/code/session_019HDRKLQNv82SEBd4zGpcXf
Adds the deterministic standards runner that drives shellcheck, yamllint,
actionlint, zizmor, markdownlint and the Node-floor check over a repo's
tracked files, plus a fixture suite where each linter has a known-bad case
that must be rejected and a clean case that must pass. A linter with
nothing to lint passes with a notice; --skip genuinely disables one.

Dogfooding the runner on this repo surfaced two real findings, fixed here:

- claude-blocking-review.yml embedded a secrets expression inside a `run:`
  comment. GitHub interpolates expressions in run blocks even in shell
  comments, and this workflow declares the secret as claude_oauth_token, so
  actionlint failed on the undefined property. Reworded to describe the
  secret without expression syntax.
- zizmor reported dependabot-cooldown and self-repository. Both are now
  ignored with rationale in zizmor.yml: a cooldown would re-introduce exactly
  the update lag behind #123, and zizmor's suggested same-repo call form is
  rejected by both GitHub and actionlint, so `./...` is the only syntax that
  runs.

Also hardens check-node-floor.sh after review of the previous commit:
package.json with no jq now fails loudly (exit 2) instead of skipping the
engines.node source in silence, and inline-comment stripping follows YAML's
rule that a comment needs preceding whitespace, so "20#c" is reported as an
unrecognisable value rather than misread as a Node 20 pin. Both are covered
by new fixtures.

Claude-Session: https://claude.ai/code/session_019HDRKLQNv82SEBd4zGpcXf
Two findings from the Task 1-2 review.

run-standards.sh reported a clean pass over any directory git could not
read. Every file-based linter enumerates through `_tracked || true`, so a
non-repo --repo (or a repo git refuses, e.g. dubious ownership) produced an
empty file list and each linter passed over nothing. Guard immediately after
argument parsing: if `git rev-parse --git-dir` fails, error and exit 2.

The fixtures in test-run-standards.sh used a plain `git init`, which fires
the user's global init.templateDir and scaffolds .editorconfig, .gitignore,
and .claude/ into every fixture — so a fixture meant to be empty was not.
An explicit empty --template overrides it; `git ls-files` on a bare fixture
now lists nothing. (test-check-node-floor.sh never calls git init; its
fixtures are plain directories.)

The new exit-2 case was observed failing against the pre-fix script
("exited 0, expected 2") before the guard was added.

Claude-Session: https://claude.ai/code/session_019HDRKLQNv82SEBd4zGpcXf
The deterministic, secret-free required check that replaces the CI judgment
reviewer (dev-env#60, #154). Installs pinned, checksum-verified linters and
runs standards/run-standards.sh from this repo at the SHA of the workflow
file itself, so the script, the configs, and the workflow always agree.

self-standards-check.yml calls it through a local path, so a PR that changes
the check dogfoods itself and produces the
`standards-check / run-standards-check` status check.

Deviations from the drafted design, each verified rather than assumed:

- The draft used `github.job_workflow_sha`. That property is absent from
  the current contexts reference and evaluates to an empty string
  (actions/runner#2417); the documented property is `job.workflow_sha`.
  Empty is the dangerous value, because actions/checkout treats an empty
  `ref` as the default branch — the check would have gone green having
  linted a different commit than the one under review. A separate step now
  resolves the SHA and exits 2 when it is empty, so a runner-side regression
  fails loudly instead of silently linting the wrong tree.

- actionlint 1.7.12 has not added `job.workflow_sha` to its context schema,
  so it reports the property as undefined. .github/actionlint.yaml adds a
  documented ignore scoped to that one message in that one file. The scoping
  was validated against a known-bad case: an unrelated bogus property in the
  same file is still reported.

- The single concatenated SKIP_LIST expression was 346 columns, over the
  shared 120-column yamllint limit. It is now one env var per linter joined
  in the shell, which keeps the fleet-wide limit intact. The join was
  exercised for the all-empty, partial, all-set, and trailing-element cases.

- zizmor.yml gains the standards-check.yml excessive-permissions entry, plus
  self-repository for the new caller and adhoc-packages for the two pinned
  CLI installs that have no lockfile.

Advances #154

Claude-Session: https://claude.ai/code/session_019HDRKLQNv82SEBd4zGpcXf
Adds the ## standards-check.yml README section (purpose, caller stub,
inputs table, check name, config precedence, pinned tool versions,
local command, validation note for PR #163/run 34267112924, and the
standards-check-v1/vX.Y.Z versioning namespace), and fills in the
previously-empty CLAUDE.md with the tests/run-tests.sh and
standards/run-standards.sh commands.

Claude-Session: https://claude.ai/code/session_019HDRKLQNv82SEBd4zGpcXf
…notes

zizmor scanned the whole repo tree, so a tracked example workflow living
outside .github/workflows/ (e.g. docs that show a caller stub) could fail
the gate even though it isn't a real workflow. Scope the zizmor pass to
tracked .github/workflows/*.yml|*.yaml, the same way the other linters
already use the tracked-file list, with the existing "no workflows" notice
when that list is empty.

Also note in zizmor.yml and README.md that zizmor.yml doubles as the
fleet-wide CI fallback policy (standards-check.yml uses it for any
consuming repo with no zizmor.yml of its own), and add a re-check pointer
in .github/actionlint.yaml's workflow_sha ignore for whoever next bumps
actionlint.

Claude-Session: https://claude.ai/code/session_019HDRKLQNv82SEBd4zGpcXf
@twistedmelonman
twistedmelonman merged commit 3168cb2 into main Sep 8, 2026
3 checks passed
@twistedmelonman
twistedmelonman deleted the claude/feat-standards-check-019HDRKL branch September 8, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant