Repository navigation
feat(workspaces): make workspaces list --since usable for CI change detection - #375
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.
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.
✅ Deploy Preview for yarn-v6 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
⏱️ Benchmark Resultsgatsby install-full-cold
📊 Raw benchmark data (gatsby install-full-cold)Base times: 4.345s, 4.294s, 4.451s, 4.285s, 4.257s, 4.372s, 4.304s, 4.314s, 4.411s, 4.312s, 4.348s, 4.314s, 4.308s, 4.349s, 4.282s, 4.284s, 4.377s, 4.325s, 4.301s, 4.292s, 4.290s, 4.309s, 4.395s, 4.343s, 4.336s, 4.296s, 4.314s, 4.255s, 4.340s, 4.445s Head times: 4.650s, 4.369s, 4.312s, 4.394s, 4.364s, 4.302s, 4.383s, 4.282s, 4.412s, 4.357s, 4.295s, 4.285s, 4.321s, 4.366s, 4.392s, 4.421s, 4.357s, 4.375s, 4.446s, 4.434s, 4.373s, 4.428s, 4.283s, 4.368s, 4.361s, 4.351s, 4.374s, 4.333s, 4.327s, 4.296s gatsby install-cache-only
📊 Raw benchmark data (gatsby install-cache-only)Base times: 1.316s, 1.305s, 1.303s, 1.308s, 1.319s, 1.305s, 1.304s, 1.310s, 1.328s, 1.331s, 1.315s, 1.315s, 1.300s, 1.326s, 1.311s, 1.324s, 1.335s, 1.326s, 1.389s, 1.320s, 1.317s, 1.332s, 1.314s, 1.337s, 1.310s, 1.310s, 1.319s, 1.333s, 1.310s, 1.320s Head times: 1.323s, 1.326s, 1.324s, 1.304s, 1.322s, 1.336s, 1.310s, 1.308s, 1.315s, 1.314s, 1.294s, 1.310s, 1.301s, 1.317s, 1.329s, 1.596s, 1.321s, 1.305s, 1.319s, 1.321s, 1.348s, 1.316s, 1.291s, 1.318s, 1.307s, 1.309s, 1.312s, 1.321s, 1.280s, 1.296s gatsby install-cache-and-lock (warm, with lockfile)
📊 Raw benchmark data (gatsby install-cache-and-lock (warm, with lockfile))Base times: 0.357s, 0.359s, 0.360s, 0.366s, 0.363s, 0.362s, 0.360s, 0.358s, 0.359s, 0.361s, 0.361s, 0.360s, 0.364s, 0.361s, 0.362s, 0.358s, 0.360s, 0.359s, 0.364s, 0.362s, 0.365s, 0.362s, 0.358s, 0.360s, 0.360s, 0.361s, 0.358s, 0.364s, 0.362s, 0.364s Head times: 0.364s, 0.363s, 0.364s, 0.395s, 0.361s, 0.360s, 0.356s, 0.360s, 0.360s, 0.359s, 0.356s, 0.361s, 0.361s, 0.367s, 0.359s, 0.359s, 0.357s, 0.356s, 0.383s, 0.355s, 0.358s, 0.361s, 0.354s, 0.352s, 0.359s, 0.359s, 0.356s, 0.357s, 0.366s, 0.362s |
… 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 workspaces foreach --since` and `yarn tasks run --since` share the same detection.
…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.
`yarn workspaces foreach --since` and `yarn tasks run --since` can now compare two refs with `--head <ref>`, like `yarn workspaces list`. An explicit `--since <ref>` / `--head <ref>` takes precedence over the `YARN_CHANGESET_*` / `TURBO_SCM_*` environment variables.
5365125 to
3dba5ef
Compare
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 28d746b3-a2c6-44a0-9471-9b5526f2e63a) |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: eaa86a90-1a0a-455d-9684-8ee99b2e8e03) |
Main already has squashed, revised versions of the earlier commits on this branch (#373, #374, #375), so main wins for every conflicted file. The only change kept from the branch is 86a2038. Its acceptance test now uses main's `@defaultTask` / `*:build` syntax, because main dropped `@workspaces` and `^name`.
…376) **Issue:** running a `@long-lived` task that depends on `^build`: - hid the output and status of the dependencies, which ran in the long-lived context; - on Ctrl-C in standalone mode, printed "still running in the background" while the server died with the process; - ignored SIGTERM. **Fix:** subscriptions deliver events for every task they pushed, whatever its context. Standalone runs stop their long-lived targets on Ctrl-C (exit 130) and SIGTERM (exit 143). Daemon runs still detach on Ctrl-C. Covered by acceptance tests. Stacked on #375. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches task runner signal handling and daemon notification filtering; behavior changes for long-lived tasks and CI shutdown paths, mitigated by new acceptance tests. > > **Overview** > Fixes `@long-lived` task runs so **dependency output and status** still stream to the CLI when those deps execute in a separate long-lived context, by treating every task the subscription pushed (`all_task_ids`) as in-scope for notifications—not only explicit targets in the run context. > > **Interrupt handling** on Unix now listens for **SIGTERM** (CI/`timeout`) as well as SIGINT. Daemon runs can still **detach** long-lived targets on Ctrl-C; **standalone** runs and any **SIGTERM** always cancel, explicitly **stop** started long-lived targets, and exit with the right signal code (**130** vs **143**). > > Adds acceptance tests for workspace dependency build output, standalone SIGINT teardown, and SIGTERM cleanup. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a759e06. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->


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 how
--sinceselects workspaces across list/foreach/tasks run, which can shift CI task scope; behavior is heavily tested but regressions would affect monorepo pipelines.Overview
Makes workspace change detection CI-friendly by centralizing git range logic and aligning behavior with Turbo-style SCM.
Adds
changesetGlobalFiles(like TurboglobalDependencies): a matching file change marks every workspace affected. Introduces--head <ref>so--sincecan diff two commits instead of the working tree; unset bounds fall back toYARN_CHANGESET_*orTURBO_SCM_*. Lockfile diffs now only flag workspaces whose dependency trees actually changed;.pnp/.yarninstall artifacts are ignored.workspaces list --sincedrops its ad-hoc git/install-state logic and uses sharedChangesetRangehelpers;--recursive(turbo ls --affected) walks dependents via manifests without a lazy install. The same path powersworkspaces foreachandtasks run--sinceselection. Acceptance tests cover global files, ref-to-ref compares, env precedence, and lockfile scoping.Reviewed by Cursor Bugbot for commit af6c4c6. Bugbot is set up for automated code reviews on this repo. Configure here.