Skip to content

Instantly share code, notes, and snippets.

@brand-it
Last active April 22, 2024 22:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brand-it/972f92815888a62c45a02bf34c6aa3ea to your computer and use it in GitHub Desktop.
Save brand-it/972f92815888a62c45a02bf34c6aa3ea to your computer and use it in GitHub Desktop.
#!/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
@brand-it
Copy link
Author

brand-it commented Jan 13, 2023

sudo wget -O /usr/local/bin/codefresh-rspec https://gist.githubusercontent.com/brand-it/972f92815888a62c45a02bf34c6aa3ea/raw/5a9837696cd6379052c8b673a289848908a3a019/codefresh-rspec
sudo chmod 755 /usr/local/bin/codefresh-rspec
sudo chown $(whoami) /usr/local/bin/codefresh-rspec

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 using export CODEFRESH_API_TOKEN=<token>. You can place this export in ~/.bashrc if your using that.

Usage: codefresh-rspec [options]
    -b, --branch [NAME]              Name of branch (default: chore/COR-13027)
    -n, --repo-name [NAME]           Name of the repo (default: weedmaps)
    -w, --workflow-name [NAME]       Name of the workflow build branch export CODEFRESH_BUILD_WORKFLOW_NAME (default: build)
    -o, --repo-owner [OWNER]         Owner of the repo (default: GhostGroup)
    -l, --log-level [LEVEL]          Log level [debug, info, warn, error] (default: info)
    -f, --format [FORMAT]            This will change the format from the default rspec spec/file:123 to somethings else [file, info, newline] (default: info)
    -a, --api-token [TOKEN]          Owner of the repo CODEFRESH_API_TOKEN=4058b4********** https://g.codefresh.io/user/settings
    -h, --help                       Prints this help message
bundle exec rspec (codefresh-rspec -f newline)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment