Replace shellcheck CI with checks that match how this repo actually breaks - #28
Merged
Conversation
.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`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The ShellCheck job has never passed — on
mainor on any PR, going back to at least November 2025. It was also aimed at the wrong target:bin/. It cannot read the 12 zsh files where most of this configuration lives, because shellcheck does not parse zsh.Not following: lib/common.sh, a path it cannot resolve becauseSCRIPT_DIRis computed at runtime) and 1 × SC2016 on a line that deliberately prints a literalevalfor the reader.What actually breaks
This, found in 30 seconds by the replacement —
.zshrc:21: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 exitreturns 127 and every new terminal opens with an error. The same unguarded source was in.bash_profile:4and.profile:22..profile:23was worse: it sourced$HOME/.cargo/env, which.zshenvand.bash_profileboth carry comments explaining can hang the shell in uninterruptible D-state I/O when/Volumes/Secondaryis unresponsive. It was the one place still touching that symlink.shellcheck cannot see any of these files.
zsh -nwould not have caught them either — the syntax is valid. Only starting the shell finds them.The replacement
bin/check-dotfiles, runnable locally viajust checkso CI and local never disagree:shellHOME, startszsh -i,zsh -l,bash -l; asserts exit 0 and no broken referencessyntaxzsh -nover 12 zsh files,bash -nover bash/sh — including everything shellcheck could not readconfigsemacsinit.elagainst an empty--init-directory, the way a new machine doesansible--syntax-checkon the provisioning playbookA 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:
aliases.zsh1 failed / 37 passedstarship.tomlsettings.json(this-function-does-not-exist)ininit.elvoid-functionplaybook.ymlNotes
apt(29.x on 24.04) — deliberately older than the 30.2 on the Macs, so ifinit.elworks in CI it works locally. That surfaced a real portability bug:which-keyis built in only as of Emacs 30, so it is now required first and installed as a fallback.bin/lint-shellstays 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.working-branch, so it stacks cleanly on Trim Emacs config to a lean editor; fix cold-start failures #27 and supersedes that PR'sseverity: warningworkaround by deleting the job outright.