class Codecov::Uploader

Constants

RECOGNIZED_CIS

CIs

Public Class Methods

build_params(ci) click to toggle source
# File lib/codecov/uploader.rb, line 130
def self.build_params(ci)
  puts [red('x>'), 'No token specified or token is empty'].join(' ') if
    ENV['CODECOV_TOKEN'].nil? || ENV['CODECOV_TOKEN'].empty?

  params = {
    'token' => ENV['CODECOV_TOKEN'],
    'flags' => ENV['CODECOV_FLAG'] || ENV['CODECOV_FLAGS'],
    'package' => "ruby-#{::Codecov::VERSION}"
  }

  case ci
  when APPVEYOR
    # http://www.appveyor.com/docs/environment-variables
    params[:service] = 'appveyor'
    params[:branch] = ENV['APPVEYOR_REPO_BRANCH']
    params[:build] = ENV['APPVEYOR_JOB_ID']
    params[:pr] = ENV['APPVEYOR_PULL_REQUEST_NUMBER']
    params[:job] = ENV['APPVEYOR_ACCOUNT_NAME'] + '/' + ENV['APPVEYOR_PROJECT_SLUG'] + '/' + ENV['APPVEYOR_BUILD_VERSION']
    params[:slug] = ENV['APPVEYOR_REPO_NAME']
    params[:commit] = ENV['APPVEYOR_REPO_COMMIT']
  when AZUREPIPELINES
    params[:service] = 'azure_pipelines'
    params[:branch] = ENV['BUILD_SOURCEBRANCH']
    params[:pull_request] = ENV['SYSTEM_PULLREQUEST_PULLREQUESTNUMBER']
    params[:job] = ENV['SYSTEM_JOBID']
    params[:build] = ENV['BUILD_BUILDID']
    params[:build_url] = "#{ENV['SYSTEM_TEAMFOUNDATIONSERVERURI']}/#{ENV['SYSTEM_TEAMPROJECT']}/_build/results?buildId=#{ENV['BUILD_BUILDID']}"
    params[:commit] = ENV['BUILD_SOURCEVERSION']
    params[:slug] = ENV['BUILD_REPOSITORY_ID']
  when BITBUCKET
    # https://confluence.atlassian.com/bitbucket/variables-in-pipelines-794502608.html
    params[:service] = 'bitbucket'
    params[:branch] = ENV['BITBUCKET_BRANCH']
    # BITBUCKET_COMMIT does not always provide full commit sha due to a bug https://jira.atlassian.com/browse/BCLOUD-19393#
    params[:commit] = (ENV['BITBUCKET_COMMIT'].length < 40 ? nil : ENV['BITBUCKET_COMMIT'])
    params[:build] = ENV['BITBUCKET_BUILD_NUMBER']
  when BITRISE
    # http://devcenter.bitrise.io/faq/available-environment-variables/
    params[:service] = 'bitrise'
    params[:branch] = ENV['BITRISE_GIT_BRANCH']
    params[:pr] = ENV['BITRISE_PULL_REQUEST']
    params[:build] = ENV['BITRISE_BUILD_NUMBER']
    params[:build_url] = ENV['BITRISE_BUILD_URL']
    params[:commit] = ENV['BITRISE_GIT_COMMIT']
    params[:slug] = ENV['BITRISEIO_GIT_REPOSITORY_OWNER'] + '/' + ENV['BITRISEIO_GIT_REPOSITORY_SLUG']
  when BUILDKITE
    # https://buildkite.com/docs/guides/environment-variables
    params[:service] = 'buildkite'
    params[:branch] = ENV['BUILDKITE_BRANCH']
    params[:build] = ENV['BUILDKITE_BUILD_NUMBER']
    params[:job] = ENV['BUILDKITE_JOB_ID']
    params[:build_url] = ENV['BUILDKITE_BUILD_URL']
    params[:slug] = ENV['BUILDKITE_PROJECT_SLUG']
    params[:commit] = ENV['BUILDKITE_COMMIT']
  when CIRCLE
    # https://circleci.com/docs/environment-variables
    params[:service] = 'circleci'
    params[:build] = ENV['CIRCLE_BUILD_NUM']
    params[:job] = ENV['CIRCLE_NODE_INDEX']
    params[:slug] = if !ENV['CIRCLE_PROJECT_REPONAME'].nil?
                      ENV['CIRCLE_PROJECT_USERNAME'] + '/' + ENV['CIRCLE_PROJECT_REPONAME']
                    else
                      ENV['CIRCLE_REPOSITORY_URL'].gsub(/^.*:/, '').gsub(/\.git$/, '')
                    end
    params[:pr] = ENV['CIRCLE_PR_NUMBER']
    params[:branch] = ENV['CIRCLE_BRANCH']
    params[:commit] = ENV['CIRCLE_SHA1']
  when CIRRUS
    # https://cirrus-ci.org/guide/writing-tasks/#environment-variables
    params[:branch] = ENV['CIRRUS_BRANCH']
    params[:build] = ENV['CIRRUS_BUILD_ID']
    params[:build_url] = "https://cirrus-ci.com/tasks/#{ENV['CIRRUS_TASK_ID']}"
    params[:commit] = ENV['CIRRUS_CHANGE_IN_REPO']
    params[:job] = ENV['CIRRUS_TASK_NAME']
    params[:pr] = ENV['CIRRUS_PR']
    params[:service] = 'cirrus-ci'
    params[:slug] = ENV['CIRRUS_REPO_FULL_NAME']
  when CODEBUILD
    # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
    # To use CodePipeline as CodeBuild source which sets no branch and slug variable:
    #
    # 1. Set up CodeStarSourceConnection as source action provider
    #    https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodestarConnectionSource.html
    # 2. Add a Namespace to your source action. Example: "CodeStar".
    #    https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-variables.html#reference-variables-concepts-namespaces
    # 3. Add these environment variables to your CodeBuild action:
    #   - CODESTAR_BRANCH_NAME: #{CodeStar.BranchName}
    #   - CODESTAR_FULL_REPOSITORY_NAME: #{CodeStar.FullRepositoryName} (optional)
    #     https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodeBuild.html#action-reference-CodeBuild-config
    #
    # PRs are not supported with CodePipeline.
    params[:service] = 'codebuild'
    params[:branch] = ENV['CODEBUILD_WEBHOOK_HEAD_REF']&.split('/')&.[](2) || ENV['CODESTAR_BRANCH_NAME']
    params[:build] = ENV['CODEBUILD_BUILD_ID']
    params[:commit] = ENV['CODEBUILD_RESOLVED_SOURCE_VERSION']
    params[:job] = ENV['CODEBUILD_BUILD_ID']
    params[:slug] = ENV['CODEBUILD_SOURCE_REPO_URL']&.match(/.*github.com\/(?<slug>.*).git/)&.[]('slug') || ENV['CODESTAR_FULL_REPOSITORY_NAME']
    params[:pr] = if ENV['CODEBUILD_SOURCE_VERSION'] && !(ENV['CODEBUILD_INITIATOR'] =~ /codepipeline/)
                    matched = ENV['CODEBUILD_SOURCE_VERSION'].match(%r{pr/(?<pr>.*)})
                    matched.nil? ? ENV['CODEBUILD_SOURCE_VERSION'] : matched['pr']
                  end
    params[:build_url] = ENV['CODEBUILD_BUILD_URL']
  when CODESHIP
    # https://www.codeship.io/documentation/continuous-integration/set-environment-variables/
    params[:service] = 'codeship'
    params[:branch] = ENV['CI_BRANCH']
    params[:commit] = ENV['CI_COMMIT_ID']
    params[:build] = ENV['CI_BUILD_NUMBER']
    params[:build_url] = ENV['CI_BUILD_URL']
  when DRONEIO
    # https://semaphoreapp.com/docs/available-environment-variables.html
    params[:service] = 'drone.io'
    params[:branch] = ENV['DRONE_BRANCH']
    params[:commit] = ENV['DRONE_COMMIT_SHA']
    params[:job] = ENV['DRONE_JOB_NUMBER']
    params[:build] = ENV['DRONE_BUILD_NUMBER']
    params[:build_url] = ENV['DRONE_BUILD_LINK'] || ENV['DRONE_BUILD_URL'] || ENV['CI_BUILD_URL']
    params[:pr] = ENV['DRONE_PULL_REQUEST']
    params[:tag] = ENV['DRONE_TAG']
  when GITHUB
    # https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
    params[:service] = 'github-actions'
    if (ENV['GITHUB_HEAD_REF'] || '').empty?
      params[:branch] = ENV['GITHUB_REF'].sub('refs/heads/', '')
    else
      params[:branch] = ENV['GITHUB_HEAD_REF']
      # PR refs are in the format: refs/pull/7/merge for pull_request events
      params[:pr] = ENV['GITHUB_REF'].split('/')[2]
    end
    params[:slug] = ENV['GITHUB_REPOSITORY']
    params[:build] = ENV['GITHUB_RUN_ID']
    params[:commit] = ENV['GITHUB_SHA']
  when GITLAB
    # http://doc.gitlab.com/ci/examples/README.html#environmental-variables
    # https://gitlab.com/gitlab-org/gitlab-ci-runner/blob/master/lib/build.rb#L96
    # GitLab Runner v9 renamed some environment variables, so we check both old and new variable names.
    params[:service] = 'gitlab'
    params[:branch] = ENV['CI_BUILD_REF_NAME'] || ENV['CI_COMMIT_REF_NAME']
    params[:build] = ENV['CI_BUILD_ID'] || ENV['CI_JOB_ID']
    slug = ENV['CI_BUILD_REPO'] || ENV['CI_REPOSITORY_URL']
    params[:slug] = slug.split('/', 4)[-1].sub('.git', '') if slug
    params[:commit] = ENV['CI_BUILD_REF'] || ENV['CI_COMMIT_SHA']
  when HEROKU
    params[:service] = 'heroku'
    params[:branch] = ENV['HEROKU_TEST_RUN_BRANCH']
    params[:build] = ENV['HEROKU_TEST_RUN_ID']
    params[:commit] = ENV['HEROKU_TEST_RUN_COMMIT_VERSION']
  when JENKINS
    # https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project
    # https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin#GitHubpullrequestbuilderplugin-EnvironmentVariables
    params[:service] = 'jenkins'
    params[:branch] = ENV['ghprbSourceBranch'] || ENV['GIT_BRANCH']
    params[:commit] = ENV['ghprbActualCommit'] || ENV['GIT_COMMIT']
    params[:pr] = ENV['ghprbPullId']
    params[:build] = ENV['BUILD_NUMBER']
    params[:root] = ENV['WORKSPACE']
    params[:build_url] = ENV['BUILD_URL']
  when SEMAPHORE
    # https://semaphoreapp.com/docs/available-environment-variables.html
    params[:service] = 'semaphore'
    params[:branch] = ENV['BRANCH_NAME']
    params[:commit] = ENV['REVISION']
    params[:build] = ENV['SEMAPHORE_BUILD_NUMBER']
    params[:job] = ENV['SEMAPHORE_CURRENT_THREAD']
    params[:slug] = ENV['SEMAPHORE_REPO_SLUG']
  when SHIPPABLE
    # http://docs.shippable.com/en/latest/config.html#common-environment-variables
    params[:service] = 'shippable'
    params[:branch] = ENV['BRANCH']
    params[:build] = ENV['BUILD_NUMBER']
    params[:build_url] = ENV['BUILD_URL']
    params[:pull_request] = ENV['PULL_REQUEST']
    params[:slug] = ENV['REPO_NAME']
    params[:commit] = ENV['COMMIT']
  when SOLANO
    # http://docs.solanolabs.com/Setup/tddium-set-environment-variables/
    params[:service] = 'solano'
    params[:branch] = ENV['TDDIUM_CURRENT_BRANCH']
    params[:commit] = ENV['TDDIUM_CURRENT_COMMIT']
    params[:build] = ENV['TDDIUM_TID']
    params[:pr] = ENV['TDDIUM_PR_ID']
  when TEAMCITY
    # https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters
    # Teamcity does not automatically make build parameters available as environment variables.
    # Add the following environment parameters to the build configuration
    # env.TEAMCITY_BUILD_BRANCH = %teamcity.build.branch%
    # env.TEAMCITY_BUILD_ID = %teamcity.build.id%
    # env.TEAMCITY_BUILD_URL = %teamcity.serverUrl%/viewLog.html?buildId=%teamcity.build.id%
    # env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number%
    # env.TEAMCITY_BUILD_REPOSITORY = %vcsroot.<YOUR TEAMCITY VCS NAME>.url%
    params[:service] = 'teamcity'
    params[:branch] = ENV['TEAMCITY_BUILD_BRANCH']
    params[:build] = ENV['TEAMCITY_BUILD_ID']
    params[:build_url] = ENV['TEAMCITY_BUILD_URL']
    params[:commit] = ENV['TEAMCITY_BUILD_COMMIT']
    params[:slug] = ENV['TEAMCITY_BUILD_REPOSITORY'].split('/', 4)[-1].sub('.git', '')
  when TRAVIS
    # http://docs.travis-ci.com/user/ci-environment/#Environment-variables
    params[:service] = 'travis'
    params[:branch] = ENV['TRAVIS_BRANCH']
    params[:pull_request] = ENV['TRAVIS_PULL_REQUEST']
    params[:job] = ENV['TRAVIS_JOB_ID']
    params[:slug] = ENV['TRAVIS_REPO_SLUG']
    params[:build] = ENV['TRAVIS_JOB_NUMBER']
    params[:commit] = ENV['TRAVIS_COMMIT']
    params[:env] = ENV['TRAVIS_RUBY_VERSION']
  when WERCKER
    # http://devcenter.wercker.com/articles/steps/variables.html
    params[:service] = 'wercker'
    params[:branch] = ENV['WERCKER_GIT_BRANCH']
    params[:build] = ENV['WERCKER_MAIN_PIPELINE_STARTED']
    params[:slug] = ENV['WERCKER_GIT_OWNER'] + '/' + ENV['WERCKER_GIT_REPOSITORY']
    params[:commit] = ENV['WERCKER_GIT_COMMIT']
  end

  if params[:branch].nil?
    # find branch, commit, repo from git command
    branch = `git rev-parse --abbrev-ref HEAD`.strip
    params[:branch] = branch != 'HEAD' ? branch : 'master'
  end

  if !ENV['VCS_COMMIT_ID'].nil?
    params[:commit] = ENV['VCS_COMMIT_ID']

  elsif params[:commit].nil?
    params[:commit] = `git rev-parse HEAD`.strip
  end

  slug = ENV['CODECOV_SLUG']
  params[:slug] = slug unless slug.nil?

  params[:pr] = params[:pr].sub('#', '') unless params[:pr].nil?

  params
end
detect_ci() click to toggle source
# File lib/codecov/uploader.rb, line 78
def self.detect_ci
  ci = if (ENV['CI'] == 'True') && (ENV['APPVEYOR'] == 'True')
         APPVEYOR
       elsif !ENV['TF_BUILD'].nil?
         AZUREPIPELINES
       elsif (ENV['CI'] == 'true') && !ENV['BITBUCKET_BRANCH'].nil?
         BITBUCKET
       elsif (ENV['CI'] == 'true') && (ENV['BITRISE_IO'] == 'true')
         BITRISE
       elsif (ENV['CI'] == 'true') && (ENV['BUILDKITE'] == 'true')
         BUILDKITE
       elsif (ENV['CI'] == 'true') && (ENV['CIRCLECI'] == 'true')
         CIRCLE
       elsif !ENV['CIRRUS_CI'].nil?
         CIRRUS
       elsif ENV['CODEBUILD_CI'] == 'true'
         CODEBUILD
       elsif (ENV['CI'] == 'true') && (ENV['CI_NAME'] == 'codeship')
         CODESHIP
       elsif ((ENV['CI'] == 'true') || (ENV['CI'] == 'drone')) && (ENV['DRONE'] == 'true')
         DRONEIO
       elsif (ENV['CI'] == 'true') && (ENV['GITHUB_ACTIONS'] == 'true')
         GITHUB
       elsif !ENV['GITLAB_CI'].nil?
         GITLAB
       elsif ENV['HEROKU_TEST_RUN_ID']
         HEROKU
       elsif !ENV['JENKINS_URL'].nil?
         JENKINS
       elsif (ENV['CI'] == 'true') && (ENV['SEMAPHORE'] == 'true')
         SEMAPHORE
       elsif (ENV['CI'] == 'true') && (ENV['SHIPPABLE'] == 'true')
         SHIPPABLE
       elsif ENV['TDDIUM'] == 'true'
         SOLANO
       elsif ENV['CI_SERVER_NAME'] == 'TeamCity'
         TEAMCITY
       elsif (ENV['CI'] == 'true') && (ENV['TRAVIS'] == 'true')
         TRAVIS
       elsif (ENV['CI'] == 'true') && !ENV['WERCKER_GIT_BRANCH'].nil?
         WERCKER
       end

  if !RECOGNIZED_CIS.include?(ci)
    puts [red('x>'), 'No CI provider detected.'].join(' ')
  else
    puts "==> #{ci} detected"
  end

  ci
