diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 2ff1c01..acdfd3d 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -19,3 +19,10 @@ globs: ignores: - node_modules/** - .venv/** + # Other Claude Code sessions' git worktrees, for the reason spelled out in + # .yamllint.yaml. `**/*.md` matches dot directories here, so a second + # checkout doubles the file count — measured at 114 files against 57 tracked. + # It has passed so far only because a worktree holds a copy of this + # repository's own markdown; a branch under review with one lint error in it + # would fail `make validate` on main, pointing at a file main does not have. + - .claude/worktrees/** diff --git a/.yamllint.yaml b/.yamllint.yaml index ff1fbba..1631f3f 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -15,6 +15,24 @@ ignore: | stacks/observability/snmp-exporter/.rendered/ node_modules/ .venv/ + # Other Claude Code sessions' git worktrees. Each one is a whole second + # checkout, so without this yamllint lints another branch's copy of every + # file here — including the ciphertext the entries above exist to exempt. + # Those patterns contain a slash, which anchors them to the repository root + # the way .gitignore does, so `secrets/*.sops.yaml` does not match + # `.claude/worktrees//secrets/observability.sops.yaml`. Measured: 23 + # findings, none of them in this tree. + # + # It is a LOCAL false red and CI never reproduces it, which is the worst + # shape for a check to have: `make validate` fails on the deploy host for a + # clean change, and an operator who learns to dismiss that has stopped + # reading the one command that gates a deploy. + # + # This list has to be maintained by hand because yamllint does not read + # .gitignore. editorconfig-checker is immune to the same problem for a + # structural reason — scripts/lint.sh hands it a `git ls-files` list rather + # than letting it walk the tree. See the comment there. + .claude/worktrees/ rules: line-length: diff --git a/scripts/lint.sh b/scripts/lint.sh index 81f1a99..549711e 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -31,6 +31,32 @@ # linter it cannot reach, CI may not, because a linter skipped there is one # nobody will ever run. # +# WHAT EACH LINTER LOOKS AT, AND WHY THAT IS NOT OBVIOUS +# +# Three of these decide their own file list, and none of them reads .gitignore. +# That only matters because a workstation has trees a CI checkout does not — +# rendered config, certificates, backups, and another session's git worktree +# under .claude/worktrees/, which is an entire second copy of this repository. +# A linter that walks the tree finds all of it and fails locally for a change +# that is clean, while CI stays green. Green where nothing runs and red where +# everything does is the wrong way round, and it is how a check stops being +# read. +# +# So, measured rather than assumed: +# +# - yamllint walks `.`, so its exclusions live in .yamllint.yaml +# - markdownlint-cli2 globs `**/*.md`, which DOES match dot directories, so +# its exclusions live in .markdownlint-cli2.yaml +# - shellcheck is handed scripts/*.sh below — one literal glob, cannot wander +# - actionlint reads only /.github/workflows. Verified with +# -verbose: it lints 2 files with a worktree present, not 4 +# - editorconfig-checker is handed a `git ls-files` list built below, so it +# sees tracked files and nothing else +# +# The last of those is the shape to copy when this comes up again: a list built +# from git cannot drift, whereas the first two carry a list of exclusions that +# has to be extended by hand every time a new ignored directory appears. +# # --skips-file follows seed-validation-env.sh's contract: the caller owns the # path and its lifetime. scripts/validate.sh passes an mktemp it removes on exit # and adds the line count to its SKIPPED counter. Without it a skip here would