From d3c55622c807da9b914a4bc529b71fb439e7180f Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Fri, 10 Jul 2026 00:33:33 +0100 Subject: [PATCH] Match existing group PRs covering a subset of job directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A grouped update PR only records the directories that actually had updates, so it can legitimately cover fewer directories than the job is configured with (e.g. one configured directory had no updatable dependencies that run). The exact set equality introduced in 43492ac83 treated such PRs as non-existent on every scheduled run, so a brand-new group PR was opened each time instead of deferring to the refresh job, and the old PR was never superseded — duplicate PRs accumulated until closed by hand. Relax the check to match when the PR's directories are a subset of the job's directories. This preserves both earlier fixes: a PR in an unrelated directory still doesn't suppress a new PR (#9430), and a stale PR covering directories outside the job's scope still doesn't match (43492ac83). Fixes #15370 Co-Authored-By: Claude Fable 5 --- .../updater/group_update_creation.rb | 8 ++- .../group_update_all_versions_spec.rb | 61 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/updater/lib/dependabot/updater/group_update_creation.rb b/updater/lib/dependabot/updater/group_update_creation.rb index 16b75d3de68..e5bf856c0d4 100644 --- a/updater/lib/dependabot/updater/group_update_creation.rb +++ b/updater/lib/dependabot/updater/group_update_creation.rb @@ -586,8 +586,12 @@ def existing_pr_covers_job_directories?(pull_request) normalized_job_dirs = job_directories.map { |d| Pathname.new(d).cleanpath.to_s }.uniq normalized_pr_dirs = pr_directories.map { |d| Pathname.new(d).cleanpath.to_s }.uniq - # Match only when the PR directories exactly match the job directories - normalized_job_dirs.sort == normalized_pr_dirs.sort + # Match when the PR's directories are a subset of the job's directories. + # A PR only records the directories that actually had updates, so it can + # legitimately cover fewer directories than the job is configured with. + # A PR covering directories outside the job's scope is stale or belongs + # to a different configuration, so it is not a match. + (normalized_pr_dirs - normalized_job_dirs).empty? end sig do diff --git a/updater/spec/dependabot/updater/operations/group_update_all_versions_spec.rb b/updater/spec/dependabot/updater/operations/group_update_all_versions_spec.rb index c05b660af54..f7b20e6d03d 100644 --- a/updater/spec/dependabot/updater/operations/group_update_all_versions_spec.rb +++ b/updater/spec/dependabot/updater/operations/group_update_all_versions_spec.rb @@ -345,6 +345,67 @@ end end + context "when PR covers a subset of the job's directories" do + before do + allow(job).to receive_messages( + existing_group_pull_requests: [ + { + "dependency-group-name" => "dummy-group", + "pr_number" => 123, + "dependencies" => [ + { + "dependency-name" => "rollup", + "dependency-version" => "2.79.2", + "directory" => "/" + } + ] + } + ].map { |pr| Dependabot::Job::ExistingGroupPullRequest.from_hash(pr) }, + source: mock_source_with_multiple_dirs + ) + end + + it "skips creating a new PR" do + expect(mock_create_group_update).not_to receive(:perform) + expect(dependency_snapshot).to receive(:mark_group_handled).with(dependency_group) + perform + end + end + + context "when PR covers directories outside the job's directories" do + before do + allow(job).to receive_messages( + existing_group_pull_requests: [ + { + "dependency-group-name" => "dummy-group", + "pr_number" => 123, + "dependencies" => [ + { + "dependency-name" => "rollup", + "dependency-version" => "2.79.2", + "directory" => "/" + }, + { + "dependency-name" => "rollup", + "dependency-version" => "2.79.2", + "directory" => "/packages/corelib" + } + ] + } + ].map { |pr| Dependabot::Job::ExistingGroupPullRequest.from_hash(pr) }, + source: mock_source + ) + allow(mock_source).to receive(:directory).and_return("/") + end + + it "creates a new PR" do + allow(mock_create_group_update).to receive(:perform).and_return(mock_dependency_change) + expect(mock_create_group_update).to receive(:perform) + expect(dependency_snapshot).not_to receive(:mark_group_handled).with(dependency_group) + perform + end + end + context "when existing PR has no directory info" do before do allow(job).to receive_messages(