Repository navigation
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Merge base ignores the head ref
- Modified fetch_branch_base to accept and use the head ref parameter when computing merge base, ensuring correct base computation when --head is set without explicit --base.
- ✅ Fixed: Head still uses working tree manifests
- Added trust_current_lockfile parameter to find_changed_workspaces to skip working tree hash verification when comparing two refs, preventing uncommitted changes from incorrectly affecting change detection.
- ✅ Fixed: Generated Yarn files mark root changed
- Added should_ignore_generated_yarn_file filter to skip .pnp.cjs, .pnp.loader.mjs, and .yarn/ files during change detection, preventing zero-install repos from incorrectly marking root as changed.
Or push these changes by commenting:
@cursor push 66cea77c2f
Preview (66cea77c2f)
diff --git a/packages/zpm/src/commands/debug/print_branch_base.rs b/packages/zpm/src/commands/debug/print_branch_base.rs
--- a/packages/zpm/src/commands/debug/print_branch_base.rs
+++ b/packages/zpm/src/commands/debug/print_branch_base.rs
@@ -14,7 +14,7 @@
= project::Project::new(None).await?;
let branch_base
- = fetch_branch_base(&project).await?;
+ = fetch_branch_base(&project, None).await?;
println!("{}", branch_base);
diff --git a/packages/zpm/src/commands/version/check.rs b/packages/zpm/src/commands/version/check.rs
--- a/packages/zpm/src/commands/version/check.rs
+++ b/packages/zpm/src/commands/version/check.rs
@@ -95,7 +95,7 @@
// Only branch-local versioning files count — base-branch entries
// can't satisfy bumps required by this branch.
- let base = match fetch_branch_base(project).await {
+ let base = match fetch_branch_base(project, None).await {
Ok(base) => base,
Err(_) => return Ok(VersioningState { releases, declined }),
};
diff --git a/packages/zpm/src/git_utils.rs b/packages/zpm/src/git_utils.rs
--- a/packages/zpm/src/git_utils.rs
+++ b/packages/zpm/src/git_utils.rs
@@ -64,7 +64,7 @@
Ok(remotes)
}
-pub async fn fetch_branch_base(project: &Project) -> Result<String, Error> {
+pub async fn fetch_branch_base(project: &Project, head: Option<&str>) -> Result<String, Error> {
let base_refs
= project.config.settings.changeset_base_refs.iter()
.map(|s| s.value.to_string())
@@ -88,7 +88,7 @@
}
let mut args
- = vec!["merge-base".to_string(), "HEAD".to_string()];
+ = vec!["merge-base".to_string(), head.unwrap_or("HEAD").to_string()];
args.extend(branches.clone());
@@ -130,6 +130,28 @@
None
}
+/// Returns true if the file is a generated Yarn file that should be ignored
+/// during change detection (PnP artifacts and zero-installs cache).
+fn should_ignore_generated_yarn_file(project_root: &Path, file: &Path) -> bool {
+ let Some(rel_path) = file.forward_relative_to(project_root) else {
+ return false;
+ };
+
+ let rel_str = rel_path.as_str();
+
+ // Skip PnP files at the root
+ if rel_str == ".pnp.cjs" || rel_str == ".pnp.loader.mjs" {
+ return true;
+ }
+
+ // Skip the .yarn directory (zero-installs cache and other artifacts)
+ if rel_str == ".yarn" || rel_str.starts_with(".yarn/") {
+ return true;
+ }
+
+ false
+}
+
pub async fn fetch_base(root: &Path, base_refs: &[&str]) -> Result<String, Error> {
let mut ancestor_bases
= Vec::new();
@@ -170,7 +192,7 @@
pub async fn fetch_changed_workspaces(project: &Project, since: Option<&str>) -> Result<BTreeMap<Ident, BTreeSet<Path>>, Error> {
let since_ref = match since {
Some(since) => since.to_string(),
- None => fetch_branch_base(project).await?,
+ None => fetch_branch_base(project, None).await?,
};
let changed_files
@@ -198,6 +220,11 @@
continue;
}
+ // Skip generated Yarn files (PnP and zero-installs cache)
+ if should_ignore_generated_yarn_file(&project.project_cwd, file) {
+ continue;
+ }
+
let workspace
= project.workspaces.iter()
.filter(|w| w.path.contains(file))
@@ -229,7 +256,7 @@
= fetch_lockfile_at_ref(project, since_ref).await
.unwrap_or_else(|_| Lockfile::new());
- for ident in find_changed_workspaces(project, &old_lockfile, ¤t_lockfile) {
+ for ident in find_changed_workspaces(project, &old_lockfile, ¤t_lockfile, head.is_some()) {
changed_workspaces.entry(ident)
.or_default()
.insert(lockfile_path.clone());
@@ -270,7 +297,7 @@
pub async fn fetch_changed_files(project: &Project, since: Option<&str>) -> Result<BTreeSet<Path>, Error> {
let since = match since {
Some(since) => since.to_string(),
- None => fetch_branch_base(project).await?,
+ None => fetch_branch_base(project, None).await?,
};
fetch_changed_files_between(project, &since, None).await
@@ -371,7 +398,7 @@
pub async fn fetch_changed_workspaces_in_range(project: &Project, range: &ChangesetRange) -> Result<BTreeSet<Ident>, Error> {
let since_ref = match &range.base {
Some(base) => base.clone(),
- None => fetch_branch_base(project).await?,
+ None => fetch_branch_base(project, range.head.as_deref()).await?,
};
let changed_files
diff --git a/packages/zpm/src/lockfile_tree.rs b/packages/zpm/src/lockfile_tree.rs
--- a/packages/zpm/src/lockfile_tree.rs
+++ b/packages/zpm/src/lockfile_tree.rs
@@ -229,7 +229,7 @@
* workspaces have the same dependencies on both sides, so we walk the two
* trees side by side until we find a descriptor that resolves differently.
*/
-pub fn find_changed_workspaces(project: &Project, base: &Lockfile, current: &Lockfile) -> BTreeSet<Ident> {
+pub fn find_changed_workspaces(project: &Project, base: &Lockfile, current: &Lockfile, trust_current_lockfile: bool) -> BTreeSet<Ident> {
let islands
= workspace_islands(project);
let workspace_dependencies
@@ -250,12 +250,14 @@
// The lockfile from the working tree may be stale, so we also check
// that its hash matches the dependencies we're about to walk.
+ // When comparing two refs (trust_current_lockfile=true), we trust
+ // the current lockfile's hash without verifying against the working tree.
let dependencies = dependencies.as_ref().ok().filter(|dependencies| {
let hash
= hash_workspace_dependencies(dependencies);
base.project.workspaces.get(ident) == Some(&hash)
- && current.project.workspaces.get(ident) == Some(&hash)
+ && (trust_current_lockfile || current.project.workspaces.get(ident) == Some(&hash))
});
let Some(dependencies) = dependencies else {
diff --git a/packages/zpm/src/versioning.rs b/packages/zpm/src/versioning.rs
--- a/packages/zpm/src/versioning.rs
+++ b/packages/zpm/src/versioning.rs
@@ -207,7 +207,7 @@
}
pub async fn versioning_path(&self) -> Result<Path, Error> {
- let Some(base) = fetch_branch_base(self.project).await.ok() else {
+ let Some(base) = fetch_branch_base(self.project, None).await.ok() else {
return self.create_versioning_path();
};You can send follow-ups to the cloud agent here.
… detection CI jobs need the list of workspaces affected by a change, including changes to files outside any workspace. - New `changesetGlobalFiles` setting: a change to a matching file marks every workspace as changed. - `--head <ref>` compares two refs instead of the working tree. `YARN_CHANGESET_BASE` / `YARN_CHANGESET_HEAD` fill unset bounds, falling back to `TURBO_SCM_BASE` / `TURBO_SCM_HEAD`. - `--since` now uses the lockfile-aware detection, so a lockfile change only marks workspaces whose dependency tree changed. - `-R` follows the manifests and no longer needs an install state. - `yarn tasks run --affected/--since` shares the same code.
…acts - With --head (or YARN_CHANGESET_HEAD / TURBO_SCM_HEAD) and no base, the merge base was computed against the current checkout rather than the head ref, so the range was wrong whenever the checkout was elsewhere. - workspaces list --since used to ignore .pnp.cjs, .pnp.loader.mjs and .yarn/; going through the shared detection made them mark the root workspace as changed. They're skipped again.
63f285a to
a2b4482
Compare
40e7e79 to
5365125
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
Bugbot Autofix is ON, but it could not run because the spend limit has been reached. To enable Bugbot Autofix, raise your spend limit in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5365125. Configure here.
| .collect::<BTreeSet<_>>(); | ||
|
|
||
| return Ok(changed_files); | ||
| } |
There was a problem hiding this comment.
Two-ref diff is tip-to-tip
Medium Severity
When both bounds are set (--since/--head or TURBO_SCM_BASE/TURBO_SCM_HEAD), changed files come from a tip-to-tip git diff of the two refs. That includes files that only moved on the base branch after the fork, so CI reports workspaces the PR never touched. Turbo’s matching env vars use a three-dot / merge-base diff instead; the no-base path already computes a merge base, so the two-ref path disagrees with both turbo and that fallback.
Reviewed by Cursor Bugbot for commit 5365125. Configure here.
|
|
||
| if touches_global_files(project, &changed_files) { | ||
| return Ok(project.workspaces.iter().map(|w| w.name.clone()).collect()); | ||
| } |
There was a problem hiding this comment.
Global files miss install artifacts
Medium Severity
Install artifacts are dropped from the changed-file set before changesetGlobalFiles is evaluated. Patterns that match .yarn/** or PnP files therefore never mark every workspace as changed, even when the user listed them as global inputs. Yarn stores patches and other repo-wide files under .yarn/, which is exactly what this setting is for.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5365125. Configure here.



Issue:
yarn workspaces list --sincewasn't enough for CI change detection: changes to repository-wide files outside workspaces were ignored, it couldn't compare two refs, and any lockfile change looked like a change to everything.Fix:
changesetGlobalFilessetting: a change to a matching file marks every workspace as changed.--head <ref>compares two refs.YARN_CHANGESET_BASE/YARN_CHANGESET_HEAD(orTURBO_SCM_BASE/TURBO_SCM_HEAD) fill unset bounds.--sinceuses the lockfile-aware detection, and-Rfollows manifests without needing an install state.yarn tasks run --affected/--sinceshares the same code.Covered by acceptance tests. Stacked on #374.
Note
Medium Risk
Changes which workspaces CI and
--since/--affectedconsider modified (lockfile semantics, global globs, ref ranges); incorrect detection could skip or over-run jobs, but behavior is covered by new acceptance tests.Overview
Improves CI-style workspace change detection for
yarn workspaces list --since,-R, andyarn tasks run --since/--affectedby centralizing logic ingit_utilsbehind aChangesetRange(base + optional head).Adds
changesetGlobalFiles(turbo-style global deps): matching globs mark every workspace as changed. Adds--head <ref>and env defaultsYARN_CHANGESET_BASE/YARN_CHANGESET_HEAD(withTURBO_SCM_*fallbacks) to compare two refs or override bounds without flags.Lockfile changes now only flag workspaces whose dependency tree actually changed;
.pnp.*/.yarnchurn is ignored.-Rreuses shared affected detection (changed workspaces + transitive dependents from manifests) and no longer requires a lazy install. Acceptance tests cover global files, ref-to-ref diffs, env vars, and lockfile scoping.Reviewed by Cursor Bugbot for commit 5365125. Bugbot is set up for automated code reviews on this repo. Configure here.