Skip to content

Commit 6463993

Browse files
Copilotrobaiken
andauthored
Fix cooldown breaking Docker updates when registry API calls fail (#14149)
* Initial plan * Fix cooldown breaking Docker updates by handling registry errors gracefully When cooldown is enabled for Docker dependencies, the update checker makes additional API calls (digest + blob HEAD) to determine tag publication dates. These calls can fail with 404, 401, or other errors for certain images/registries. Changes: - Add error handling in get_tag_publication_details to catch registry errors (NotFound, auth, rate limiting) and return nil instead of crashing the entire update process - Fix apply_cooldown to treat tags with unknown publication dates as "not in cooldown" instead of skipping them (which could block all updates when the registry doesn't support the required API calls) - Remove incorrect T.cast in publication_detail that would fail when get_tag_publication_details returns nil Co-authored-by: robaiken <6567647+robaiken@users.noreply.github.com> * Address code review: clarify apply_cooldown comment Co-authored-by: robaiken <6567647+robaiken@users.noreply.github.com> * Merge main (includes #14691), rename test to reflect HEAD request context Co-authored-by: robaiken <6567647+robaiken@users.noreply.github.com> * Fix RSpec/ReceiveMessages lint offenses in cooldown tests Co-authored-by: robaiken <6567647+robaiken@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: robaiken <6567647+robaiken@users.noreply.github.com>
1 parent e10de6e commit 6463993

2 files changed

Lines changed: 135 additions & 2 deletions

File tree

docker/lib/dependabot/docker/update_checker.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,9 @@ def apply_cooldown(candidate_tags)
374374
candidate_tags.reverse_each do |tag|
375375
details = publication_detail(tag)
376376

377-
next if !details || !details.released_at
377+
# If we can't determine publication details, skip cooldown for this tag and use it
378+
# rather than blocking the update when the registry doesn't support the required API calls
379+
return [tag] if !details || !details.released_at
378380

379381
return [tag] unless cooldown_period?(T.must(details.released_at), tag)
380382

@@ -389,7 +391,7 @@ def publication_detail(candidate_tag)
389391
return publication_details[candidate_tag.name] if publication_details.key?(candidate_tag.name)
390392

391393
details = get_tag_publication_details(candidate_tag)
392-
publication_details[candidate_tag.name] = T.cast(details, Dependabot::Package::PackageRelease)
394+
publication_details[candidate_tag.name] = details
393395

394396
details
395397
end
@@ -425,6 +427,15 @@ def get_tag_publication_details(tag)
425427
url: nil,
426428
package_type: "docker"
427429
)
430+
rescue *transient_docker_errors,
431+
DockerRegistry2::RegistryAuthenticationException,
432+
RestClient::Forbidden,
433+
RestClient::TooManyRequests => e
434+
Dependabot.logger.warn(
435+
"Failed to fetch publication details for #{docker_repo_name}:#{tag.name}, " \
436+
"skipping cooldown: #{e.class} - #{e.message}"
437+
)
438+
nil
428439
end
429440

430441
sig do

docker/spec/dependabot/docker/update_checker_spec.rb

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,6 +1522,56 @@ def stub_tag_with_no_digest(tag)
15221522
it { is_expected.to eq("17.10") }
15231523
end
15241524

1525+
describe "with cooldown options when HEAD request returns 404" do
1526+
subject(:latest_version) { checker.latest_version }
1527+
1528+
let(:update_cooldown) do
1529+
Dependabot::Package::ReleaseCooldownOptions.new(default_days: 7)
1530+
end
1531+
1532+
before do
1533+
mock_client = instance_double(DockerRegistry2::Registry)
1534+
allow(checker).to receive(:docker_registry_client).and_return(mock_client)
1535+
allow(mock_client).to receive_messages(
1536+
tags: { "tags" => %w(17.04 17.10) },
1537+
digest: "sha256:abc123",
1538+
manifest_digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86ca97eba880ebf600d68608"
1539+
)
1540+
allow(mock_client).to receive(:dohead).and_raise(DockerRegistry2::NotFound)
1541+
allow(Dependabot.logger).to receive(:warn)
1542+
allow(Dependabot.logger).to receive(:info)
1543+
end
1544+
1545+
it "still returns the latest version instead of crashing" do
1546+
expect(latest_version).to eq("17.10")
1547+
end
1548+
end
1549+
1550+
describe "with cooldown options when digest request raises authentication error" do
1551+
subject(:latest_version) { checker.latest_version }
1552+
1553+
let(:update_cooldown) do
1554+
Dependabot::Package::ReleaseCooldownOptions.new(default_days: 7)
1555+
end
1556+
1557+
before do
1558+
mock_client = instance_double(DockerRegistry2::Registry)
1559+
allow(checker).to receive(:docker_registry_client).and_return(mock_client)
1560+
allow(mock_client).to receive_messages(
1561+
tags: { "tags" => %w(17.04 17.10) },
1562+
manifest_digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86ca97eba880ebf600d68608"
1563+
)
1564+
allow(mock_client).to receive(:digest)
1565+
.and_raise(DockerRegistry2::RegistryAuthenticationException)
1566+
allow(Dependabot.logger).to receive(:warn)
1567+
allow(Dependabot.logger).to receive(:info)
1568+
end
1569+
1570+
it "still returns the latest version instead of crashing" do
1571+
expect(latest_version).to eq("17.10")
1572+
end
1573+
end
1574+
15251575
context "when the dependency has a compound suffix with alpine version" do
15261576
let(:dependency_name) { "golang" }
15271577
let(:version) { "1.26.0-alpine3.23" }
@@ -2944,6 +2994,78 @@ def stub_tag_with_no_digest(tag)
29442994
expect(result.version.class).to eq(Dependabot::Docker::Version)
29452995
end
29462996
end
2997+
2998+
context "when client.digest raises DockerRegistry2::NotFound" do
2999+
before do
3000+
allow(mock_client).to receive(:digest).and_raise(DockerRegistry2::NotFound)
3001+
allow(Dependabot.logger).to receive(:warn)
3002+
end
3003+
3004+
it "returns nil and logs a warning" do
3005+
expect(get_tag_publication_details).to be_nil
3006+
expect(Dependabot.logger).to have_received(:warn).with(
3007+
/Failed to fetch publication details.*skipping cooldown.*NotFound/
3008+
)
3009+
end
3010+
end
3011+
3012+
context "when client.dohead raises DockerRegistry2::NotFound for blob" do
3013+
before do
3014+
allow(mock_client).to receive(:digest).and_return("sha256:abc123")
3015+
allow(mock_client).to receive(:dohead).and_raise(DockerRegistry2::NotFound)
3016+
allow(Dependabot.logger).to receive(:warn)
3017+
end
3018+
3019+
it "returns nil and logs a warning" do
3020+
expect(get_tag_publication_details).to be_nil
3021+
expect(Dependabot.logger).to have_received(:warn).with(
3022+
/Failed to fetch publication details.*skipping cooldown.*NotFound/
3023+
)
3024+
end
3025+
end
3026+
3027+
context "when client.digest raises RegistryAuthenticationException" do
3028+
before do
3029+
allow(mock_client).to receive(:digest)
3030+
.and_raise(DockerRegistry2::RegistryAuthenticationException)
3031+
allow(Dependabot.logger).to receive(:warn)
3032+
end
3033+
3034+
it "returns nil and logs a warning" do
3035+
expect(get_tag_publication_details).to be_nil
3036+
expect(Dependabot.logger).to have_received(:warn).with(
3037+
/Failed to fetch publication details.*skipping cooldown.*RegistryAuthenticationException/
3038+
)
3039+
end
3040+
end
3041+
3042+
context "when client.digest raises RestClient::Forbidden" do
3043+
before do
3044+
allow(mock_client).to receive(:digest).and_raise(RestClient::Forbidden)
3045+
allow(Dependabot.logger).to receive(:warn)
3046+
end
3047+
3048+
it "returns nil and logs a warning" do
3049+
expect(get_tag_publication_details).to be_nil
3050+
expect(Dependabot.logger).to have_received(:warn).with(
3051+
/Failed to fetch publication details.*skipping cooldown.*Forbidden/
3052+
)
3053+
end
3054+
end
3055+
3056+
context "when client.digest raises RestClient::TooManyRequests" do
3057+
before do
3058+
allow(mock_client).to receive(:digest).and_raise(RestClient::TooManyRequests)
3059+
allow(Dependabot.logger).to receive(:warn)
3060+
end
3061+
3062+
it "returns nil and logs a warning" do
3063+
expect(get_tag_publication_details).to be_nil
3064+
expect(Dependabot.logger).to have_received(:warn).with(
3065+
/Failed to fetch publication details.*skipping cooldown.*TooManyRequests/
3066+
)
3067+
end
3068+
end
29473069
end
29483070

29493071
describe "#digest_up_to_date?" do

0 commit comments

Comments
 (0)