Class: ActiveSupport::ContinuousIntegration

Inherits:
Object
  • Object
show all
Defined in:
activesupport/lib/active_support/continuous_integration.rb

Overview

Provides a DSL for declaring a continuous integration workflow that can be run either locally or in the cloud. Each step is timed, reports success/error, and is aggregated into a collective report that reports total runtime, as well as whether the entire run was successful or not.

Example:

ActiveSupport::ContinuousIntegration.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby", "bin/rubocop"
  step "Security: Gem audit", "bin/bundler-audit"
  step "Tests: Rails", "bin/rails test test:system"

  if success?
    step "Signoff: Ready for merge and deploy", "gh signoff"
  else
    failure "Skipping signoff; CI failed.", "Fix the issues and try again."
  end
end

Starting with Rails 8.1, a default bin/ci and config/ci.rb file are created to provide out-of-the-box CI.

Constant Summary collapse

COLORS =
{
  banner: "\033[1;32m",   # Green
  title: "\033[1;35m",    # Purple
  subtitle: "\033[1;90m", # Medium Gray
  error: "\033[1;31m",    # Red
  success: "\033[1;32m"   # Green
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeContinuousIntegration

Returns a new instance of ContinuousIntegration.



66
67
68
# File 'activesupport/lib/active_support/continuous_integration.rb', line 66

def initialize
  @results = []
end

Instance Attribute Details

#resultsObject (readonly)

Returns the value of attribute results.



33
34
35
# File 'activesupport/lib/active_support/continuous_integration.rb', line 33

def results
  @results
end

Class Method Details

.run(title = "Continuous Integration", subtitle = "Running tests, style checks, and security audits", &block) ⇒ Object

Perform a CI run. Execute each step, show their results and runtime, and exit with a non-zero status if there are any failures.

Pass an optional title, subtitle, and a block that declares the steps to be executed.

Sets the CI environment variable to “true” to allow for conditional behavior in the app, like enabling eager loading and disabling logging.

A ‘fail fast’ option can be passed as a CLI argument (-f or –fail-fast). This exits with a non-zero status directly after a step fails.

Example:

ActiveSupport::ContinuousIntegration.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby", "bin/rubocop"
  step "Security: Gem audit", "bin/bundler-audit"
  step "Tests: Rails", "bin/rails test test:system"

  if success?
    step "Signoff: Ready for merge and deploy", "gh signoff"
  else
    failure "Skipping signoff; CI failed.", "Fix the issues and try again."
  end
end


57
58
59
60
61
62
63
64
# File 'activesupport/lib/active_support/continuous_integration.rb', line 57

def self.run(title = "Continuous Integration", subtitle = "Running tests, style checks, and security audits", &block)
  new.tap do |ci|
    ENV["CI"] = "true"
    ci.heading title, subtitle, padding: false
    ci.report(title, &block)
    abort unless ci.success?
  end
end

Instance Method Details

#echo(text, type:) ⇒ Object

Echo text to the terminal in the color corresponding to the type of the text.

Examples:

echo "This is going to be green!", type: :success
echo "This is going to be red!", type: :error

See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.



113
114
115
# File 'activesupport/lib/active_support/continuous_integration.rb', line 113

def echo(text, type:)
  puts colorize(text, type)
end

#fail_fast?Boolean

:nodoc:

Returns:

  • (Boolean)


156
157
158
# File 'activesupport/lib/active_support/continuous_integration.rb', line 156

def fail_fast?
  ARGV.include?("-f") || ARGV.include?("--fail-fast")
end

#failure(title, subtitle = nil) ⇒ Object

Display an error heading with the title and optional subtitle to reflect that the run failed.



88
89
90
# File 'activesupport/lib/active_support/continuous_integration.rb', line 88

def failure(title, subtitle = nil)
  heading title, subtitle, type: :error
end

#failuresObject

:nodoc:



146
147
148
# File 'activesupport/lib/active_support/continuous_integration.rb', line 146

def failures
  results.reject(&:first)
end

#heading(heading, subtitle = nil, type: :banner, padding: true) ⇒ Object

Display a colorized heading followed by an optional subtitle.

Examples:

heading "Smoke Testing", "End-to-end tests verifying key functionality", padding: false
heading "Skipping video encoding tests", "Install FFmpeg to run these tests", type: :error

See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.



100
101
102
103
# File 'activesupport/lib/active_support/continuous_integration.rb', line 100

def heading(heading, subtitle = nil, type: :banner, padding: true)
  echo "#{padding ? "\n\n" : ""}#{heading}", type: type
  echo "#{subtitle}#{padding ? "\n" : ""}", type: :subtitle if subtitle
end

#multiple_results?Boolean

:nodoc:

Returns:

  • (Boolean)


151
152
153
# File 'activesupport/lib/active_support/continuous_integration.rb', line 151

def multiple_results?
  results.size > 1
end

#report(title, &block) ⇒ Object

:nodoc:



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'activesupport/lib/active_support/continuous_integration.rb', line 118

def report(title, &block)
  Signal.trap("INT") { abort colorize("\n❌ #{title} interrupted", :error) }

  ci = self.class.new
  elapsed = timing { ci.instance_eval(&block) }

  if ci.success?
    echo "\n✅ #{title} passed in #{elapsed}", type: :success
  else
    echo "\n❌ #{title} failed in #{elapsed}", type: :error

    abort if ci.fail_fast?

    if ci.multiple_results?
      ci.failures.each do |success, title|
        unless success
          echo "   ↳ #{title} failed", type: :error
        end
      end
    end
  end

  results.concat ci.results
ensure
  Signal.trap("INT", "-")
end

#step(title, *command) ⇒ Object

Declare a step with a title and a command. The command can either be given as a single string or as multiple strings that will be passed to system as individual arguments (and therefore correctly escaped for paths etc).

Examples:

step "Setup", "bin/setup"
step "Single test", "bin/rails", "test", "--name", "test_that_is_one"


77
78
79
80
# File 'activesupport/lib/active_support/continuous_integration.rb', line 77

def step(title, *command)
  heading title, command.join(" "), type: :title
  report(title) { results << [ system(*command), title ] }
end

#success?Boolean

Returns true if all steps were successful.

Returns:

  • (Boolean)


83
84
85
# File 'activesupport/lib/active_support/continuous_integration.rb', line 83

def success?
  results.map(&:first).all?
end