Skip to content

Add a zsh -n gate: nothing was checking the files the shell sources - #95

Merged
jfmercer merged 1 commit into
masterfrom
zsh-syntax-gate
Jul 28, 2026
Merged

Add a zsh -n gate: nothing was checking the files the shell sources#95
jfmercer merged 1 commit into
masterfrom
zsh-syntax-gate

Conversation

@jfmercer

Copy link
Copy Markdown
Owner

First of the three remaining backlog topics (CI quality gates). This closes one concrete, proven hole rather than adding gates speculatively.

The hole

shellcheck cannot parse zsh, so the lint job's shebang-based globs never looked at any of the ~25 files that get sourced into every interactive shell — dot_zshrc, dot_zshenv, and every */*.zsh topic file. Zero CI coverage.

I proved it by injecting if [[ broken syntax into system/alii.zsh:

zsh -n says:        system/alii.zsh:93: parse error: condition expected: broken
shellcheck sees it: NO — lint job still passes

And the failure mode is quiet. A parse error in a sourced file does not hard-fail the shell — zsh reports it and carries on, silently dropping every alias and setting after the error. So a break would ship green and surface days later as "some of my aliases disappeared", with no obvious cause. That's precisely the kind of thing a syntax gate should catch.

The gate

A new Syntax-check zsh files step in the existing (already matrixed) lint job.

  • File list derived from globs, not hardcoded — a newly added topic file is covered automatically rather than silently skipped. The step also fails if the globs ever match nothing, so the list can't rot into a no-op.
  • -n parses without executing. The only safe choice: these files alter PATH, install hooks and start the prompt, so actually sourcing them in CI would be wrong.
  • Installs zsh on Linux if absent; macOS runners already have it.

Verification

Rather than paraphrase the step, I extracted its body from the workflow YAML and ran it verbatim:

  • 25 files pass
  • injecting a syntax error into system/alii.zshFAIL, exit 1, parse error shown
  • injecting an unterminated string into dot_zshenvFAIL, unmatched "
  • restores clean, no drift

Running it on both matrix legs also gives a free cross-version check, since Ubuntu's zsh may differ from macOS's 5.9.

Also

CLAUDE.md's CI table described lint as ubuntu-only running just shellcheck. It has since been matrixed over both OSes and gained py_compile and bump-deps --self-test. Corrected, and recorded why zsh -n has to be its own gate.

Not in this PR

Per discussion, no shell-startup performance guard — a wall-clock threshold would be flaky on shared runners, and the 180 ms figure is already documented.

Remaining backlog: unit tests, and a selective Python port of bin/secret and bin/herdr-reload-all (the measurements argued against a broad migration — python3 starts ~2.5× slower than sh, which would penalise the twelve 1–13 line git-* wrappers for no benefit).

shellcheck cannot parse zsh, so the lint job's globs never looked at the ~25
files that get sourced into every interactive shell: dot_zshrc, dot_zshenv and
every */*.zsh topic file. They had no CI coverage at all.

That is worse than it first appears. A parse error in a topic file does not
hard-fail the shell -- zsh reports it and carries on, silently dropping every
alias and setting after the error. So a break ships green and shows up later as
"some of my aliases disappeared", with no obvious cause. Verified by injecting
`if [[ broken syntax` into system/alii.zsh: zsh -n catches it, the existing
shellcheck steps do not, and the lint job passed.

- ci.yaml: new "Syntax-check zsh files" step in the lint job. Installs zsh on
  Linux if absent; macOS runners have it. The file list is derived from globs
  rather than hardcoded, so a newly added topic file is covered automatically
  instead of being silently skipped, and the step fails if the globs ever match
  nothing. Uses -n so files are parsed but never executed -- they modify PATH,
  install hooks and start the prompt, so sourcing them in CI would be wrong.
- CLAUDE.md: correct the lint row, which described only shellcheck and claimed
  ubuntu-only; it has been matrixed and has gained py_compile and the bump-deps
  self-test since. Record why zsh -n needs to be its own gate.

Verified by extracting the step body from the workflow and running it verbatim:
25 files pass, and it correctly fails on a broken topic file and on a broken
dot_zshenv.
@jfmercer
jfmercer merged commit b55d83b into master Jul 28, 2026
7 checks passed
@jfmercer
jfmercer deleted the zsh-syntax-gate branch July 28, 2026 14:36
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