Skip to content

Commit 92a378e

Browse files
markhallenMark AllenCopilot
authored
Suppress Docker digest-only updates when tag version is unchanged (#15103)
* Suppress Docker digest-only updates when tag version is unchanged When a Dockerfile pins both a tag and a digest (e.g., FROM golang:1.26.3@sha256:...), Dependabot would propose PRs that only update the digest when the same tag was re-pushed on the registry, even though the tag version hadn't changed. This adds a new experiment flag docker_digest_only_update_suppression that, when enabled, treats the digest as up-to-date if the latest resolved tag name matches the current tag name. This prevents noisy digest-only PRs while still updating the digest whenever the tag version actually advances. Fixes #15081 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Gate digest suppression on comparable tags only Non-comparable tags (e.g., 'latest', distro codenames like 'artful') should still receive digest updates since they cannot be version-compared. Only versioned/comparable tags get digest-only suppression. Adds test for non-comparable tag+digest pin scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Mark Allen <markhallen@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e15f8ca commit 92a378e

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

docker/lib/dependabot/docker/update_checker.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,18 @@ def digest_up_to_date?
213213
expected_digest =
214214
if source_tag
215215
latest_tag = latest_tag_from(source_tag)
216+
217+
# When digest-only updates are suppressed and the tag hasn't changed,
218+
# treat the digest as up-to-date to avoid proposing a PR that only
219+
# bumps the digest without a corresponding version change.
220+
# Only apply to comparable (versioned) tags — non-comparable tags like
221+
# "latest" or distro codenames should still get digest updates.
222+
if Dependabot::Experiments.enabled?(:docker_digest_only_update_suppression) &&
223+
Tag.new(source_tag).comparable? &&
224+
latest_tag.name == source_tag
225+
next true
226+
end
227+
216228
digest_of(latest_tag.name)
217229
else
218230
updated_digest

docker/spec/dependabot/docker/update_checker_spec.rb

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ def stub_tag_with_no_digest(tag)
8585
end
8686

8787
it { is_expected.to be_falsy }
88+
89+
context "when docker_digest_only_update_suppression experiment is enabled" do
90+
before do
91+
allow(Dependabot::Experiments).to receive(:enabled?)
92+
.with(:docker_digest_only_update_suppression).and_return(true)
93+
allow(Dependabot::Experiments).to receive(:enabled?)
94+
.with(:docker_created_timestamp_validation).and_return(false)
95+
allow(Dependabot::Experiments).to receive(:enabled?)
96+
.with(:docker_pin_digests).and_return(false)
97+
end
98+
99+
it { is_expected.to be_truthy }
100+
end
88101
end
89102
end
90103

@@ -3225,6 +3238,130 @@ def stub_tag_with_no_digest(tag)
32253238
end
32263239
end
32273240

3241+
describe "#digest_up_to_date? with docker_digest_only_update_suppression experiment" do
3242+
subject(:digest_up_to_date?) { checker.send(:digest_up_to_date?) }
3243+
3244+
let(:headers_response) do
3245+
fixture("docker", "registry_manifest_headers", "generic.json")
3246+
end
3247+
3248+
context "when experiment is enabled" do
3249+
before do
3250+
allow(Dependabot::Experiments).to receive(:enabled?)
3251+
.with(:docker_digest_only_update_suppression).and_return(true)
3252+
allow(Dependabot::Experiments).to receive(:enabled?)
3253+
.with(:docker_created_timestamp_validation).and_return(false)
3254+
allow(Dependabot::Experiments).to receive(:enabled?)
3255+
.with(:docker_pin_digests).and_return(false)
3256+
end
3257+
3258+
context "when the tag has not changed but the digest has" do
3259+
let(:version) { "17.10" }
3260+
let(:source) do
3261+
{
3262+
tag: "17.10",
3263+
digest: "old_digest_that_differs_from_registry"
3264+
}
3265+
end
3266+
3267+
before do
3268+
stub_request(:head, repo_url + "manifests/17.10")
3269+
.and_return(status: 200, headers: JSON.parse(headers_response))
3270+
end
3271+
3272+
it "treats the digest as up-to-date (suppresses digest-only update)" do
3273+
expect(digest_up_to_date?).to be true
3274+
end
3275+
end
3276+
3277+
context "when the tag has changed and the digest differs" do
3278+
let(:version) { "17.04" }
3279+
let(:source) do
3280+
{
3281+
tag: "17.04",
3282+
digest: "old_digest"
3283+
}
3284+
end
3285+
3286+
before do
3287+
stub_request(:head, repo_url + "manifests/17.10")
3288+
.and_return(status: 200, headers: JSON.parse(headers_response))
3289+
end
3290+
3291+
it "reports the digest as out of date" do
3292+
expect(digest_up_to_date?).to be false
3293+
end
3294+
end
3295+
3296+
context "when only a digest is present (no tag)" do
3297+
let(:version) { "latest" }
3298+
let(:source) do
3299+
{
3300+
digest: "old_digest"
3301+
}
3302+
end
3303+
3304+
before do
3305+
stub_request(:head, repo_url + "manifests/latest")
3306+
.and_return(status: 200, headers: JSON.parse(headers_response))
3307+
end
3308+
3309+
it "still detects digest changes (suppression only applies to tagged images)" do
3310+
expect(digest_up_to_date?).to be false
3311+
end
3312+
end
3313+
3314+
context "when the tag is non-comparable (e.g., 'latest' or distro codename) with digest" do
3315+
let(:version) { "artful" }
3316+
let(:source) do
3317+
{
3318+
tag: "artful",
3319+
digest: "old_digest_that_differs_from_registry"
3320+
}
3321+
end
3322+
3323+
before do
3324+
stub_request(:head, repo_url + "manifests/artful")
3325+
.and_return(status: 200, headers: JSON.parse(headers_response))
3326+
end
3327+
3328+
it "still detects digest changes (suppression only applies to versioned tags)" do
3329+
expect(digest_up_to_date?).to be false
3330+
end
3331+
end
3332+
end
3333+
3334+
context "when experiment is disabled" do
3335+
before do
3336+
allow(Dependabot::Experiments).to receive(:enabled?)
3337+
.with(:docker_digest_only_update_suppression).and_return(false)
3338+
allow(Dependabot::Experiments).to receive(:enabled?)
3339+
.with(:docker_created_timestamp_validation).and_return(false)
3340+
allow(Dependabot::Experiments).to receive(:enabled?)
3341+
.with(:docker_pin_digests).and_return(false)
3342+
end
3343+
3344+
context "when the tag has not changed but the digest has" do
3345+
let(:version) { "17.10" }
3346+
let(:source) do
3347+
{
3348+
tag: "17.10",
3349+
digest: "old_digest_that_differs_from_registry"
3350+
}
3351+
end
3352+
3353+
before do
3354+
stub_request(:head, repo_url + "manifests/17.10")
3355+
.and_return(status: 200, headers: JSON.parse(headers_response))
3356+
end
3357+
3358+
it "reports the digest as out of date (existing behavior)" do
3359+
expect(digest_up_to_date?).to be false
3360+
end
3361+
end
3362+
end
3363+
end
3364+
32283365
private
32293366

32303367
def stub_same_sha_for(*tags)

0 commit comments

Comments
 (0)