jdx/mise · error

lockfile changed while generation was being prepared; retry…

Error message

lockfile {} changed while generation was being prepared; retry the command

What it means

After acquiring transaction locks, `mise lock` re-verifies every input (configs, initial lockfiles, staged upgrade sources) against snapshots taken before lock acquisition. If a staged lockfile's content no longer matches its pre-lock snapshot, another writer changed it while mise waited for the lock, and generation would be based on stale data — so it aborts.

Solutions

  1. Re-run the command once no other mise writers are active — it is designed to be retried
  2. Serialize mise commands in CI (single job step, or a file lock around them)
  3. Identify the other writer (editor plugins, direnv, scripts calling `mise use`) and prevent concurrent writes

Example fix

// before
mise lock --upgrade &
mise install &
wait
// after
mise lock --upgrade
mise install
Defensive patterns

Strategy: retry

Try / catch

// bash: retry after concurrent-writer abort
mise lock --upgrade || { sleep 5; mise lock --upgrade; }

Prevention

When it happens

Trigger: A concurrent process rewrote a lockfile staged for upgrade between snapshotting and lock acquisition in `run_with_installed`; `read_optional_file(&staged.path)` differs from `staged.original_content`.

Common situations: Parallel `mise lock`/`mise install`/`mise use` invocations in monorepos or CI jobs sharing one checkout; a background tool rewriting mise.lock while you upgrade.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/1a65c2d7c0d9acf0. Report an issue: GitHub.

Appendix: source

Thrown at src/cli/lock.rs:937

                })
                .collect::<Result<Vec<_>>>()?;
            let transaction_paths =
                publication_lock_paths(&mutation_paths, &config_snapshots, &initial_lockfiles);
            let mut transaction_locks = Vec::with_capacity(transaction_paths.len());
            for path in transaction_paths.difference(&generation_lock_paths) {
                transaction_locks.push(
                    crate::lock_file::LockFile::new(path)
                        .with_callback(|l| debug!("waiting for lock on {}", display_path(l)))
                        .lock()?,
                );
            }

            // Lock acquisition may wait behind another writer. Recheck every input,
            // including bumped configs and migration sources, under the transaction locks.
            verify_generation_snapshots(config_snapshots.iter().chain(initial_lockfiles.iter()))?;
            for staged in &staged_upgrade_writes {
                if read_optional_file(&staged.path)? != staged.original_content {
                    bail!(
                        "lockfile {} changed while generation was being prepared; retry the command",
                        display_path(&staged.path)
                    );
                }
            }

            verify_generation_snapshots(
                snapshots
                    .iter()
                    .map(|snapshot| (&snapshot.path, &snapshot.content)),
            )?;
            // Materialize and sync every rollback replacement before the first
            // mutation. Recovery then only needs same-directory renames, so a
            // later disk-full failure cannot prevent restoration by requiring
            // more file data to be written.
            let rollbacks = prepare_lockfile_rollback(&snapshots)?;
            let mut graph_cleanups = Vec::new();
            verify_generation_snapshots(config_snapshots.iter().chain(initial_lockfiles.iter()))?;

View on GitHub (pinned to 533346cc37)