@@ -27,7 +27,8 @@ class ContinuousIntegration
2727 title : "\033 [1;35m" , # Purple
2828 subtitle : "\033 [1;90m" , # Medium Gray
2929 error : "\033 [1;31m" , # Red
30- success : "\033 [1;32m" # Green
30+ success : "\033 [1;32m" , # Green
31+ progress : "\033 [1;36m" # Cyan
3132 }
3233
3334 attr_reader :results
@@ -55,12 +56,15 @@ class ContinuousIntegration
5556 # end
5657 # end
5758 def self . run ( title = "Continuous Integration" , subtitle = "Running tests, style checks, and security audits" , &block )
58- new . tap do |ci |
59- ENV [ "CI" ] = "true"
60- ci . heading title , subtitle , padding : false
61- ci . report ( title , &block )
62- abort unless ci . success?
63- end
59+ ENV [ "CI" ] = "true"
60+ new . run ( title , subtitle , &block )
61+ end
62+
63+ def run ( title , subtitle , &block )
64+ heading title , subtitle , padding : false
65+ success , seconds = execute ( title , &block )
66+ result_line ( title , success , seconds )
67+ abort unless success?
6468 end
6569
6670 def initialize
@@ -75,8 +79,36 @@ def initialize
7579 # step "Setup", "bin/setup"
7680 # step "Single test", "bin/rails", "test", "--name", "test_that_is_one"
7781 def step ( title , *command )
78- heading title , command . join ( " " ) , type : :title
79- report ( title ) { results << [ system ( *command ) , title ] }
82+ Signal . trap ( "INT" ) { abort colorize ( "\n ❌ #{ title } interrupted" , :error ) }
83+ success = report_step ( title , command ) do
84+ started = Time . now . to_f
85+ [ system ( *command ) , Time . now . to_f - started ]
86+ end
87+ abort if fail_fast? && !success
88+ end
89+
90+ # Declare a group of steps that can be run in parallel. Steps within the group are collected first,
91+ # then executed either concurrently (when +parallel+ > 1) or sequentially (when +parallel+ is 1).
92+ #
93+ # When running in parallel, each step's output is captured to avoid interleaving, and a progress
94+ # display shows which steps are currently running.
95+ #
96+ # Sub-groups within a parallel group occupy a single parallel slot and run their steps sequentially.
97+ #
98+ # Examples:
99+ #
100+ # group "Checks", parallel: 3 do
101+ # step "Style: Ruby", "bin/rubocop"
102+ # step "Security: Brakeman", "bin/brakeman --quiet"
103+ # step "Security: Gem audit", "bin/bundler-audit"
104+ # end
105+ #
106+ # group "Tests" do
107+ # step "Unit tests", "bin/rails test"
108+ # step "System tests", "bin/rails test:system"
109+ # end
110+ def group ( name , parallel : 1 , &block )
111+ Group . new ( self , name , parallel : parallel , &block ) . run
80112 end
81113
82114 # Returns true if all steps were successful.
@@ -95,8 +127,6 @@ def failure(title, subtitle = nil)
95127 #
96128 # heading "Smoke Testing", "End-to-end tests verifying key functionality", padding: false
97129 # heading "Skipping video encoding tests", "Install FFmpeg to run these tests", type: :error
98- #
99- # See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.
100130 def heading ( heading , subtitle = nil , type : :banner , padding : true )
101131 echo "#{ padding ? "\n \n " : "" } #{ heading } " , type : type
102132 echo "#{ subtitle } #{ padding ? "\n " : "" } " , type : :subtitle if subtitle
@@ -108,48 +138,22 @@ def heading(heading, subtitle = nil, type: :banner, padding: true)
108138 #
109139 # echo "This is going to be green!", type: :success
110140 # echo "This is going to be red!", type: :error
111- #
112- # See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.
113141 def echo ( text , type :)
114142 puts colorize ( text , type )
115143 end
116144
117145 # :nodoc:
118- def report ( title , &block )
119- Signal . trap ( "INT" ) { abort colorize ( "\n ❌ #{ title } interrupted" , :error ) }
120-
121- ci = self . class . new
122- elapsed = timing { ci . instance_eval ( &block ) }
123-
124- if ci . success?
125- echo "\n ✅ #{ title } passed in #{ elapsed } " , type : :success
126- else
127- echo "\n ❌ #{ title } failed in #{ elapsed } " , type : :error
128-
129- abort if ci . fail_fast?
130-
131- if ci . multiple_results?
132- ci . failures . each do |success , title |
133- unless success
134- echo " ↳ #{ title } failed" , type : :error
135- end
136- end
137- end
138- end
139-
140- results . concat ci . results
141- ensure
142- Signal . trap ( "INT" , "-" )
143- end
144-
145- # :nodoc:
146- def failures
147- results . reject ( &:first )
146+ def report_step ( title , command )
147+ heading title , command . join ( " " ) , type : :title
148+ success , seconds = yield
149+ result_line ( title , success , seconds )
150+ results << [ success , title ]
151+ success
148152 end
149153
150154 # :nodoc:
151- def multiple_results?
152- results . size > 1
155+ def colorize ( text , type )
156+ " #{ COLORS . fetch ( type ) } #{ text } \033 [0m"
153157 end
154158
155159 # :nodoc:
@@ -158,15 +162,54 @@ def fail_fast?
158162 end
159163
160164 private
165+ def failures
166+ results . reject ( &:first )
167+ end
168+
169+ def multiple_results?
170+ results . size > 1
171+ end
172+
173+ def execute ( title , &block )
174+ Signal . trap ( "INT" ) { abort colorize ( "\n ❌ #{ title } interrupted" , :error ) }
175+
176+ seconds = timing { instance_eval ( &block ) }
177+
178+ unless success?
179+ if multiple_results?
180+ failures . each do |success , title |
181+ unless success
182+ echo " ↳ #{ title } failed" , type : :error
183+ end
184+ end
185+ end
186+ end
187+
188+ [ success? , seconds ]
189+ ensure
190+ Signal . trap ( "INT" , "-" )
191+ end
192+
193+ def result_line ( title , success , seconds )
194+ elapsed = format_elapsed ( seconds )
195+ if success
196+ echo "\n ✅ #{ title } passed in #{ elapsed } " , type : :success
197+ else
198+ echo "\n ❌ #{ title } failed in #{ elapsed } " , type : :error
199+ end
200+ end
201+
202+ def format_elapsed ( seconds )
203+ min , sec = seconds . divmod ( 60 )
204+ "#{ "#{ min . to_i } m" if min > 0 } %.2fs" % sec
205+ end
206+
161207 def timing
162208 started_at = Time . now . to_f
163209 yield
164- min , sec = ( Time . now . to_f - started_at ) . divmod ( 60 )
165- "#{ "#{ min } m" if min > 0 } %.2fs" % sec
166- end
167-
168- def colorize ( text , type )
169- "#{ COLORS . fetch ( type ) } #{ text } \033 [0m"
210+ Time . now . to_f - started_at
170211 end
171212 end
172213end
214+
215+ require_relative "continuous_integration/group"
0 commit comments