Skip to content

Replace shellcheck CI with checks that match how this repo actually breaks - #28

Merged
tlockney merged 3 commits into
mainfrom
ci-meaningful-checks
Jul 27, 2026
Merged

Replace shellcheck CI with checks that match how this repo actually breaks#28
tlockney merged 3 commits into
mainfrom
ci-meaningful-checks

Conversation

@tlockney

Copy link
Copy Markdown
Owner

Why

The ShellCheck job has never passed — on main or on any PR, going back to at least November 2025. It was also aimed at the wrong target:

  • It scans 23 files under bin/. It cannot read the 12 zsh files where most of this configuration lives, because shellcheck does not parse zsh.
  • Its only findings are 13 × SC1091 (Not following: lib/common.sh, a path it cannot resolve because SCRIPT_DIR is computed at runtime) and 1 × SC2016 on a line that deliberately prints a literal eval for the reader.
  • Dotfiles do not break by having an unquoted variable in a helper script.

What actually breaks

This, found in 30 seconds by the replacement — .zshrc:21:

. "$HOME/.local/bin/env"

Every other source in that file goes through source_if_exists. This one did not. On a machine without that file (written by the uv/rustup installers), zsh -i -c exit returns 127 and every new terminal opens with an error. The same unguarded source was in .bash_profile:4 and .profile:22.

.profile:23 was worse: it sourced $HOME/.cargo/env, which .zshenv and .bash_profile both carry comments explaining can hang the shell in uninterruptible D-state I/O when /Volumes/Secondary is unresponsive. It was the one place still touching that symlink.

shellcheck cannot see any of these files. zsh -n would not have caught them either — the syntax is valid. Only starting the shell finds them.

The replacement

bin/check-dotfiles, runnable locally via just check so CI and local never disagree:

Section What it does
shell Stages tracked shell config into an empty HOME, starts zsh -i, zsh -l, bash -l; asserts exit 0 and no broken references
syntax zsh -n over 12 zsh files, bash -n over bash/sh — including everything shellcheck could not read
configs TOML, YAML, JSON, Lua. VS Code's JSONC is parsed with comments stripped rather than skipped, with errors reported against the file's real line numbers
emacs Boots init.el against an empty --init-directory, the way a new machine does
ansible --syntax-check on the provisioning playbook

A check whose tool is missing reports SKIP, never a silent pass — a green run should never mean "we checked nothing".

Verification

All 119 checks pass locally. Each check was fault-injected to prove it can actually fail:

Injected fault Result
zsh parse error in aliases.zsh caught, 1 failed / 37 passed
unclosed table in starship.toml caught, with line number
invalid JSON appended to settings.json caught at line 405 (the real fault, not the JSONC comment on line 1)
(this-function-does-not-exist) in init.el caught, void-function
malformed task in playbook.yml caught, YAML parse error with line
unguarded source (the real bug) caught, exit 127

Notes

  • CI runs Ubuntu only, as agreed. Emacs comes from apt (29.x on 24.04) — deliberately older than the 30.2 on the Macs, so if init.el works in CI it works locally. That surfaced a real portability bug: which-key is built in only as of Emacs 30, so it is now required first and installed as a fallback.
  • bin/lint-shell stays as an advisory local tool. It already tolerated these findings by running shellcheck with || true — that mismatch with a hard-failing CI job is what made the check unfixable in practice.
  • Branches off working-branch, so it stacks cleanly on Trim Emacs config to a lean editor; fix cold-start failures #27 and supersedes that PR's severity: warning workaround by deleting the job outright.

tlockney added 3 commits July 27, 2026 11:47
.zshrc, .bash_profile and .profile each sourced ~/.local/bin/env with no
existence check. That file is written by the uv and rustup installers, so on
a machine that has not run them the source fails and takes the whole shell
with it: `zsh -i -c exit` returns 127, and every new terminal opens with an
error. .zshrc routes every other source through source_if_exists, and
.bash_profile guards the atuin line directly above; these three were the
exceptions.

.profile additionally sourced ~/.cargo/env. ~/.cargo is a symlink to
/Volumes/Secondary, and .zshenv and .bash_profile both carry a comment
explaining that any filesystem operation on it blocks indefinitely in
uninterruptible D-state I/O wait when that volume is unresponsive -- which is
why they add the path directly instead. .profile was the one place still
touching it, so it now does the same.
… on 29

Tree-sitter modes are now enabled one language at a time and only where
`treesit-language-available-p' says the grammar is actually present, with
treesit-language-source-alist populated so the missing ones can be built on
demand. The config this replaced used treesit-auto with
`(treesit-auto-add-to-auto-mode-alist 'all)', which remapped every language
whether or not its grammar existed, so opening a JSON file failed with
"Tree-sitter for JSON isn't available"; gating on availability makes that
class of error impossible.

Eglot replaces the removed lsp-mode stack. It is never required at startup --
naming eglot-ensure in a hook is enough to autoload it -- so startup pays
nothing until a supported file is opened. TypeScript projects dispatch on the
presence of a deno.json, since Deno and typescript-language-server disagree
about module resolution and eglot ships no Deno entry.

Also: which-key is built in as of Emacs 30, which is what the Macs run, but
Ubuntu 24.04 ships 29, where calling which-key-mode is a void-function error.
Require it first and install the package only as a fallback, so one init.el
works on both. On 30 the built-in is used and nothing is downloaded.
The ShellCheck job scanned 23 files under bin/ and had never passed, on any
branch, going back to at least November 2025. It could not read the 12 zsh
files where most of this configuration lives -- shellcheck does not parse zsh
-- and its only findings were advisory notes about a source path it had no
way to resolve. It was aimed at a failure mode this repo does not have.

The failure mode this repo does have is "I merged, opened a terminal, and my
shell is broken", or "I provisioned a new laptop and the bootstrap died".
bin/check-dotfiles targets that:

- shell startup: stages the tracked shell config into an empty HOME and
  actually starts zsh -i, zsh -l and bash -l there, asserting a zero exit and
  no broken references. This is what found the unguarded ~/.local/bin/env
  sources fixed two commits back; no syntax-level tool could have.
- syntax: zsh -n over the zsh files, bash -n over the bash and sh files.
  Covers what ShellCheck nominally covered, including the files it could not
  read at all.
- configs: TOML, YAML, JSON and Lua parse checks. VS Code's settings.json is
  JSONC, so comments and trailing commas are stripped before parsing rather
  than the file being skipped, and errors are reported against the line
  numbers of the file as written.
- emacs: boots init.el against an empty init-directory, the way a new machine
  does, and fails on errors raised loading our own config.
- ansible: --syntax-check on the provisioning playbook.

Every section runs locally through `just check`, so a broken shell can be
caught before yadm merge rather than after. A check whose tool is missing
reports SKIP rather than silently passing, so a green run never means "we
checked nothing".

bin/lint-shell stays as an advisory local tool; it already tolerated these
findings by running shellcheck with `|| true`.
@tlockney
tlockney marked this pull request as ready for review July 27, 2026 18:56
@tlockney
tlockney merged commit 69b7cf5 into main Jul 27, 2026
1 check passed
@tlockney
tlockney deleted the ci-meaningful-checks branch July 27, 2026 18:56
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