# frozen_string_literal: true
require 'uri'
require 'json'
require 'net/http'
require 'simplecov'
require 'zlib'
require_relative 'version'
class Codecov::Uploader
### CIs
RECOGNIZED_CIS = [
APPVEYOR = 'Appveyor CI',
AZUREPIPELINES = 'Azure Pipelines',
BITBUCKET = 'Bitbucket',
BITRISE = 'Bitrise CI',
BUILDKITE = 'Buildkite CI',
CIRCLE = 'Circle CI',
CIRRUS = 'Cirrus CI',
CODEBUILD = 'Codebuild CI',
CODESHIP = 'Codeship CI',
DRONEIO = 'Drone CI',
GITHUB = 'GitHub Actions',
GITLAB = 'GitLab CI',
HEROKU = 'Heroku CI',
JENKINS = 'Jenkins CI',
SEMAPHORE = 'Semaphore CI',
SHIPPABLE = 'Shippable',
SOLANO = 'Solano CI',
TEAMCITY = 'TeamCity CI',
TRAVIS = 'Travis CI',
WERCKER = 'Wercker CI'
].freeze
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
def self.display_header
puts [
'',
' _____ _',
' / ____| | |',
'| | ___ __| | ___ ___ _____ __',
'| | / _ \ / _\`|/ _ \/ __/ _ \ \ / /',
'| |___| (_) | (_| | __/ (_| (_) \ V /',
' \_____\___/ \__,_|\___|\___\___/ \_/',
" Ruby-#{::Codecov::VERSION}",
''
].join("\n")
end
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
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
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
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
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
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
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
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
private
# Toggle VCR and WebMock on or off
#
# @param switch Toggle switch for Net Blockers.
# @return [Boolean]
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
# Convenience color methods
def self.black(str)
str.nil? ? '' : "\e[30m#{str}\e[0m"
end
def self.red(str)
str.nil? ? '' : "\e[31m#{str}\e[0m"
end
def self.green(str)
str.nil? ? '' : "\e[32m#{str}\e[0m"
end
end
require_relative 'configuration'
Codecov.extend Codecov::Configuration