Skip to content

Instantly share code, notes, and snippets.

@pcreux
Created November 21, 2019 19:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pcreux/a7a7b0b3cb06ddd3dd6b75a21dc91794 to your computer and use it in GitHub Desktop.
Save pcreux/a7a7b0b3cb06ddd3dd6b75a21dc91794 to your computer and use it in GitHub Desktop.
Generate a trace of your circleci workflows
#!/usr/local/env ruby
#
# Generate html traces from CircleCI workflows
#
require 'json'
require 'http'
OWNER = 'githubowner'
REPO = 'githubrepo'
TOKEN = 'your circleci token'
PAGES = 10 # we pull 100 jobs per page
def run
g = CircleCli::CommitGateway.new(
owner: OWNER,
github_repo: REPO,
api_token: TOKEN
)
g.find_all_commits('master')[0..-2].each do |commit|
File.open("circleci-#{commit.jobs.map(&:start_at).min.strftime("%F-%T")}-#{commit.revision}.html", 'w') { |f| f.puts html(commit) }
end
end
def html(commit)
min_start_at = commit.jobs.map(&:start_at).compact.min
max_stop_at = commit.jobs.map(&:stop_at).compact.max
max = min_start_at + 30 * 60 # 30 min for 800px
width = 800
ratio = width / (max - min_start_at)
commit.jobs.sort_by { |j|
[ (j.start_at.to_i / 30).round, j.stop_at ]
}.map do |job|
next if job.stop_at.nil?
padding = 5
margin_left = (job.start_at - min_start_at) * ratio
width = (job.stop_at - job.start_at) * ratio - padding * 2
"<div style='
margin: 5px;
margin-left: #{margin_left}px;
width: #{width}px;
background: #add;
padding: #{padding}px;
border: solid 2px #ddd;
border-radius: 5px;
font-family: monospace;
'>
#{job.name}&nbsp;(#{((job.stop_at - job.start_at) / 60).to_i}m)
</div>"
end.join + "Total duration: #{"%.2f" % ((max_stop_at - min_start_at) / 60)}m - #{commit.revision[0..6]} - #{commit.date} - #{commit.author} - #{commit.subject}"
end
module CircleCli
class Commit
attr_reader :date, :author, :revision, :subject, :jobs
def initialize(date:, author:, revision:, subject:, jobs:)
@date = date
@author = author
@revision = revision
@subject = subject
@jobs = jobs
end
def ==(other)
other.revision == revision
end
def status
return 'success' if @jobs.all? { |j| j.status == 'success' }
return 'failed' if @jobs.all? { |j| j.status == 'failed' }
return 'failing' if @jobs.any? { |j| j.status == 'failed' }
return 'running' if @jobs.any? { |j| j.status == 'running' }
return 'not_running' if @jobs.any? { |j| j.status == 'not_running' }
return 'queued' if @jobs.any? { |j| j.status == 'queued' }
@jobs.map(&:status).uniq
end
end
class Job
attr_reader :name, :status, :queued_at, :start_at, :build_time, :stop_at
def initialize(name:, status:, queued_at:, start_at:, stop_at:, build_time:)
@name = name
@status = status
@queued_at = Time.parse(queued_at) if queued_at
@start_at = Time.parse(start_at) if start_at
@stop_at = Time.parse(stop_at) if stop_at
@build_time = build_time
end
end
class CommitGateway
def initialize(owner:, github_repo:, api_token:)
@api_token = api_token
@owner = owner
@github_repo = github_repo
end
def find_all_commits(branch)
all_jobs = []
JOBS.times do |n|
puts n
all_jobs += fetch_jobs(branch: branch, page: n)
end
if all_jobs.empty?
puts "No jobs found for github.com/#{@owner}/#{@github_repo} - #{branch}"
exit(-1)
end
all_jobs.map do |job|
committer_date = job['committer_date']
next if committer_date.nil?
Commit.new(
date: Time.parse(committer_date).strftime("%h %d %H:%M"),
author: job['user']['login'],
revision: job['vcs_revision'],
subject: job['subject'],
jobs: extract_jobs_for_commit(
all_jobs: all_jobs,
commit_date: committer_date
)
)
end.compact.uniq.select { |c| c.status == 'success' }
end
def find_latest_commit(branch)
all_jobs = fetch_jobs(branch: branch)
if all_jobs.empty?
puts "No jobs found for github.com/#{@owner}/#{@github_repo} - #{branch}"
exit(-1)
end
committer_date = all_jobs.first['committer_date']
Commit.new(
date: Time.parse(committer_date).strftime("%h %d %H:%M"),
author: all_jobs.first['user']['login'],
revision: all_jobs.first['vcs_revision'],
subject: all_jobs.first['subject'],
jobs: extract_jobs_for_commit(
all_jobs: all_jobs,
commit_date: committer_date
)
)
end
private def extract_jobs_for_commit(all_jobs:, commit_date:)
all_jobs
.select { |job| job['committer_date'] == commit_date }
.map { |job| extract_job(job) }
end
private def extract_job(circle_job)
Job.new(
name: circle_job['job_name'] || circle_job['workflows']['job_name'],
status: circle_job['status'],
queued_at: circle_job['queued_at'],
start_at: circle_job['start_time'],
stop_at: circle_job['stop_time'],
build_time: calculate_build_time_in_seconds(circle_job)
)
end
private def calculate_build_time_in_seconds(circle_job)
if circle_job['build_time_millis']
circle_job['build_time_millis'].to_i / 1000
elsif circle_job['queued_at']
(Time.now - Time.parse(circle_job['queued_at'])).to_i
elsif circle_job['start_at']
(Time.now - Time.parse(circle_job['start_at'])).to_i
else
0
end
end
private def fetch_jobs(branch:, page: 0)
api_url = "https://circleci.com/api/v1.1/project/github/#{@owner}/#{@github_repo}/tree/#{branch}?limit=100&offset=#{100 * page}"
JSON.parse(
HTTP
.headers('Accept' => "application/json")
.basic_auth(user: @api_token, pass: "")
.get(api_url).to_s
)
end
end
end
run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment