From 2786946ca8bad97c90628a18b25ccf316dfc947f Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 16:48:08 -0500 Subject: [PATCH 1/3] ci(DEN-1462): materialize checkout-owned recovery repair --- ...erialize-den1462-recovery-project-lock.yml | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .github/workflows/materialize-den1462-recovery-project-lock.yml diff --git a/.github/workflows/materialize-den1462-recovery-project-lock.yml b/.github/workflows/materialize-den1462-recovery-project-lock.yml new file mode 100644 index 00000000..577d828a --- /dev/null +++ b/.github/workflows/materialize-den1462-recovery-project-lock.yml @@ -0,0 +1,99 @@ +name: materialize checkout-owned recovery repair + +on: + pull_request: + branches: + - agent/den-1462-mise-export-current-main + paths: + - .github/workflows/materialize-den1462-recovery-project-lock.yml + - src/main.rs + +permissions: + contents: write + +concurrency: + group: den1462-recovery-project-lock + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + materialize: + if: github.event.pull_request.head.repo.full_name == github.repository && github.head_ref == 'fix/den-1462-recovery-project-lock' + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Check out exact repair branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: fix/den-1462-recovery-project-lock + persist-credentials: true + show-progress: false + + - name: Install Rust quality toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: stable + components: rustfmt,clippy + + - name: Acquire checkout ownership before transaction recovery + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + path = Path('src/main.rs') + text = path.read_text() + old = ''' if cwd.join(zed_cli::transaction::STAGING_DIR).is_dir() { + // Every live project transaction already owns this kernel-backed + // install lock. Recover under the same lock so a concurrent process + // cannot mistake an in-flight rollback journal for an abandoned one. + let store = Store::new(&cfg.home); + let _recovery_lock = store.install_lock()?; + zed_cli::transaction::recover_pending(&cwd)?; + } + ''' + new = ''' if cwd.join(zed_cli::transaction::STAGING_DIR).is_dir() { + // Recovery mutates checkout-owned paths. Acquire the canonical + // checkout operation lock before the store lock so another process + // cannot mistake a live transaction journal for an abandoned one. + zed_cli::project_lock::with_lock( + &cwd, + "recover interrupted project transaction", + || { + let store = Store::new(&cfg.home); + let _recovery_lock = store.install_lock()?; + zed_cli::transaction::recover_pending(&cwd)?; + Ok(()) + }, + )?; + } + ''' + if old not in text: + raise SystemExit('startup recovery lock anchor not found') + path.write_text(text.replace(old, new, 1)) + PY + + - name: Validate exact recovery and exporter boundaries + run: | + set -euo pipefail + cargo fmt --all + cargo fmt --all --check + cargo test --locked --test mise_export_api_boundaries + cargo test --locked --test mise_export_cli + cargo test --locked --test mise_export_reserved_paths_cli + cargo clippy --locked --bin zed --test mise_export_api_boundaries \ + --test mise_export_cli --test mise_export_reserved_paths_cli -- -D warnings + + - name: Commit ordinary product source and remove carrier + run: | + set -euo pipefail + rm .github/workflows/materialize-den1462-recovery-project-lock.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add src/main.rs .github/workflows/materialize-den1462-recovery-project-lock.yml + git diff --cached --check + git commit -m 'fix(DEN-1462): serialize startup recovery by checkout' + git push origin HEAD:fix/den-1462-recovery-project-lock From 263e790ae95d4ca73314a828df91e899156778cc Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 16:52:49 -0500 Subject: [PATCH 2/3] test(DEN-1462): clean only the regular operation rendezvous --- tests/mise_export_reserved_paths_cli.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/mise_export_reserved_paths_cli.rs b/tests/mise_export_reserved_paths_cli.rs index 584eca58..a3ec66e3 100644 --- a/tests/mise_export_reserved_paths_cli.rs +++ b/tests/mise_export_reserved_paths_cli.rs @@ -51,6 +51,28 @@ fn run_zed(project: &Path, home: &Path, output: &str) -> Output { run_zed_with_plan(project, home, "zed-env.json", output) } +fn remove_operation_rendezvous(project: &Path) { + let zed_dir = project.join(".zed"); + let operation_lock = zed_dir.join("operation.lock"); + let metadata = fs::symlink_metadata(&operation_lock) + .expect("CLI mutation attempts should retain the operation rendezvous"); + assert!(metadata.is_file()); + assert!(!metadata.file_type().is_symlink()); + + let unexpected = fs::read_dir(&zed_dir) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .filter(|name| name != "operation.lock") + .collect::>(); + assert!( + unexpected.is_empty(), + "reserved-path rejection left native adapter state: {unexpected:?}" + ); + + fs::remove_file(operation_lock).unwrap(); + fs::remove_dir(zed_dir).unwrap(); +} + #[test] fn real_cli_refuses_source_state_and_transaction_staging_destinations() { let temp = tempfile::tempdir().unwrap(); @@ -94,7 +116,7 @@ fn real_cli_refuses_source_state_and_transaction_staging_destinations() { ); assert!(!project.join("mise.toml").exists()); fs::remove_file(project.join(".zed/mise-export-state.json")).unwrap(); - fs::remove_dir(project.join(".zed")).unwrap(); + remove_operation_rendezvous(&project); assert_eq!( fs::read(project.join("zed-env.json")).unwrap(), From 3d17b96aef1ac7ec700c6ada8ddf00910c1415cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:54:21 +0000 Subject: [PATCH 3/3] fix(DEN-1462): serialize startup recovery by checkout --- ...erialize-den1462-recovery-project-lock.yml | 99 ------------------- src/main.rs | 15 +-- 2 files changed, 9 insertions(+), 105 deletions(-) delete mode 100644 .github/workflows/materialize-den1462-recovery-project-lock.yml diff --git a/.github/workflows/materialize-den1462-recovery-project-lock.yml b/.github/workflows/materialize-den1462-recovery-project-lock.yml deleted file mode 100644 index 577d828a..00000000 --- a/.github/workflows/materialize-den1462-recovery-project-lock.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: materialize checkout-owned recovery repair - -on: - pull_request: - branches: - - agent/den-1462-mise-export-current-main - paths: - - .github/workflows/materialize-den1462-recovery-project-lock.yml - - src/main.rs - -permissions: - contents: write - -concurrency: - group: den1462-recovery-project-lock - cancel-in-progress: false - -defaults: - run: - shell: bash - -jobs: - materialize: - if: github.event.pull_request.head.repo.full_name == github.repository && github.head_ref == 'fix/den-1462-recovery-project-lock' - runs-on: ubuntu-24.04 - timeout-minutes: 30 - steps: - - name: Check out exact repair branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: fix/den-1462-recovery-project-lock - persist-credentials: true - show-progress: false - - - name: Install Rust quality toolchain - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 - with: - toolchain: stable - components: rustfmt,clippy - - - name: Acquire checkout ownership before transaction recovery - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - - path = Path('src/main.rs') - text = path.read_text() - old = ''' if cwd.join(zed_cli::transaction::STAGING_DIR).is_dir() { - // Every live project transaction already owns this kernel-backed - // install lock. Recover under the same lock so a concurrent process - // cannot mistake an in-flight rollback journal for an abandoned one. - let store = Store::new(&cfg.home); - let _recovery_lock = store.install_lock()?; - zed_cli::transaction::recover_pending(&cwd)?; - } - ''' - new = ''' if cwd.join(zed_cli::transaction::STAGING_DIR).is_dir() { - // Recovery mutates checkout-owned paths. Acquire the canonical - // checkout operation lock before the store lock so another process - // cannot mistake a live transaction journal for an abandoned one. - zed_cli::project_lock::with_lock( - &cwd, - "recover interrupted project transaction", - || { - let store = Store::new(&cfg.home); - let _recovery_lock = store.install_lock()?; - zed_cli::transaction::recover_pending(&cwd)?; - Ok(()) - }, - )?; - } - ''' - if old not in text: - raise SystemExit('startup recovery lock anchor not found') - path.write_text(text.replace(old, new, 1)) - PY - - - name: Validate exact recovery and exporter boundaries - run: | - set -euo pipefail - cargo fmt --all - cargo fmt --all --check - cargo test --locked --test mise_export_api_boundaries - cargo test --locked --test mise_export_cli - cargo test --locked --test mise_export_reserved_paths_cli - cargo clippy --locked --bin zed --test mise_export_api_boundaries \ - --test mise_export_cli --test mise_export_reserved_paths_cli -- -D warnings - - - name: Commit ordinary product source and remove carrier - run: | - set -euo pipefail - rm .github/workflows/materialize-den1462-recovery-project-lock.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add src/main.rs .github/workflows/materialize-den1462-recovery-project-lock.yml - git diff --cached --check - git commit -m 'fix(DEN-1462): serialize startup recovery by checkout' - git push origin HEAD:fix/den-1462-recovery-project-lock diff --git a/src/main.rs b/src/main.rs index 53bbad23..522db4dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,12 +174,15 @@ fn run(cli: Cli) -> anyhow::Result<()> { let git_submodules = cli.globals.git_submodules; let cwd = std::env::current_dir()?; if cwd.join(zed_cli::transaction::STAGING_DIR).is_dir() { - // Every live project transaction already owns this kernel-backed - // install lock. Recover under the same lock so a concurrent process - // cannot mistake an in-flight rollback journal for an abandoned one. - let store = Store::new(&cfg.home); - let _recovery_lock = store.install_lock()?; - zed_cli::transaction::recover_pending(&cwd)?; + // Recovery mutates checkout-owned paths. Acquire the canonical + // checkout operation lock before the store lock so another process + // cannot mistake a live transaction journal for an abandoned one. + zed_cli::project_lock::with_lock(&cwd, "recover interrupted project transaction", || { + let store = Store::new(&cfg.home); + let _recovery_lock = store.install_lock()?; + zed_cli::transaction::recover_pending(&cwd)?; + Ok(()) + })?; } match cli.cmd { Cmd::Init { org, name } => ops::init(&cwd, org, name, cfg.interactive),