home blog about

How to rerun your latest failing CircleCI specs locally

Context

You push your latest feature branch to Github, CircleCI runs your specs and some of them fail.

Typically, you could open the build in CircleCI and inspect the failing specs to determine what went wrong.

What if you could rerun the specs locally with a single command, to inspect the failures from the comfort of your favorite IDE?

bin/rails my:ci_rerun RAILS_ENV=test to the rescue!

The implementation

Notes

# lib/tasks/my.rake

namespace :my do
  task ci_rerun: :environment do
    pipeline_id = latest_pipeline_by_branch['id']
    workflow_id = fetch_workflow_id(pipeline_id)
    job_number = fetch_job_number(workflow_id)
    failed_tests = extract_failed_tests(fetch_job_details(job_number))
    run_failed_tests(failed_tests)
  end

  def build_branch(pipeline_id)
    api_call("pipeline/#{pipeline_id}")['vcs']['branch']
  end

  def api_call(relative_url)
    uri = URI("https://circleci.com/api/v2/#{relative_url}")
    req = Net::HTTP::Get.new(uri)
    req['Circle-Token'] = ENV.fetch('DEVELOPER_CIRCLE_CI_API_TOKEN')
    result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
      http.request(req)
    end
    raise "Failed to fetch data for #{relative_url}" unless result.is_a?(Net::HTTPSuccess)

    JSON.parse(result.body)
  end

  def fetch_job_number(workflow_id)
    job = api_call("workflow/#{workflow_id}/job")['items'].detect do |job|
      job['name'] == 'test'
    end
    raise 'No job found' unless job

    puts "Job Number: #{job['job_number']}"
    job['job_number']
  end

  def extract_failed_tests(job_details)
    return [] unless job_details['items']

    job_details['items']
      .select { |item| item['result'] == 'failure' }
      .map { |item| { file: item['file'], name: item['name'] } }
      .uniq
  end

  def fetch_job_details(job_number)
    api_call("project/#{project_slug}/#{job_number}/tests")
  end

  def latest_pipeline_by_branch
    pipeline = api_call("project/#{project_slug}/pipeline?branch=#{local_branch_name}")['items'].first
    raise "No pipeline found for branch #{local_branch_name}" if pipeline.blank?

    puts "Pipeline ID: #{pipeline['id']}"
    pipeline
  end

  def local_branch_name
    `git rev-parse --abbrev-ref HEAD`.strip
  end

  def project_slug
    'github/Organization/Project' # Update this to match your project
  end

  def run_failed_tests(failed_tests)
    return if failed_tests.empty?

    test_commands = failed_tests.map do |test|
      "#{test[:file]} --example \"#{test[:name]}\""
    end.join(' ')
    system("rspec #{test_commands}")
  end

  def fetch_workflow_id(pipeline_id)
    workflow = api_call("pipeline/#{pipeline_id}/workflow")['items'].first
    puts "Workflow ID: #{workflow['id']}"
    workflow['id']
  end
end


How to use it

Make sure you set the following environment variable:

Then, run the following command:

bin/rails my:ci_rerun RAILS_ENV=test

The script will fetch the latest pipeline_id for your current branch, find the failed job, extract the failed tests, and rerun them locally.