{"record":{"id":"49f09f393e46b82b","repo":"jdx/mise","slug":"changed-while-preparing-enrollment-concurrent","errorCode":null,"errorMessage":"{} changed while preparing enrollment; concurrent declaration edit preserved; retry","messagePattern":"(.+?) changed while preparing enrollment; concurrent declaration edit preserved; retry","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/cli/dotfiles/track.rs","lineNumber":332,"sourceCode":"    declaration_lock_for(&crate::config::global_shared_config_path())\n}\n\nfn declaration_lock_for(config: &Path) -> Result<fslock::LockFile> {\n    crate::lock_file::LockFile::new(&config.with_extension(\"dotfiles-declarations.lock\"))\n        .try_lock()?\n        .ok_or_else(|| {\n            eyre::eyre!(\"another tracking declaration command is running; retry shortly\")\n        })\n}\n\nfn check_declaration(path: &Path, expected: Option<&str>) -> Result<()> {\n    let current = match std::fs::read_to_string(path) {\n        Ok(body) => Some(body),\n        Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,\n        Err(error) => return Err(error.into()),\n    };\n    if current.as_deref() != expected {\n        bail!(\n            \"{} changed while preparing enrollment; concurrent declaration edit preserved; retry\",\n            display_path(path)\n        );\n    }\n    Ok(())\n}\n\nfn commit_declaration(\n    path: &Path,\n    expected: Option<&str>,\n    prepared: file::PreparedAtomicWrite,\n) -> Result<()> {\n    // Check after formatting, directory creation, writing, and fsync. The\n    // declaration lock coordinates mise writers; unrelated editors do not\n    // participate, so this is not a filesystem compare-and-swap guarantee.\n    check_declaration(path, expected)?;\n    prepared.commit()\n}","sourceCodeStart":314,"sourceCodeEnd":350,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/cli/dotfiles/track.rs#L314-L350","documentation":"Before committing a declaration edit, track.rs re-reads the declaration file and verifies it still matches the content the edit was prepared against (compare-and-swap guard). If another process modified it concurrently, the bail fires to preserve that concurrent edit; the whole enrollment must be retried from scratch.","triggerScenarios":"Two `mise dot track` (or track/untrack) invocations overlapping in time: process A reads the declaration file under declaration_lock(), prepares its edit, and by the time it re-checks, process B (or any external editor) has rewritten the file.","commonSituations":"Parallel CI jobs enrolling dotfiles simultaneously; a user hand-editing the global declaration config while mise is tracking another path; sync tools (e.g. dotfile sync daemons) rewriting the declaration.","solutions":["Simply re-run the `mise dot track` command — the concurrent edit was preserved and is safe","Serialize the enrollments (run them one at a time or under your own lock in scripts)","Inspect the declaration file if repeated retries keep failing, to find the concurrent writer"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for i in 1 2 3; do mise dot track ~/.gitconfig && break; sleep 1; done","preventionTips":["Don't run multiple `mise dot track`/`untrack` commands in parallel","Don't hand-edit the declaration file while track commands run","Suspend dotfile-sync daemons during enrollment"],"tags":["dotfiles","concurrency","conflict","retry"],"backgroundTag":"concurrent-write-conflict","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}