Skip to content

Commit 2407484

Browse files
committed
[ci.rb] Add attempts variable to allow CI steps to retry
Some CI steps might flake and currently that fails the whole local CI run. To work around that, there's a new `attempts: 1` optional variable that can be set on a step to retry. By default it's 1, meaning no retries. If set to a higher number, the step will be retried up to that many times before failing. If it's not positive, there will be an ArgumentError. New test cases have been added for this feature.
1 parent d3d17c2 commit 2407484

File tree

3 files changed

+56
-2
lines changed

3 files changed

+56
-2
lines changed

activesupport/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,14 @@
1616

1717
*Gannon McGibbon*
1818

19+
* Add `attempts` parameter to `ActiveSupport::ContinuousIntegration#step` to
20+
allow retrying flaky CI steps a specified number of times before marking
21+
them as failed.
22+
```ruby
23+
step "Flaky test", "bin/rails test test/models/flaky_test.rb", attempts: 3
24+
```
25+
26+
*Harsh Deep*
27+
1928

2029
Please check [8-1-stable](https://github.com/rails/rails/blob/8-1-stable/activesupport/CHANGELOG.md) for previous changes.

activesupport/lib/active_support/continuous_integration.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,17 @@ def initialize
7272
#
7373
# step "Setup", "bin/setup"
7474
# step "Single test", "bin/rails", "test", "--name", "test_that_is_one"
75-
def step(title, *command)
75+
# step "Flaky test", "bin/rails test test/models/flaky_test.rb", attempts: 3
76+
def step(title, *command, attempts: 1)
77+
raise ArgumentError, "attempts must be a positive integer" unless attempts.positive?
78+
7679
heading title, command.join(" "), type: :title
77-
report(title) { results << [ system(*command), title ] }
80+
81+
successful = retry_until_success(attempts) do
82+
system(*command)
83+
end
84+
85+
report(title) { results << [ successful, title ] }
7886
end
7987

8088
# Returns true if all steps were successful.
@@ -159,5 +167,13 @@ def timing
159167
def colorize(text, type)
160168
"#{COLORS.fetch(type)}#{text}\033[0m"
161169
end
170+
171+
def retry_until_success(attempts)
172+
attempts.times do |attempt_number|
173+
return true if yield
174+
echo "Attempt #{attempt_number + 1} failed. Retrying...", type: :error
175+
end
176+
false
177+
end
162178
end
163179
end

activesupport/test/continuous_integration_test.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,35 @@ class ContinuousIntegrationTest < ActiveSupport::TestCase
1818
assert_not @CI.success?
1919
end
2020

21+
test "step retries on failures" do
22+
output = capture_io {
23+
@CI.step "Failed!", "false", attempts: 2
24+
}.to_s
25+
26+
assert_not @CI.success?
27+
assert_match(/Attempt 1 failed. Retrying.../, output)
28+
assert_match(/Attempt 2 failed. Retrying.../, output)
29+
end
30+
31+
test "successful step does not retry" do
32+
output = capture_io {
33+
@CI.step "Success!", "true", attempts: 2
34+
}.to_s
35+
36+
assert @CI.success?
37+
assert_no_match(/Attempt \d+ failed. Retrying.../, output)
38+
end
39+
40+
test "step raises error for invalid attempts" do
41+
assert_raises ArgumentError, match: "attempts must be a positive integer" do
42+
@CI.step "Invalid attempts", "true", attempts: 0
43+
end
44+
45+
assert_raises ArgumentError, match: "attempts must be a positive integer" do
46+
@CI.step "Invalid attempts", "true", attempts: -1
47+
end
48+
end
49+
2150
test "report with only successful steps combined gives success" do
2251
output = capture_io do
2352
@CI.report("CI") do

0 commit comments

Comments
 (0)