Skip to content

Instantly share code, notes, and snippets.

@danielfone
Last active October 13, 2019 20:22
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 danielfone/19d7ecb2451616bb9b42c364d6a5527b to your computer and use it in GitHub Desktop.
Save danielfone/19d7ecb2451616bb9b42c364d6a5527b to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby -w
require 'time'
require 'json'
#
# Helper methods
#
module Enumerable
def mean(method=:self)
map(&method).reduce(:+) / size.to_f
end
end
class CycleTimes
HEROKU_APP = ARGV.shift or raise "Usage: #{$0} HEROKU_APP"
RELEASE_COUNT = Integer(ENV['RELEASE_COUNT'] || 100)
FORTNIGHT_AGO = (Date.today - 14).to_time
def self.print
new.print
end
def print
puts fortnight_cycles
puts
puts "## #{HEROKU_APP}: Average Commit Cycle Time"
puts "Fortnight: #{Cycle.stats(fortnight_cycles)}"
puts "Overall: #{Cycle.stats(cycles)} since #{deploys.last.time}"
end
private
def fortnight_cycles
@fortnight_cycles ||= cycles.select { |c| c.commit.time > FORTNIGHT_AGO }
end
def cycles
@cycles ||= Commit.fetch_all(commit_range).map do |commit|
Cycle.new(commit, current_deploy(commit))
end
end
def commit_range
@commit_range ||= "#{deploys.first.sha}...#{deploys.last.sha}"
end
def current_deploy(commit)
@next_deploys ||= deploys.uniq(&:sha)
@current_deploy = @next_deploys.shift if commit.sha == @next_deploys.first.sha
@current_deploy
end
def deploys
@deploys ||= Deploy.fetch_all(HEROKU_APP, RELEASE_COUNT)
end
Cycle = Struct.new(:commit, :deploy) do
def self.duration(seconds)
hours = seconds.to_i / 3600
mins = (seconds.to_i - hours * 3600) / 60
"#{hours}h #{mins}m"
end
def self.stats(cycles)
deploys = cycles.uniq(&:deploy)
"#{duration cycles.mean(:cycle_time)} (#{cycles.size} commits, #{deploys.size} deploys)"
end
def cycle_time
@cycle_time ||= deploy.time - commit.time
end
def duration
self.class.duration(cycle_time)
end
def to_s
"#{commit.sha} #{commit.time} -> #{deploy.time} (#{duration}) #{commit.subject}"
end
end
Deploy = Struct.new(:sha, :time) do
def self.fetch_all(app, release_count)
releases(app, release_count)
.select { |release| release['description'].start_with?('Deploy') }
.map(&method(:build))
end
def self.build(release)
new(
release['description'].match(/Deploy (\w+)/)[1],
Time.parse(release['created_at'])
)
end
def self.releases(app, count)
JSON.parse `heroku releases --json --num #{count} --app #{app}`
end
end
Commit = Struct.new(:sha, :time, :subject) do
def self.fetch_all(commit_range)
git_log_lines(commit_range).map do |line|
sha, time, subject = line.split(' ', 3)
new(sha, Time.parse(time), subject.strip)
end
end
def self.git_log_lines(range)
`git log --pretty="%h %aI %s" #{range}`.each_line
end
end
end
CycleTimes.print
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment