Created
November 21, 2019 19:42
-
-
Save pcreux/a7a7b0b3cb06ddd3dd6b75a21dc91794 to your computer and use it in GitHub Desktop.
Generate a trace of your circleci workflows
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/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} (#{((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