Recover a workspace whose chezmoi config predates a new dotfiles variable - #332
Conversation
…edates a new template variable chezmoi update applies with the config rendered by chezmoi init at create, and nothing regenerated it. A dotfiles repo that adds a variable therefore left every existing workspace pulling fine and aborting on 'map has no entry for key' under missingkey=error -- on dl <ws> dotfiles and the attach refresh alike, the two commands whose job is to unstick exactly that workspace. The refresh now re-renders the config and applies again when the first attempt fails. The retry ordering is the point: the template naming the new variable arrives in the pull, so re-initialising has to follow it.
Reviewer's GuideAdds a failure-handling retry path to the dotfiles refresh flow so that a chezmoi config rendered before new template variables can be regenerated and Sequence diagram for dotfiles refresh retry with chezmoi re-initsequenceDiagram
actor User
participant DevlaunchCLI as DevlaunchCLI
participant Shell as dotfiles_command
participant Chezmoi as chezmoi
participant Pixi as pixi
User->>DevlaunchCLI: run dotfiles refresh
DevlaunchCLI->>Shell: dotfiles_command(dotfiles_url, bound)
Shell->>Chezmoi: chezmoi update --force
alt update succeeds
Chezmoi-->>Shell: success
Shell->>Pixi: pixi global sync
Pixi-->>Shell: success
Shell-->>DevlaunchCLI: "Dotfiles updated successfully"
else update fails (e.g. missing template variable)
Chezmoi-->>Shell: error
Shell->>Shell: echo Re-initialising the chezmoi config and retrying...
Shell->>Chezmoi: chezmoi init --force
Chezmoi-->>Shell: success
Shell->>Chezmoi: chezmoi update --force
Chezmoi-->>Shell: success or error
opt second update succeeds
Shell->>Pixi: pixi global sync
Pixi-->>Shell: success
Shell-->>DevlaunchCLI: "Dotfiles updated successfully"
end
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
blooop
left a comment
There was a problem hiding this comment.
This was generated by AI during review.
Retrospective wf-review of a merged PR, judged against current main (a5f7ed8). This PR merged with no review — Sourcery was rate-limited and no wf-review ran. Findings below were reproduced by running the exact shell shape dotfiles_command emits against real chezmoi v2.72.0 in throwaway HOMEs, not read off the diff.
First, the good news, because it is verifiable and it is the point of the PR. The stated scenario reproduces and the fix heals it. With a source repo that adds a toolbox variable and a config rendered before it existed:
Updating dotfiles...
e6a3b62..7b0e336 main -> origin/main
chezmoi: .marker: template: dot_marker.tmpl:1:29: executing "dot_marker.tmpl" at <.toolbox>: map has no entry for key "toolbox"
Re-initialising the chezmoi config and retrying...
Already up to date.
Dotfiles updated successfully # exit 0; config gains toolbox = false; the manifest applies
A healthy workspace never enters the branch. That much holds.
Standards
1. chezmoi init --force is not non-interactive, and this is the one invocation with no bound. (blocking)
rust/devlaunch-core/src/flows/launch.rs:1694. --force is chezmoi's global flag — --force Make all changes without prompting, i.e. do not ask before overwriting files. It does not suppress the prompt* template functions in .chezmoi.toml.tmpl; the flag for that is --promptDefaults.
So in exactly the case this PR exists for — a dotfiles repo that adds a template variable — if that variable is supplied by any prompt* function, the existing config has no value for it and chezmoi init --force prompts. Reproduced:
$ chezmoi init --force </dev/null # no tty
What is the new variable?
chezmoi: template: chezmoi.toml:1:13: ... error calling promptStringOnce:
could not open a new TTY: open /dev/tty: no such device or address # exit 1
$ script -qec 'chezmoi init --force' /dev/null # with a tty
What is the new variable? > ... # still blocked when timeout killed it at 12s
dl <ws> dotfiles passes bound: None (the docstring at launch.rs:1666-1671 says so deliberately), so with a TTY there is nothing to stop that wait. The docstring three lines above the change names this exact hazard — "An unreachable remote or a credential prompt is exactly the case where it does not [exit]" — and the new path is added to the one invocation that has no bound. On the attach refresh it is bounded, at the cost of the full 60s of DOTFILES_ATTACH_TIMEOUT in front of every shell.
Second-order, and it contradicts the docstring: because init failing short-circuits the &&, the original error is replaced. End-to-end, unreachable remote plus a prompting config template:
Updating dotfiles...
fatal: unable to access 'https://…': Failed to connect …
chezmoi: git: exit status 1
Re-initialising the chezmoi config and retrying...
chezmoi: … could not open a new TTY: open /dev/tty: no such device or address # exit 1
The docstring promises "then fails again with the error that mattered". It does not; the connection failure is the second-to-last thing on screen and the exit is attributed to a TTY.
Fix: chezmoi init --force --promptDefaults (add --no-tty if a missing default should be a fast error rather than a wait).
2. The retry fires on every failure, and chezmoi init with no repo argument is not read-only. (blocking)
Same line. chezmoi init --help: "If chezmoi does not detect a Git repository in the source directory, chezmoi will clone the provided repo into the source directory. If no repo is provided, chezmoi will initialize a new Git repository."
Reproduced against a workspace whose chezmoi source directory is present but not a git repo:
$ chezmoi update --force
fatal: not a git repository (or any of the parent directories): .git # actionable
chezmoi: git: exit status 128
$ chezmoi init --force # the retry
# exit 0 — and ~/.local/share/chezmoi/.git now exists, empty
$ chezmoi update --force # the second attempt
There is no tracking information for the current branch. …
chezmoi: git: exit status 1
The workspace is now permanently beyond this command's reach: every later dl <ws> dotfiles finds a git repo, so init is a no-op, and update fails on the manufactured empty repo forever. The one diagnostic that told the operator what was actually wrong is gone. The PR's claim that a retry after an unrelated failure "re-renders a config locally, which is cheap" holds only for the failures it anticipated; chezmoi init also writes to the source directory.
Fix: narrow the trigger to the failure it is for. Either pass the URL the command already has in scope (chezmoi init --force <dotfiles-url>, which makes a missing repo a clone rather than a git init), or capture the first attempt's output and retry only on map has no entry for key, or guard with git -C "$(chezmoi source-path)" rev-parse --git-dir before re-initialising.
3. The message misattributes every non-config failure. (non-blocking) Re-initialising the chezmoi config and retrying... is printed for an unreachable remote, a dirty source tree and a failing run_ script alike. The PR body acknowledges this; a reader of the log does not have the PR body. Fix: say what it is doing rather than why (Retrying with a freshly rendered chezmoi config...), or move the sentence behind the narrowed trigger from finding 2.
4. The README was not updated. (non-blocking) CLAUDE.md:206 is explicit: "When modifying CLI commands, flags, or usage patterns, update the README.md to reflect the current tool behavior." README.md:261 still glosses dl <user/repo> dotfiles as "(chezmoi update)", and the "Refreshing dotfiles on attach" section at README.md:1016 never says the command can now regenerate the workspace's chezmoi config. That matters more than a usual doc lag, because the new branch writes to ~/.config/chezmoi/chezmoi.toml and (finding 2) to the source directory — a side effect a reader of the README has no way to know about. Fix: one sentence in the 1016 section.
Checked and clean: the change hard-codes no path at all, so the "never hard-code /home/<user>" rule is not in play; the retry is single-shot and idempotent; a mid-recovery failure short-circuits correctly so pixi global sync is skipped and a non-zero status reaches the caller; the emitted string is still safely quoted through posix_quote on the bounded path.
Spec
No spec available. This PR closes no ticket — closingIssuesReferences is empty, the branch's single commit references no issue, and no open or closed issue in the tracker covers it. Per the two-axis method the Spec axis reports that rather than inferring requirements from the code, so both findings above are Standards findings and this axis is empty. Worth noting for a repo that otherwise runs every change through a wayfinder ticket: an unticketed behaviour change is also an unreviewed decision about when the recovery should fire, which is precisely where findings 1 and 2 land.
Verdict
Request changes (recorded in writing; the PR is merged, so this is a follow-up ticket rather than a gate).
Blocking:
launch.rs:1694—chezmoi init --forceprompts. Add--promptDefaults.launch.rs:1694— the retry fires on every failure andchezmoi initgit inits an empty source directory, permanently stranding a workspace whose source dir is not a repo. Narrow the trigger or pass the URL.
Non-blocking: the retry message misattributes unrelated failures, and the README still describes this command as plain chezmoi update.
The core fix is right, verified working, and cheap for a healthy workspace. What is missing is that the retry treats "update failed" as a synonym for "the config is stale", and the recovery it runs is neither read-only nor non-interactive.
The defect
chezmoi updateapplies with the config rendered bychezmoi initwhen the workspace was created, and nothing ever regenerates it. A dotfiles repo that adds a template variable therefore leaves every existing workspace pulling fine and then aborting on every apply — undermissingkey=error,map has no entry for keyis fatal. Bothdl <ws> dotfilesand the attach refresh hit it, which are precisely the commands whose job is to bring such a workspace forward, so the workspace was stuck with no in-band recovery.Hit for real: after blooop/dotfiles added a capability flag, every pre-existing workspace failed its refresh with
The fix
The refresh re-renders the config and applies again when the first attempt fails. Ordering is the substance of it: the template that names the new variable arrives in the pull, so re-initialising has to come after
update, not before — which is also why this is a retry rather than takingupdateapart into pull and apply and owning git's failure modes on this side.A healthy workspace never takes the branch. A retry after an unreachable remote re-renders a config locally, which is cheap, then fails again with the error that mattered.
Verification
a_refresh_that_cannot_apply_re_renders_its_config_and_tries_again, confirmed red against the old command (a failed update re-renders the config) before the fix.dotfiles_commandemits were run against a deliberately stale setup (source 8 commits behind, config rendered before the new flag): it healed — config gains the variable, the manifest applies.cargo build --locked --all-targets, all crate test suites (1187 in devlaunch-core),clippy -D warnings,fmt --check.🤖 Generated with Claude Code
Summary by Sourcery
Recover stale workspaces during dotfiles refreshes by retrying chezmoi with a regenerated configuration after update failures.
Bug Fixes:
Tests: