Skip to content

Recover a workspace whose chezmoi config predates a new dotfiles variable - #332

Merged
blooop merged 1 commit into
mainfrom
refresh-reinits-config
Aug 21, 2026
Merged

Recover a workspace whose chezmoi config predates a new dotfiles variable#332
blooop merged 1 commit into
mainfrom
refresh-reinits-config

Conversation

@blooop

@blooop blooop commented Aug 21, 2026

Copy link
Copy Markdown
Owner

The defect

chezmoi update applies with the config rendered by chezmoi init when 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 — under missingkey=error, map has no entry for key is fatal. Both dl <ws> dotfiles and 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

chezmoi: .../.chezmoiexternal.toml: template: ... at <.toolbox>: map has no entry for key "toolbox"

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 taking update apart 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

  • New test 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.
  • The exact bytes dotfiles_command emits 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.
  • Full CI commands locally: 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:

  • Allow dotfiles refreshes to recover workspaces with stale chezmoi configurations after a failed update by re-rendering the config and retrying the apply.
  • Prevent package synchronization from proceeding until the dotfiles update and recovery attempt succeeds.

Tests:

  • Add coverage verifying that reinitialization occurs only after the first chezmoi update fails and before the retry.

…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.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @blooop, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a failure-handling retry path to the dotfiles refresh flow so that a chezmoi config rendered before new template variables can be regenerated and chezmoi update re-run, plus a regression test and documentation in the changelog and module docs.

Sequence diagram for dotfiles refresh retry with chezmoi re-init

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
Add a conditional retry path in the dotfiles refresh shell command so a failed chezmoi update triggers chezmoi init --force and a second chezmoi update.
  • Extend the dotfiles_command shell string to wrap chezmoi update --force in a failure branch that echoes a message, runs chezmoi init --force, then retries chezmoi update --force.
  • Ensure the retry sequence preserves the original order: first update, then init, then a second update, followed by pixi global sync only after a successful attempt.
  • Keep behavior for workspaces without chezmoi unchanged and preserve overall command structure and messages.
rust/devlaunch-core/src/flows/launch.rs
Document and test the new dotfiles refresh recovery behavior.
  • Add detailed rustdoc comments to DotfilesRefresh explaining the failure mode with stale chezmoi configs and why reinitialization happens after a failed update.
  • Add the test a_refresh_that_cannot_apply_re_renders_its_config_and_tries_again to assert the command string contains the initial update, a conditional init, a retry update, and that pixi global sync comes after the retry.
  • Update the changelog with a Fixed entry describing the bug and the new recovery behavior for long-lived workspaces with outdated chezmoi configs.
rust/devlaunch-core/src/flows/launch.rs
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@blooop
blooop merged commit 187819e into main Aug 21, 2026
14 checks passed
@blooop
blooop deleted the refresh-reinits-config branch August 21, 2026 20:58
@blooop blooop mentioned this pull request Aug 21, 2026
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.97%. Comparing base (aee2de0) to head (b401d26).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 95.35% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 95.35% <100.00%> (+<0.01%) ⬆️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:1694chezmoi init --force prompts. Add --promptDefaults.
  • launch.rs:1694 — the retry fires on every failure and chezmoi init git 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.

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