Skip to content

Commit 9befd4a

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 9befd4a

File tree

3 files changed

+57
-2
lines changed

3 files changed

+57
-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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ 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+
assert_no_match(/Attempt 3 failed. Retrying.../, output)
30+
end
31+
32+
test "successful step does not retry" do
33+
output = capture_io {
34+
@CI.step "Success!", "true", attempts: 2
35+
}.to_s
36+
37+
assert @CI.success?
38+
assert_no_match(/Attempt \d+ failed. Retrying.../, output)
39+
end
40+
41+
test "step raises error for invalid attempts" do
42+
assert_raises ArgumentError, match: "attempts must be a positive integer" do
43+
@CI.step "Invalid attempts", "true", attempts: 0
44+
end
45+
46+
assert_raises ArgumentError, match: "attempts must be a positive integer" do
47+
@CI.step "Invalid attempts", "true", attempts: -1
48+
end
49+
end
50+
2151
test "report with only successful steps combined gives success" do
2252
output = capture_io do
2353
@CI.report("CI") do

0 commit comments

Comments
 (0)