Last active
April 22, 2024 22:59
-
-
Save brand-it/972f92815888a62c45a02bf34c6aa3ea to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# frozen_string_literal: true | |
require 'uri' | |
require 'json' | |
require 'net/http' | |
require 'optparse' | |
require 'forwardable' | |
COMMAND_NAME = File.basename(__FILE__) | |
# A nice way to add color to strings | |
class PrettyString < String | |
# https://no-color.org/ | |
NO_COLOR = ENV.key?('NO_COLOR') || `tput colors`.chomp.to_i < 8 | |
ANSI_COLORS = { | |
white: 0, | |
red: 31, | |
green: 32, | |
yellow: 33, | |
blue: 34, | |
magenta: 35 | |
}.freeze | |
ANSI_COLORS.each do |name, code| | |
define_method(name) { NO_COLOR ? self : "\e[#{code}m#{self}\e[0m" } | |
end | |
end | |
class VerifyWorkflow | |
def self.call(codefresh_response) | |
if codefresh_response.workflow.empty? | |
Logger.warn "Could not find a worflow for #{ArgParser.options['repo-name']}/#{ArgParser.options['repo-owner']} (#{ArgParser.options['branch']})" | |
exit 1 | |
elsif codefresh_response.workflow['status'] != 'error' | |
Logger.warn( | |
"Current workflow is status is #{codefresh_response.workflow['status']} and has to be error" | |
) | |
Logger.details( | |
"View Build Here: #{Codefresh::API_URL}/build/#{codefresh_response.workflow['id']}" | |
) | |
exit 1 | |
end | |
end | |
end | |
# Standard logging to STDOUT | |
# Logger.info('foo') | |
# Logger.info('foo', 'bar') | |
# Logger.debug { ['foo', 'bar'] } | |
# Logger.obscure('something something') | |
# Logger.level = :debug | |
class Logger | |
LEVELS = %i[debug info warn error].freeze | |
DEFAULT_LEVEL = :info | |
class << self | |
def level=(level) | |
@level = LEVELS.index(level&.to_sym) | |
end | |
def level | |
@level ||= LEVELS.index(DEFAULT_LEVEL) | |
end | |
def info(*messages) | |
log(messages, :white) if loggable?(:info) | |
end | |
def error(*messages) | |
log(messages, :red) if loggable?(:error) | |
end | |
def warn(*messages) | |
log(messages, :yellow) if loggable?(:warn) | |
end | |
def success(*messages) | |
log(messages, :green) if loggable?(:info) | |
end | |
def details(*messages) | |
log(messages, :blue) if loggable?(:info) | |
end | |
def debug | |
log(yield, :magenta) if loggable?(:debug) | |
end | |
def loggable?(l) | |
LEVELS.index(l) >= level | |
end | |
def log(*messages, color) | |
Array(messages).flatten.each do |message| | |
puts PrettyString.new(message.to_s).public_send(color) | |
end | |
end | |
def obscure(string, length = 6) | |
string = string.to_s | |
total_obscured = [10, length].max | |
"#{string[0, length]}#{'*' * total_obscured}" | |
end | |
end | |
end | |
# Usage | |
# ArgParser.parser('transitions') do |ops| | |
# ops.on('-t', '--transition-to', 'Prints this help message') | |
# end | |
# | |
# ArgParser.parser.require('transitions') | |
# | |
# calling options will excute a parse and then return a hash of options | |
# if you defined a option of --transition-to the key will be 'transition-to' | |
# ArgParser.options | |
class ArgParser | |
class << self | |
def parser(command_name = nil) | |
@parser ||= OptionParser.new do |opts| | |
opts.banner = "Usage: #{command_name} [options]" | |
yield opts | |
opts.on('-h', '--help', 'Prints this help message') do | |
Logger.info opts.to_s | |
exit | |
end | |
end | |
end | |
def require(key) | |
return if options[key].to_s != '' | |
Logger.info parser | |
Logger.error "Missing option: --#{key}" | |
exit 1 | |
end | |
def options | |
@options ||= new.tap(&:parse!).options | |
end | |
end | |
def options | |
@options ||= {} | |
end | |
def parse! | |
self.class.parser.parse!(into: options) | |
options.transform_keys!(&:to_s) | |
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e | |
Logger.info self.class.parser | |
Logger.error "Invalid option: #{e.message}" | |
exit 1 | |
end | |
end | |
class GitInfo | |
REMOTE_URL_MATCHER = %r{(?<host>\S+)(:|/)(?<repo_owner>\S+)/(?<repo_name>\S+)\.(?<prefix>\S+)}.freeze | |
class << self | |
def github_url | |
@github_url ||= "https://github.com/#{repo_path}" | |
end | |
def repo_path | |
@repo_path ||= "#{repo_owner}/#{repo_name}" | |
end | |
def repo_name | |
@repo_name ||= remote_origin_url[:repo_name] | |
end | |
def repo_owner | |
@repo_owner ||= remote_origin_url[:repo_owner] | |
end | |
def branch | |
@branch ||= `git rev-parse --abbrev-ref HEAD`.chomp.strip | |
end | |
private | |
def remote_origin_url | |
`git config --get remote.origin.url`.match(REMOTE_URL_MATCHER) || {} | |
end | |
end | |
end | |
class Codefresh | |
API_URL = ENV['CODEFRESH_API_URL'] || 'https://g.codefresh.io' | |
Response = Struct.new(:response, :workflow) | |
class << self | |
def progress_logs | |
new.progress_logs | |
end | |
end | |
def progress_logs | |
return Response.new(nil, workflow) if progress.nil? | |
Response.new api_request(progress.dig('location', 'url'), content_type: nil), workflow | |
end | |
private | |
def api_client(url) | |
Net::HTTP.new(url.host, url.port).tap do |http| | |
http.use_ssl = true | |
end | |
end | |
def workflow | |
return @workflow if @workflow | |
query = { | |
limit: 20, | |
repoName: ArgParser.options['repo-name'], | |
repoOwner: ArgParser.options['repo-owner'], | |
branchName: ArgParser.options['branch'] | |
} | |
path = "workflow?#{URI.encode_www_form(query)}" | |
docs = api_request([API_URL, 'api', path].join('/')).dig('workflows', 'docs') || [] | |
@workflow = docs.find(-> { {} }) do |workflow| | |
workflow['serviceName'].downcase == ArgParser.options['workflow-name'].downcase && | |
ArgParser.options['repo-name'] == workflow['repoName'] | |
end | |
end | |
def progress | |
return @progress if @progress | |
return if workflow['progress'].nil? || workflow['status'] != 'error' | |
@progress = api_request([API_URL, 'api', "progress/#{workflow['progress']}"].join('/')) | |
end | |
def parse_json(response, default = {}) | |
return default if response.to_s == '' | |
JSON.parse(response) | |
rescue JSON::ParserError | |
Logger.error "Failed to parse response: #{response}" | |
{} | |
end | |
def api_request(url, method: :get, body: nil, content_type: 'application/json', redirect_limit: 5) | |
uri = URI.parse(url) | |
api_client = api_client(uri) | |
request = Net::HTTP.const_get(method.to_s.capitalize).new([uri.path, uri.query].compact.join('?')) | |
request.body = body.to_json if body | |
request.add_field('Content-Type', content_type) if content_type | |
request.add_field('Authorization', "Bearer #{ArgParser.options['api-token']}") | |
Logger.debug do | |
[ | |
"Method: #{method}", | |
"URI: #{uri}", | |
"Query: #{uri.query}", | |
"Request Path: #{request.path}", | |
"Body: #{body}", | |
"Request: #{request.inspect}", | |
"Bearer Token: #{Logger.obscure(ArgParser.options['api-token'].split('.').last)}", | |
"Host: #{uri.host}", | |
"API Client: #{api_client.inspect}", | |
"Headers: #{request.to_hash}" | |
] | |
end | |
response = api_client.request(request) | |
Logger.debug do | |
[ | |
"Response Code: #{response.code}", | |
"Response Message: #{response.message}", | |
"Response Body: #{response.body[0..1000]}" | |
] | |
end | |
if response.code == '301' && redirect_limit.positive? | |
return api_request( | |
response['location'], | |
method: method, | |
body: body, | |
content_type: content_type, | |
redirect_limit: (redirect_limit - 1) | |
) | |
end | |
parse_json(response.body) | |
end | |
end | |
class ScanFailures | |
attr_reader :failures | |
START_MATCHER = /(Failures:|Failure\/Error:)/ | |
END_MATCHER = /Finished in \d+/ | |
def initialize(failures) | |
@failures = failures.lines.map(&:strip) | |
end | |
def call | |
capture = false | |
failures.each_with_object([]) do |failure, result| | |
if failure =~ END_MATCHER | |
Logger.debug { "End Capture at: #{failure}" } | |
capture = false | |
result << failure | |
elsif failure =~ START_MATCHER | |
Logger.debug { "Start Capture at: #{failure}" } | |
capture = true | |
result << failure | |
elsif capture | |
result << failure | |
end | |
end | |
end | |
end | |
class UpdateScript | |
GIST_URL = URI('https://api.github.com/gists/972f92815888a62c45a02bf34c6aa3ea') | |
GIST_LINK = 'https://gist.github.com/brand-it/972f92815888a62c45a02bf34c6aa3ea' | |
ONE_DAY = 86_400 | |
UPDATE_COMPLETE_TEXT = <<~TXT | |
███████╗███╗ ██╗██╗ ██╗ █████╗ ███╗ ██╗ ██████╗███████╗ | |
██╔════╝████╗ ██║██║ ██║██╔══██╗████╗ ██║██╔════╝██╔════╝ | |
█████╗ ██╔██╗ ██║███████║███████║██╔██╗ ██║██║ █████╗ | |
██╔══╝ ██║╚██╗██║██╔══██║██╔══██║██║╚██╗██║██║ ██╔══╝ | |
███████╗██║ ╚████║██║ ██║██║ ██║██║ ╚████║╚██████╗███████╗ | |
╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝╚══════╝ | |
██████╗ ██████╗ ███╗ ███╗██████╗ ██╗ ███████╗████████╗███████╗ | |
██╔════╝██╔═══██╗████╗ ████║██╔══██╗██║ ██╔════╝╚══██╔══╝██╔════╝ | |
██║ ██║ ██║██╔████╔██║██████╔╝██║ █████╗ ██║ █████╗ | |
██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██╔══╝ ██║ ██╔══╝ | |
╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ███████╗███████╗ ██║ ███████╗ | |
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝ | |
TXT | |
def diff? | |
not_changed_recently? && diffs.any? | |
end | |
def diffs | |
return @diffs if defined? @diffs | |
@diffs = [] unless get | |
unless get && get.dig(:files, :'codefresh-rspec', :content) | |
touch | |
@diffs = [] | |
end | |
@diffs ||= reject_blanks(get.dig(:files, :'codefresh-rspec', :content).lines) - reject_blanks(file.lines.compact) | |
end | |
def reject_blanks(array) | |
array.reject { |s| s.strip.empty? } | |
end | |
def not_changed_recently? | |
updated_last + ONE_DAY < Time.now | |
end | |
def update! | |
File.open(__FILE__, 'w') do |file| | |
file.write(get.dig(:files, :'codefresh-rspec', :content)) | |
end | |
end | |
def touch | |
File.lutime(Time.now, Time.now, __FILE__) | |
end | |
def updated_last | |
File.mtime(__FILE__) | |
end | |
def file | |
File.read(__FILE__) | |
end | |
def lines | |
@lines ||= get&.dig(:files, :'codefresh-rspec', :content)&.lines || [] | |
end | |
def get | |
@get ||= JSON.parse(Net::HTTP.get(GIST_URL), symbolize_names: true) | |
rescue StandardError => e | |
puts e.message.to_s | |
nil | |
end | |
def show_diffs | |
diffs | |
end | |
end | |
begin | |
update_script = UpdateScript.new | |
if update_script.diff? | |
update_script.update! | |
puts UpdateScript::UPDATE_COMPLETE_TEXT | |
end | |
ArgParser.parser(COMMAND_NAME) do |ops| | |
ops.on('-b', '--branch [NAME]', "Name of branch (default: #{GitInfo.branch})") | |
ops.on('-n', '--repo-name [NAME]', "Name of the repo (default: #{GitInfo.repo_name})") | |
ops.on('-w', '--workflow-name [NAME]', | |
"Name of the workflow build branch #{ENV['CODEFRESH_BUILD_WORKFLOW_NAME'] || 'export CODEFRESH_BUILD_WORKFLOW_NAME'} (default: build)") | |
ops.on('-o', '--repo-owner [OWNER]', "Owner of the repo (default: #{GitInfo.repo_owner})") | |
ops.on('-l', '--log-level [LEVEL]', "Log level [#{Logger::LEVELS.join(', ')}] (default: #{Logger::DEFAULT_LEVEL})") | |
ops.on('-f', '--format [FORMAT]', | |
'This will change the format from the default rspec spec/file:123 to somethings else [space, info, newline] (default: info)') | |
ops.on('-a', '--api-token [TOKEN]', | |
"Owner of the repo #{ENV['CODEFRESH_API_TOKEN'].to_s != '' ? "CODEFRESH_API_TOKEN=#{Logger.obscure(ENV['CODEFRESH_API_TOKEN'].split('.').last)}" : 'export CODEFRESH_API_TOKEN=<token>'} https://g.codefresh.io/user/settings") | |
end | |
ArgParser.options.tap do |options| | |
Logger.level = options['log-level'] | |
options['branch'] ||= GitInfo.branch | |
options['repo-name'] ||= GitInfo.repo_name | |
options['repo-owner'] ||= GitInfo.repo_owner | |
options['api-token'] ||= ENV['CODEFRESH_API_TOKEN'] | |
options['workflow-name'] ||= ENV['CODEFRESH_BUILD_WORKFLOW_NAME'] || 'build' | |
options['format'] ||= 'info' | |
end | |
ArgParser.require('branch') | |
ArgParser.require('repo-name') | |
ArgParser.require('repo-owner') | |
ArgParser.require('api-token') | |
ArgParser.require('format') | |
Logger.debug { ArgParser.options } | |
case ArgParser.options['format'] | |
when 'space', 'newline' | |
progress_logs = Codefresh.progress_logs | |
VerifyWorkflow.call(progress_logs) | |
logs = progress_logs.response['steps']&.flat_map { |s| s['logs'] }&.join || '' | |
found = ScanFailures.new(logs).call | |
# failed_pattern = %r{\[31mrspec \./(?<spec>spec/\S+:\d+)} | |
failed_pattern = %r{(?<spec>spec/\S+_spec.rb:\d+)} | |
found.select! { _1.match(failed_pattern) } | |
.map! { _1.match(failed_pattern)[:spec].strip } | |
.sort! | |
.uniq! | |
join_with = ArgParser.options['format'] == 'newline' ? "\n" : ' ' | |
puts found.join(join_with) | |
when 'info' | |
progress_logs = Codefresh.progress_logs | |
VerifyWorkflow.call(progress_logs) | |
logs = progress_logs.response['steps']&.flat_map { |s| s['logs'] }&.join || '' | |
found = ScanFailures.new(logs).call | |
puts found.empty? ? logs : found.join("\n") | |
Logger.details( | |
"View Build Here: #{Codefresh::API_URL}/build/#{progress_logs.workflow['id']}" | |
) | |
else | |
Logger.info ArgParser.parser.help | |
Logger.error "Invalid format #{ArgParser.options['format']} use file or info" | |
exit 1 | |
end | |
rescue Interrupt | |
puts 'Interrupted' | |
exit | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
To get setup visit https://g.codefresh.io/user/settings. You will need to grab a token to access the API. once you have it pass the token in as an argument
codefresh-rspec -a 1352348BAWARG3852SB
. To make it so you don't have to type it in all the time export the token usingexport CODEFRESH_API_TOKEN=<token>
. You can place this export in~/.bashrc
if your using that.