Module: Toys::CI::Mixin

Includes:
Mixin
Defined in:
lib/toys/ci/mixin.rb

Overview

A mixin that provides methods useful for implementing CI tools.

This mixin is a lower-level mechanism that depends on you to write your own run method and define any needed flags. For a more batteries-included experience, consider Template, which does that work for you.

To implement a CI tool using this mixin, you will:

This mixin adds various public and private methods, and several instance variables to the tool. All added method and instance variable names begin with toys_ci_, so avoid that prefix for any other methods and variables you are using in your tool.

Examples:


# Define the "test" tool
expand :minitest, bundler: true

# Define the "rubocop" tool
expand :rubocop, bundler: true

# Define a "ci" tool that runs both the above, controlled by
# flags "--tests", "--rubocop", and/or "--all".
tool "ci" do
  # Activate the toys-ci gem and pull in the mixin
  load_gem "toys-ci"
  include Toys::CI::Mixin

  flag :tests, desc: "Run tests"
  flag :rubocop, desc: "Run rubocop"
  flag :all, desc: "Run all CI tasks"

  def run
    toys_ci_init
    toys_ci_tool_job("Tests", ["test"]) if tests || all
    toys_ci_tool_job("Rubocop", ["rubocop"]) if rubocop || all
    toys_ci_report_results
  end
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#toys_ci_failed_jobsArray<String> (readonly)

Returns The names of the failed jobs so far.

Returns:

  • (Array<String>)

    The names of the failed jobs so far



253
254
255
# File 'lib/toys/ci/mixin.rb', line 253

def toys_ci_failed_jobs
  @toys_ci_failed_jobs
end

#toys_ci_skipped_jobsArray<String> (readonly)

Returns The names of the skipped jobs so far.

Returns:

  • (Array<String>)

    The names of the skipped jobs so far



263
264
265
# File 'lib/toys/ci/mixin.rb', line 263

def toys_ci_skipped_jobs
  @toys_ci_skipped_jobs
end

#toys_ci_successful_jobsArray<String> (readonly)

Returns The names of the successful jobs so far.

Returns:

  • (Array<String>)

    The names of the successful jobs so far



258
259
260
# File 'lib/toys/ci/mixin.rb', line 258

def toys_ci_successful_jobs
  @toys_ci_successful_jobs
end

Instance Method Details

#toys_ci_cmd_job(name, cmd, trigger_paths: nil, env: nil, chdir: nil) ⇒ :success, ...

Run a CI job implemented by an external process, and record the results.

Parameters:

  • name (String)

    A user-visible name for the job. Required.

  • cmd (Array<String>)

    The command to run. Required.

  • trigger_paths (Array<String>, String, nil) (defaults to: nil)

    An array of file or directory paths, relative to the repo root, that must have changes in order to trigger the job. If not specified, the job is always triggered.

  • env (Hash{String=>String}) (defaults to: nil)

    Environment variables to set during the run. Optional.

  • chdir (String) (defaults to: nil)

    The working directory for the run. Optional.

Returns:

  • (:success)

    If the job succeeded

  • (:failure)

    If the job failed

  • (:skipped)

    If the job was skipped because it did not match the trigger paths



166
167
168
169
170
171
172
173
# File 'lib/toys/ci/mixin.rb', line 166

def toys_ci_cmd_job(name, cmd, trigger_paths: nil, env: nil, chdir: nil)
  toys_ci_job(name, trigger_paths: trigger_paths) do
    opts = {name: name}
    opts[:env] = env if env
    opts[:chdir] = chdir if chdir
    exec(cmd, **opts).success?
  end
end

#toys_ci_github_event_base_shaString?

Look for environment variables set by a GitHub workflow, and attempt to extract a suitable change base. Returns a git SHA, or nil if one could not be obtained from the current environment.

This may read the GITHUB_EVENT_NAME and GITHUB_EVENT_PATH environment variables, and may also read the event payload file if found. However, the exact logic is not specified.

The result can be passed to the :limit_by_changes_since argument of #toys_ci_init.

Returns:

  • (String)

    The git SHA for the change base

  • (nil)

    if no change base can be determined



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/toys/ci/mixin.rb', line 98

def toys_ci_github_event_base_sha
  event_path = ::ENV["GITHUB_EVENT_PATH"].to_s
  if event_path.empty?
    logger.info("GITHUB_EVENT_PATH is empty or unset; cannot determine event payload file")
    return nil
  end
  event_payload = toys_ci_read_event_payload_file(event_path)
  return nil unless event_payload

  event_name = ::ENV["GITHUB_EVENT_NAME"]
  case event_name
  when "push"
    logger.info("Getting change base from push event")
    event_payload["before"]
  when "pull_request"
    logger.info("Getting change base from pull_request event")
    event_payload.dig("pull_request", "base", "sha")
  else
    logger.info("Did not find a change base from event #{event_name.inspect}")
    nil
  end
end

#toys_ci_init(fail_fast: false, limit_by_changes_since: nil) ⇒ self

Initialize the CI tool. This must be called first before any other toys_ci_ methods.

Parameters:

  • fail_fast (boolean) (defaults to: false)

    If true, CI will terminate once any job ends in failure. Otherwise, all jobs will be run.

  • limit_by_changes_since (String, nil) (defaults to: nil)

    If set to a git ref, finds all the changed files since that ref, and skips CI jobs that declare trigger paths that do not match. Optional. By default, no jobs are skipped for this reason.