end
display_header() click to toggle source
# File lib/codecov/uploader.rb, line 64
def self.display_header
  puts [
    '',
    '  _____          _',
    ' / ____|        | |',
    '| |     ___   __| | ___  ___ _____   __',
    '| |    / _ \ / _\`|/ _ \/ __/ _ \ \ / /',
    '| |___| (_) | (_| |  __/ (_| (_) \ V /',
    ' \_____\___/ \__,_|\___|\___\___/ \_/',
    "                               Ruby-#{::Codecov::VERSION}",
    ''
  ].join("\n")
end
gzip_report(report) click to toggle source
# File lib/codecov/uploader.rb, line 393
def self.gzip_report(report)
  puts [green('==>'), 'Gzipping contents'].join(' ')

  io = StringIO.new
  gzip = Zlib::GzipWriter.new(io)
  gzip << report
  gzip.close

  io.string
end
handle_report_response(report) click to toggle source
# File lib/codecov/uploader.rb, line 516
def self.handle_report_response(report)
  if report['result']['uploaded']
    puts "    View reports at #{report['result']['url']}"
  else
    puts red('    X> Failed to upload coverage reports')
  end
end
retry_request(req, https) click to toggle source
# File lib/codecov/uploader.rb, line 366
def self.retry_request(req, https)
  retries = 3
  begin
    response = https.request(req)
  rescue Timeout::Error, SocketError => e
    retries -= 1

    if retries.zero?
      puts 'Timeout or connection error uploading coverage reports to Codecov. Out of retries.'
      puts e
      return response
    end

    puts 'Timeout or connection error uploading coverage reports to Codecov. Retrying...'
    puts e
    retry
  rescue StandardError => e
    puts 'Error uploading coverage reports to Codecov. Sorry'
    puts e.class.name
    puts e
    puts "Backtrace:\n\t#{e.backtrace}"
    return response
  end

  response
end
upload(report, disable_net_blockers = true) click to toggle source
# File lib/codecov/uploader.rb, line 36
def self.upload(report, disable_net_blockers = true)
  net_blockers(:off) if disable_net_blockers

  display_header
  ci = detect_ci

  begin
    response = upload_to_codecov(ci, report)
  rescue StandardError => e
    puts e.message
    puts e.backtrace.join("\n")
    raise e unless ::Codecov.pass_ci_if_error

    response = false
  end

  net_blockers(:on) if disable_net_blockers

  unless response
    report['result'] = { 'uploaded' => false }
    raise StandardError.new 'Could not upload reports to Codecov' unless ::Codecov.pass_ci_if_error
    return report
  end
  report['result'] = JSON.parse(response)
  handle_report_response(report)
  report
end
upload_to_codecov(ci, report) click to toggle source
# File lib/codecov/uploader.rb, line 404
def self.upload_to_codecov(ci, report)
  url = ENV['CODECOV_URL'] || 'https://codecov.io'
  is_enterprise = url != 'https://codecov.io'

  params = build_params(ci)
  params_secret_token = params.clone
  params_secret_token['token'] = 'secret'

  query = URI.encode_www_form(params)
  query_without_token = URI.encode_www_form(params_secret_token)

  gzipped_report = gzip_report(report['codecov'])

  report['params'] = params
  report['query'] = query

  puts [green('==>'), 'Uploading reports'].join(' ')
  puts "    url:   #{url}"
  puts "    query: #{query_without_token}"

  response = false
  unless is_enterprise
    response = upload_to_v4(url, gzipped_report, query, query_without_token)
    return false if response == false
  end

  response || upload_to_v2(url, gzipped_report, query, query_without_token)
end
upload_to_v2(url, report, query, query_without_token) click to toggle source
# File lib/codecov/uploader.rb, line 494
def self.upload_to_v2(url, report, query, query_without_token)
  uri = URI.parse(url.chomp('/') + '/upload/v2')
  https = Net::HTTP.new(uri.host, uri.port)
  https.use_ssl = !url.match(/^https/).nil?

  puts [green('-> '), 'Uploading to Codecov'].join(' ')
  puts "#{url}#{uri.path}?#{query_without_token}"

  req = Net::HTTP::Post.new(
    "#{uri.path}?#{query}",
    {
      'Accept' => 'application/json',
      'Content-Encoding' => 'gzip',
      'Content-Type' => 'text/plain',
      'X-Content-Encoding' => 'gzip'
    }
  )
  req.body = report
  res = retry_request(req, https)
  res&.body
end
upload_to_v4(url, report, query, query_without_token) click to toggle source
# File lib/codecov/uploader.rb, line 433
def self.upload_to_v4(url, report, query, query_without_token)
  uri = URI.parse(url.chomp('/') + '/upload/v4')
  https = Net::HTTP.new(uri.host, uri.port)
  https.use_ssl = !url.match(/^https/).nil?

  puts [green('-> '), 'Pinging Codecov'].join(' ')
  puts "#{url}#{uri.path}?#{query_without_token}"

  req = Net::HTTP::Post.new(
    "#{uri.path}?#{query}",
    {
      'X-Reduced-Redundancy' => 'false',
      'X-Content-Encoding' => 'application/x-gzip',
      'Content-Type' => 'text/plain'
    }
  )
  response = retry_request(req, https)
  if !response&.code || response.code == '400'
    puts red(response&.body)
    return false
  end

  reports_url = response.body.lines[0]
  s3target = response.body.lines[1]

  if s3target.nil? || s3target.empty?
    puts red(response.body)
    return false
  end

  puts [green('-> '), 'Uploading to'].join(' ')
  puts s3target

  uri = URI(s3target)
  https = Net::HTTP.new(uri.host, uri.port)
  https.use_ssl = true
  req = Net::HTTP::Put.new(
    s3target,
    {
      'Content-Encoding' => 'gzip',
      'Content-Type' => 'text/plain'
    }
  )
  req.body = report
  res = retry_request(req, https)
  if res&.body == ''
    {
      'uploaded' => true,
      'url' => reports_url,
      'meta' => {
        'status' => res.code
      },
      'message' => 'Coverage reports upload successfully'
    }.to_json
  else
    puts [black('-> '), 'Could not upload reports via v4 API, defaulting to v2'].join(' ')
    puts red(res&.body || 'nil')
    nil
  end
end

Private Class Methods

black(str) click to toggle source

Convenience color methods

# File lib/codecov/uploader.rb, line 557
def self.black(str)
  str.nil? ? '' : "\e[30m#{str}\e[0m"
end
green(str) click to toggle source
# File lib/codecov/uploader.rb, line 565
def self.green(str)
  str.nil? ? '' : "\e[32m#{str}\e[0m"
end
net_blockers(switch) click to toggle source

Toggle VCR and WebMock on or off

@param switch Toggle switch for Net Blockers. @return [Boolean]

# File lib/codecov/uploader.rb, line 530
def self.net_blockers(switch)
  throw 'Only :on or :off' unless %i[on off].include? switch

  if defined?(VCR)
    case switch
    when :on
      VCR.turn_on!
    when :off
      VCR.turn_off!(ignore_cassettes: true)
    end
  end

  if defined?(WebMock)
    # WebMock on by default
    # VCR depends on WebMock 1.8.11; no method to check whether enabled.
    case switch
    when :on
      WebMock.enable!
    when :off
      WebMock.disable!
    end
  end

  true
end
red(str) click to toggle source
# File lib/codecov/uploader.rb, line 561
def self.red(str)
  str.nil? ? '' : "\e[31m#{str}\e[0m"
end