Skip to content

Commit b6d2463

Browse files
author
v-HaripriyaC
committed
feat: add cooldown filter for github_actions with rebase
This implements the fix for #14579 where the cooldown filter only checked the latest release, blocking all updates for frequently-released dependencies. Key improvements: - Fetch all allowed version tags with release dates in single git clone - Evaluate cooldown across all allowed versions - Return newest version outside cooldown window instead of 'no viable release' - Use shared CooldownCalculation.within_cooldown_window? for consistency - Proper error handling with appropriate logging levels - Type-safe with Sorbet signatures Fixed issues from code review: - Consolidated duplicate allowed_version_tags_with_release_dates logic - Use shared CooldownCalculation utility (not hand-rolled) - Removed unused excon dependency - Fixed RuboCop line length violations - Fixed logger.error usage in error paths
1 parent e70a010 commit b6d2463

5 files changed

Lines changed: 413 additions & 32 deletions

File tree

github_actions/lib/dependabot/github_actions/package/package_details_fetcher.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require "time"
99

1010
require "dependabot/errors"
11+
require "dependabot/git_tag_with_detail"
1112
require "dependabot/github_actions/helpers"
1213
require "dependabot/github_actions/requirement"
1314
require "dependabot/github_actions/update_checker"
@@ -16,6 +17,7 @@
1617
require "dependabot/package/package_release"
1718
require "dependabot/registry_client"
1819
require "dependabot/shared_helpers"
20+
require "dependabot/source"
1921

2022
module Dependabot
2123
module GithubActions
@@ -127,6 +129,54 @@ def latest_version_tag
127129
)
128130
end
129131

132+
sig { returns(T::Array[Dependabot::GitTagWithDetail]) }
133+
def fetch_tag_and_release_date
134+
allowed_version_tags = git_commit_checker.allowed_version_tags
135+
allowed_tag_names = Set.new(allowed_version_tags.map(&:name))
136+
137+
# Use the shared GitCommitChecker#refs_for_tag_with_detail to fetch all tags
138+
# with release dates in a single clone (instead of one clone per tag)
139+
all_refs_with_detail = git_commit_checker.refs_for_tag_with_detail
140+
141+
result = all_refs_with_detail.select do |ref|
142+
allowed_tag_names.include?(ref.tag)
143+
end
144+
145+
# Log an error if we couldn't fetch any release dates
146+
if result.empty? && allowed_version_tags.any?
147+
Dependabot.logger.error("Error fetching tag and release date: unable to fetch for allowed tags")
148+
end
149+
150+
result
151+
rescue StandardError => e
152+
Dependabot.logger.error("Error fetching tag and release date: #{e.message}")
153+
[]
154+
end
155+
156+
sig do
157+
returns(T::Array[T::Hash[Symbol, T.untyped]])
158+
end
159+
def allowed_version_tags_with_release_dates
160+
allowed_version_tags_hashes = git_commit_checker.local_tags_for_allowed_versions
161+
tag_to_release_date = T.let({}, T::Hash[String, T.nilable(String)])
162+
163+
# Build a map of tag names to release dates for quick lookup
164+
fetch_tag_and_release_date.each do |git_tag_with_detail|
165+
tag_to_release_date[git_tag_with_detail.tag] = git_tag_with_detail.release_date
166+
end
167+
168+
# Combine version info with release dates and sort by version descending
169+
result = allowed_version_tags_hashes.map do |tag_hash|
170+
tag_name = tag_hash.fetch(:tag)
171+
tag_hash.merge(
172+
release_date: tag_to_release_date[tag_name]
173+
)
174+
end
175+
176+
# Sort by version descending (newest first)
177+
result.sort_by { |tag_hash| tag_hash.fetch(:version) }.reverse
178+
end
179+
130180
private
131181

132182
sig { returns(Dependabot::GitCommitChecker) }

github_actions/lib/dependabot/github_actions/update_checker/latest_version_finder.rb

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# typed: strict
22
# frozen_string_literal: true
33

4-
require "excon"
54
require "sorbet-runtime"
65

76
require "dependabot/errors"
@@ -107,7 +106,7 @@ def latest_version_tag
107106

108107
sig { returns(T.nilable(Dependabot::GithubActions::Package::PackageDetailsFetcher)) }
109108
def package_details_fetcher
110-
@package_details_fetcher = T.let(
109+
@package_details_fetcher ||= T.let(
111110
Dependabot::GithubActions::Package::PackageDetailsFetcher
112111
.new(
113112
dependency: dependency,
@@ -158,19 +157,58 @@ def cooldown_filter(release)
158157
return release unless cooldown_options
159158

160159
Dependabot.logger.info("Initializing cooldown filter")
161-
release_date = commit_metadata_details
162160

163-
unless release_date
164-
Dependabot.logger.info("No release date found, skipping cooldown filtering")
165-
return release
166-
end
161+
# If the proposed release is a commit SHA (String), check its date against cooldown
162+
if release.is_a?(String)
163+
Dependabot.logger.info("Checking cooldown for commit SHA: #{release}")
164+
return release unless check_if_version_in_cooldown_period?(commit_metadata_details)
167165

168-
if release_in_cooldown_period?(Time.parse(release_date))
169-
Dependabot.logger.info("Filtered out (cooldown) #{dependency.name}, #{release}")
166+
# Proposed SHA is in cooldown; for a SHA-based proposal, return nil (don't fall back to tags)
167+
Dependabot.logger.info("Proposed commit SHA is in cooldown, returning nil")
170168
return nil
171169
end
172170

173-
release
171+
# For version tag proposals, fetch all allowed versions with release dates (single clone)
172+
# This reuses a single GitCommitChecker instance within package_details_fetcher
173+
allowed_versions_with_dates = T.must(package_details_fetcher).allowed_version_tags_with_release_dates
174+
tags_in_cooldown = Set.new(select_version_tags_in_cooldown_period(allowed_versions_with_dates))
175+
return release if tags_in_cooldown.empty?
176+
177+
# Walk through all allowed version tags in descending order (newest first)
178+
# and return the first one NOT in cooldown
179+
allowed_versions_with_dates.each do |tag_info|
180+
tag_name = tag_info.fetch(:tag)
181+
next if tags_in_cooldown.include?(tag_name)
182+
183+
# Found a version not in cooldown, return it
184+
version = tag_info.fetch(:version)
185+
Dependabot.logger.info("Found acceptable version outside cooldown: #{version}")
186+
return version
187+
end
188+
189+
# All versions are in cooldown, return nil to fallback to current version
190+
Dependabot.logger.info("All versions are in cooldown period, returning current version")
191+
nil
192+
end
193+
194+
sig do
195+
params(
196+
tags_with_dates: T.nilable(
197+
T.any(T::Array[Dependabot::GitTagWithDetail], T::Array[T::Hash[Symbol, T.untyped]])
198+
)
199+
).returns(T::Array[String])
200+
end
201+
def select_version_tags_in_cooldown_period(tags_with_dates = nil)
202+
tags_to_check = tags_with_dates || T.must(package_details_fetcher).fetch_tag_and_release_date
203+
# Handle both GitTagWithDetail objects and hashes with release_date
204+
in_cooldown = tags_to_check.select do |tag|
205+
release_date = tag.is_a?(Hash) ? tag.fetch(:release_date, nil) : tag.release_date
206+
check_if_version_in_cooldown_period?(release_date)
207+
end
208+
in_cooldown.map { |tag| tag.is_a?(Hash) ? tag.fetch(:tag) : tag.tag }
209+
rescue StandardError => e
210+
Dependabot.logger.error("Error checking if version is in cooldown (using empty filter): #{e.message}")
211+
[]
174212
end
175213

176214
sig { returns(T.nilable(String)) }
@@ -194,30 +232,39 @@ def commit_metadata_details
194232
end
195233
end
196234
rescue StandardError => e
197-
Dependabot.logger.error("Error (github actions) while checking release date for #{dependency.name}")
198-
Dependabot.logger.error(e.message)
235+
msg = "Error (github actions) while checking release date for #{dependency.name}: #{e.message}"
236+
Dependabot.logger.warn(msg)
199237

200238
nil
201239
end,
202240
T.nilable(String)
203241
)
204242
end
205243

206-
sig { params(release_date: Time).returns(T::Boolean) }
207-
def release_in_cooldown_period?(release_date)
208-
cooldown = @cooldown_options
244+
sig { params(release_date: T.nilable(String)).returns(T::Boolean) }
245+
def check_if_version_in_cooldown_period?(release_date)
246+
return false unless release_date&.length&.positive?
247+
return false unless cooldown_options
248+
return false unless T.must(cooldown_options).included?(dependency.name)
209249

210-
return false unless T.must(cooldown).included?(dependency.name)
250+
release_time = Time.parse(T.must(release_date))
251+
cooldown_days = T.must(cooldown_options).default_days
211252

212-
days = T.must(cooldown).default_days
253+
is_in_cooldown = Dependabot::UpdateCheckers::CooldownCalculation.within_cooldown_window?(
254+
release_time,
255+
cooldown_days
256+
)
213257

258+
passed_seconds = Time.now.to_i - release_time.to_i
259+
days_since = passed_seconds / Dependabot::UpdateCheckers::CooldownCalculation::DAY_IN_SECONDS
214260
Dependabot.logger.info(
215-
"Days since release : #{(Time.now.to_i - release_date.to_i) / (24 * 60 * 60)} " \
216-
"(cooldown days #{days})"
261+
"Days since release : #{days_since} (cooldown days #{cooldown_days})"
217262
)
218263

219-
Dependabot::UpdateCheckers::CooldownCalculation
220-
.within_cooldown_window?(release_date, days)
264+
is_in_cooldown
265+
rescue StandardError => e
266+
Dependabot.logger.debug("Error parsing release date: #{e.message}")
267+
false
221268
end
222269

223270
sig { returns(String) }

github_actions/spec/dependabot/github_actions/package/package_details_fetcher_spec.rb

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,111 @@
252252
end
253253
end
254254
end
255+
256+
describe "#fetch_tag_and_release_date" do
257+
subject(:fetch_tag_and_release_date) { fetcher.fetch_tag_and_release_date }
258+
259+
let(:upload_pack_fixture) { "setup-node" }
260+
let(:git_tag_with_details) do
261+
[
262+
Dependabot::GitTagWithDetail.new(tag: "v1.0.0", release_date: "2024-01-01T00:00:00Z"),
263+
Dependabot::GitTagWithDetail.new(tag: "v2.0.0", release_date: "2024-02-01T00:00:00Z"),
264+
Dependabot::GitTagWithDetail.new(tag: "v3.0.0", release_date: "2024-03-01T00:00:00Z")
265+
]
266+
end
267+
268+
before do
269+
# Stub git_commit_checker to return mock tags with release dates
270+
mock_checker = instance_double(Dependabot::GitCommitChecker)
271+
272+
# Also stub allowed_version_tags to include all our test tags
273+
allow(mock_checker).to receive_messages(
274+
refs_for_tag_with_detail: git_tag_with_details,
275+
allowed_version_tags: git_tag_with_details.map { |tag|
276+
double(name: tag.tag)
277+
}
278+
)
279+
allow(fetcher).to receive(:git_commit_checker).and_return(mock_checker)
280+
end
281+
282+
it "returns array of GitTagWithDetail objects" do
283+
expect(fetch_tag_and_release_date).to be_an(Array)
284+
expect(fetch_tag_and_release_date.first).to be_a(Dependabot::GitTagWithDetail)
285+
end
286+
287+
it "includes tag and release_date attributes with correct values" do
288+
results = fetch_tag_and_release_date
289+
expect(results).to(
290+
all(
291+
have_attributes(
292+
tag: an_instance_of(String),
293+
release_date: an_instance_of(String)
294+
)
295+
)
296+
)
297+
# Assert specific tag values
298+
tags = results.map(&:tag).sort
299+
expect(tags).to include("v1.0.0", "v2.0.0", "v3.0.0")
300+
end
301+
302+
it "filters to only allowed version tags" do
303+
results = fetch_tag_and_release_date
304+
expect(results.map(&:tag)).not_to be_empty
305+
# All returned tags should be in the git_tag_with_details
306+
expected_tags = git_tag_with_details.map(&:tag)
307+
actual_tags = results.map(&:tag)
308+
expect(actual_tags).to match_array(expected_tags)
309+
end
310+
311+
it "preserves release dates for each tag" do
312+
results = fetch_tag_and_release_date
313+
tag_date_map = results.to_h { |item| [item.tag, item.release_date] }
314+
315+
git_tag_with_details.each do |git_tag|
316+
expect(tag_date_map[git_tag.tag]).to eq(git_tag.release_date)
317+
end
318+
end
319+
320+
context "when git_commit_checker.refs_for_tag_with_detail fails" do
321+
before do
322+
mock_checker = instance_double(Dependabot::GitCommitChecker)
323+
allow(mock_checker).to receive(:allowed_version_tags).and_return([double(name: "v1.0.0")])
324+
allow(mock_checker).to receive(:refs_for_tag_with_detail)
325+
.and_raise(StandardError, "git error")
326+
allow(fetcher).to receive(:git_commit_checker).and_return(mock_checker)
327+
end
328+
329+
it "handles error gracefully and returns empty array" do
330+
expect(fetch_tag_and_release_date).to eq([])
331+
end
332+
333+
it "logs the error" do
334+
expect(Dependabot.logger).to receive(:error).with(/Error fetching tag and release date/)
335+
fetch_tag_and_release_date
336+
end
337+
end
338+
339+
context "when no tags match allowed versions" do
340+
before do
341+
mock_checker = instance_double(Dependabot::GitCommitChecker)
342+
allow(mock_checker).to receive_messages(
343+
refs_for_tag_with_detail: [
344+
Dependabot::GitTagWithDetail.new(tag: "v999.0.0", release_date: "2099-01-01T00:00:00Z")
345+
],
346+
allowed_version_tags: [double(name: "v1.0.0")]
347+
)
348+
allow(fetcher).to receive(:git_commit_checker).and_return(mock_checker)
349+
end
350+
351+
it "returns empty array when no tags match allowed versions" do
352+
expect(fetch_tag_and_release_date).to eq([])
353+
end
354+
355+
it "logs error when no release dates found for allowed tags" do
356+
expect(Dependabot.logger).to receive(:error)
357+
.with(/Error fetching tag and release date: unable to fetch for allowed tags/)
358+
fetch_tag_and_release_date
359+
end
360+
end
361+
end
255362
end

0 commit comments

Comments
 (0)