Returns:

  • (self)


74
75
76
77
78
79
80
81
# File 'lib/toys/ci/mixin.rb', line 74

def toys_ci_init(fail_fast: false, limit_by_changes_since: nil)
  @toys_ci_fail_fast = fail_fast
  @toys_ci_changed_paths = toys_ci_find_changes_since(limit_by_changes_since)
  @toys_ci_successful_jobs = []
  @toys_ci_failed_jobs = []
  @toys_ci_skipped_jobs = []
  self
end

#toys_ci_job(name, trigger_paths: nil, &block) ⇒ :success, ...

Run a CI job implemented by a block, and record the results.

Parameters:

  • name (String)

    A user-visible name for the job. Required.

  • trigger_paths (Array<String>, String, nil) (defaults to: nil)

    An array of file or directory paths, relative to the repo root, that must have changes in order to trigger the job. If not specified, the job is always triggered.

  • block (Proc)

    The block to run. It will be run with self set to the tool context, and should return true or false indicating success or failure.

Returns:

  • (:success)

    If the job succeeded

  • (:failure)

    If the job failed

  • (:skipped)

    If the job was skipped because it did not match the trigger paths



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/toys/ci/mixin.rb', line 192

def toys_ci_job(name, trigger_paths: nil, &block)
  unless defined?(@toys_ci_successful_jobs)
    raise ::Toys::ToolDefinitionError, "You must call toys_ci_init before running a job"
  end
  return :skipped unless toys_ci_check_trigger_paths(trigger_paths, name)
  puts("**** RUNNING: #{name}", :cyan, :bold)
  result =
    begin
      instance_exec(&block)
    rescue ::StandardError => e
      trace = e.backtrace
      write("#{trace.first}: ")
      puts("#{e.message} (#{e.class})", :bold)
      Array(trace[1..]).each { |line| puts "        from #{line}" }
      false
    end
  toys_ci_job_result(name, result)
end

#toys_ci_report_results(exit: true) ⇒ Integer

Print out a final report of the results, including a summary of the failed jobs. By default, this will also exit and never return. You can instead get the exit value by passing exit: false.

Parameters:

  • exit (boolean) (defaults to: true)

    Whether to exit. Default is true.

Returns:

  • (Integer)

    The exit value



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/toys/ci/mixin.rb', line 220

def toys_ci_report_results(exit: true) # rubocop:disable Metrics/MethodLength
  unless defined?(@toys_ci_successful_jobs)
    raise ::Toys::ToolDefinitionError, "You must call toys_ci_init before reporting job results"
  end
  success_count = @toys_ci_successful_jobs.size
  failure_count = @toys_ci_failed_jobs.size
  skipped_count = @toys_ci_skipped_jobs.size
  total_job_count = success_count + failure_count + skipped_count
  result =
    if total_job_count.zero?
      puts("**** CI: NO JOBS REQUESTED", :red, :bold)
      puts("Try passing --help to see how to activate CI jobs.")
      2
    elsif failure_count.positive?
      puts("**** CI: SKIPPED #{skipped_count} OF #{total_job_count} JOBS", :bold) unless skipped_count.zero?
      puts("**** CI: FAILED #{failure_count} OF #{success_count + failure_count} RUNNABLE JOBS:", :red, :bold)
      @toys_ci_failed_jobs.each { |name| puts(name, :red) }
      1
    elsif success_count.positive?
      puts("**** CI: SKIPPED #{skipped_count} OF #{total_job_count} JOBS", :bold) unless skipped_count.zero?
      puts("**** CI: ALL #{success_count} RUNNABLE JOBS SUCCEEDED", :green, :bold)
      0
    else
      puts("**** CI: ALL #{skipped_count} JOBS SKIPPED", :yellow, :bold)
      0
    end
  self.exit(result) if exit
  result
end

#toys_ci_tool_job(name, tool, trigger_paths: nil, env: nil, chdir: nil) ⇒ :success, ...

Run a CI job implemented by a tool, and record the results.

Parameters:

  • name (String)

    A user-visible name for the job. Required.

  • tool (Array<String>)

    The Toys tool to run. Required.

  • trigger_paths (Array<String>, String, nil) (defaults to: nil)

    An array of file or directory paths, relative to the repo root, that must have changes in order to trigger the job. If not specified, the job is always triggered.

  • env (Hash{String=>String}) (defaults to: nil)

    Environment variables to set during the run. Optional.

  • chdir (String) (defaults to: nil)

    The working directory for the run. Optional.

Returns:

  • (:success)

    If the job succeeded

  • (:failure)

    If the job failed

  • (:skipped)

    If the job was skipped because it did not match the trigger paths



139
140
141
142
143
144
145
146
# File 'lib/toys/ci/mixin.rb', line 139

def toys_ci_tool_job(name, tool, trigger_paths: nil, env: nil, chdir: nil)
  toys_ci_job(name, trigger_paths: trigger_paths) do
    opts = {name: name}
    opts[:env] = env if env
    opts[:chdir] = chdir if chdir
    exec_separate_tool(tool, **opts).success?
  end
end