Skip to content

Commit

Permalink
Merge pull request #12 from NoRedInk/format-dependency-changes
Browse files Browse the repository at this point in the history
Format dependency changes for slack output
  • Loading branch information
dgtized authored Jan 23, 2019
2 parents 6a000dc + fb1612b commit 8805ce6
Show file tree
Hide file tree
Showing 23 changed files with 864 additions and 304 deletions.
37 changes: 13 additions & 24 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,45 +1,34 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-01-03 16:54:26 -0600 using RuboCop version 0.60.0.
# on 2019-01-22 16:22:58 -0600 using RuboCop version 0.60.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 1
# Cop supports --auto-correct.
Lint/UnneededCopDisableDirective:
Exclude:
- 'lib/deploy_complexity/checklists.rb'

# Offense count: 6
# Cop supports --auto-correct.
Lint/UnneededCopEnableDirective:
Exclude:
- 'exe/deploy-complexity.rb'

# Offense count: 2
# Offense count: 3
Metrics/AbcSize:
Max: 43

# Offense count: 1
# Configuration parameters: CountComments, ExcludedMethods.
# ExcludedMethods: refine
Metrics/BlockLength:
Max: 32
Max: 52

# Offense count: 2
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 124

# Offense count: 6
# Configuration parameters: CountComments, ExcludedMethods.
Metrics/MethodLength:
Max: 33
Max: 41

# Offense count: 1
Metrics/PerceivedComplexity:
Max: 10
Max: 9

# Offense count: 11
# Offense count: 12
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- 'exe/deploy-complexity.rb'
- 'lib/deploy_complexity/deploy.rb'
- 'lib/deploy_complexity/output_formatter.rb'
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ PATH
specs:
deploy-complexity (0.7.0)
octokit (~> 4.0)
slack-notifier (~> 2.3.2)
values (~> 1.8.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -57,7 +59,9 @@ GEM
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
slack-notifier (2.3.2)
unicode-display_width (1.4.0)
values (1.8.0)
yard (0.9.16)

PLATFORMS
Expand Down
2 changes: 2 additions & 0 deletions deploy-complexity.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "rake", "~> 10.0"

spec.add_dependency "octokit", "~> 4.0"
spec.add_dependency "slack-notifier", "~> 2.3.2"
spec.add_dependency "values", "~> 1.8.0"
end
153 changes: 13 additions & 140 deletions exe/deploy-complexity.rb
Original file line number Diff line number Diff line change
@@ -1,139 +1,12 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'time'
require 'bundler/setup'
require 'deploy_complexity/version'
require 'deploy_complexity/revision_comparator'
require 'deploy_complexity/changed_files'
require 'deploy_complexity/changed_elm_packages'
require 'deploy_complexity/changed_javascript_packages'
require 'deploy_complexity/changed_ruby_gems'

# tag format: production-2016-10-22-0103 or $ENV-YYYY-MM-DD-HHmm
def parse_when(tag)
tag.match(/-(\d{4}-\d{2}-\d{2}-\d{2}\d{2})/) do |m|
Time.strptime(m[1], '%Y-%m-%d-%H%M')
end
end

COMPARE_FORMAT = "%s/compare/%s...%s"

def time_between_deploys(from, to)
deploy_time = parse_when(to)
last_time = parse_when(from)

hours = deploy_time && (deploy_time - last_time) / 60**2

if hours.nil?
"pending deploy"
elsif hours < 24
"after %2.1f %s" % [hours, "hours"]
else
"after %2.1f %s" % [(hours / 24), "days"]
end
end

# converts origin/master -> master
def safe_name(name)
name.chomp.split(%r{/}).last
end

# converts a branch name like master into the closest tag or commit sha
def reference(name)
branch = safe_name(name)
tag = `git tag --points-at #{name} | grep #{branch}`.chomp
if tag.empty?
`git rev-parse --short #{branch}`.chomp
else
tag
end
end

def pull_requests(merges, gh_url)
prs = merges.map do |line|
line.match(/pull request #(\d+) from (.*)$/) do |m|
[gh_url, m[1].to_i, "-", safe_name(m[2])]
end || line.match(/(\w+)\s+(.*)\(\#(\d+)\)/) do |m|
[gh_url, m[3].to_i, "S", m[2]] # squash merge
end
end
prs.compact.map { |x| "%s/pull/%d %1s %s" % x }
end

def list_migrations(changed_files)
migrations = changed_files.migrations
return unless migrations.any?

puts "Migrations:"
puts migrations
puts
end

def file_changes(changed_files, base:, to:)
list_migrations(changed_files)

RevisionComparator.new(
ChangedElmPackages, changed_files.elm_packages, base, to
).output("Changed Elm packages:")

RevisionComparator.new(
ChangedRubyGems, changed_files.ruby_dependencies, base, to
).output("Ruby dependency changes:")

RevisionComparator.new(
ChangedJavascriptPackages, changed_files.javascript_dependencies, base, to
).output("Javascript Dependency Changes:")

# TODO: scan for changes to app/jobs and report changes to params
end
require 'deploy_complexity/deploy'
require 'optparse'

# deploys are the delta from base -> to, so to contains commits to add to base
def deploy(base, to, options)
gh_url = options[:gh_url]
dirstat = options[:dirstat]
stat = options[:stat]

range = "#{base}...#{to}"

# tag_revision = `git rev-parse --short #{to}`.chomp
revision = `git rev-list --abbrev-commit -n1 #{to}`.chomp

time_delta = time_between_deploys(safe_name(base), safe_name(to))

commits = `git log --oneline #{range}`.split(/\n/)
merges = commits.grep(/Merges|\#\d+/)

shortstat = `git diff --shortstat --summary #{range}`.split(/\n/)
names_only = `git diff --name-only #{range}`
versioned_url = "#{gh_url}/blob/#{safe_name(to)}/"

dirstat = `git diff --dirstat=lines,cumulative #{range}` if dirstat
# TODO: investigate summarizing language / spec content based on file suffix,
# and possibly per PR, or classify frontend, backend, spec changes
stat = `git diff --stat #{range}` if stat

pull_requests = pull_requests(merges, gh_url)

puts "Deploy tag %s [%s]" % [to, revision]
if !commits.empty?
puts "%d pull requests of %d merges, %d commits %s" %
[pull_requests.count, merges.count, commits.count, time_delta]
puts shortstat.first.strip unless shortstat.empty?
puts COMPARE_FORMAT % [gh_url, reference(base), reference(to)]
puts
file_changes(ChangedFiles.new(names_only, versioned_url), base: base, to: to)
if pull_requests.any?
# FIXME: there may be commits in the deploy unassociated with a PR
puts "Pull Requests:", pull_requests
else
puts "Commits:", commits
end
puts "Dirstats:", dirstat if dirstat
puts "Stats:", stat if stat
else
puts "redeployed %s %s" % [base, time_delta]
end
puts DeployComplexity::Deploy.new(base, to, options).generate
puts
end

Expand All @@ -142,12 +15,11 @@ def deploy(base, to, options)
action = nil
options = {}

require 'optparse'
optparse = OptionParser.new do |opts|
optparse = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
opts.banner =
"Usage: %s [[base branch] deploy branch]" % [File.basename($PROGRAM_NAME)]
opts.on("-b", "--branch BRANCH", String, "Specify the base branch") do |e|
branch = safe_name(e) || branch
branch = DeployComplexity::Git.safe_name(e) || branch
end
opts.on("-d", "--deploys [N]", Integer,
"Show historical deploys, shows all if N is not specified") do |e|
Expand All @@ -167,6 +39,14 @@ def deploy(base, to, options)
"Github project url to construct links from") do |url|
options[:gh_url] = url
end
opts.on("--slack #foo,#bar", Array,
"Report changes to slack channels") do |channels|
if channels.any? && ENV['SLACK_WEBHOOK']
options[:slack_channels] = channels
else
STDERR.puts "Must specify slack channels & include SLACK_WEBHOOK in environment."
end
end
opts.on_tail("-v", "--version", "Show version info and exit") do
abort <<~BOILERPLATE
deploy-complexity.rb #{DeployComplexity::VERSION}
Expand Down Expand Up @@ -206,10 +86,3 @@ def deploy(base, to, options)
else
abort(optparse.to_s)
end

# rubocop:enable Style/FormatStringToken
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/BlockLength
92 changes: 47 additions & 45 deletions lib/deploy_complexity/changed_dependencies.rb
Original file line number Diff line number Diff line change
@@ -1,61 +1,63 @@
# frozen_string_literal: true

# Detects and formats changes in dependencies
# This is the parent class - each type of dependency file should implement
# it's own version of this. See changed_javascript_packages.rb for an example.
class ChangedDependencies
def initialize(file:, old:, new:)
@file = file
@old_dependencies = parse_dependencies(old)
@new_dependencies = parse_dependencies(new)
end
module DeployComplexity
# Detects and formats changes in dependencies
# This is the parent class - each type of dependency file should implement
# it's own version of this. See changed_javascript_packages.rb for an example.
class ChangedDependencies
def initialize(file:, old:, new:)
@file = file
@old_dependencies = parse_dependencies(old)
@new_dependencies = parse_dependencies(new)
end

def changes
[
format_dependencies("Added", added_dependencies),
format_dependencies("Removed", removed_dependencies),
format_updated_dependencies(updated_dependencies)
].flatten
end
def changes
[
format_dependencies("Added", added_dependencies),
format_dependencies("Removed", removed_dependencies),
format_updated_dependencies(updated_dependencies)
].flatten
end

private
private

# This should be implemented in the child classes
# @param [String] the dependency file to be parsed
# @return [ Object{String => String} ] map of the dependency name to the version
def parse_dependencies(_file)
{}
end
# This should be implemented in the child classes
# @param [String] the dependency file to be parsed
# @return [ Object{String => String} ] map of the dependency name to the version
def parse_dependencies(_file)
{}
end

def added_dependencies
@new_dependencies.dup.delete_if { |package, _| @old_dependencies.key?(package) }
end
def added_dependencies
@new_dependencies.dup.delete_if { |package, _| @old_dependencies.key?(package) }
end

def removed_dependencies
@old_dependencies.dup.delete_if { |package, _| @new_dependencies.key?(package) }
end
def removed_dependencies
@old_dependencies.dup.delete_if { |package, _| @new_dependencies.key?(package) }
end

def updated_dependencies
@new_dependencies.each_with_object({}) do |(package, new_version), changed_dependencies|
next if @old_dependencies[package].nil?
next if @old_dependencies[package] == new_version
def updated_dependencies
@new_dependencies.each_with_object({}) do |(package, new_version), changed_dependencies|
next if @old_dependencies[package].nil?
next if @old_dependencies[package] == new_version

changed_dependencies[package] = {
old: @old_dependencies[package],
new: new_version
}
changed_dependencies[package] = {
old: @old_dependencies[package],
new: new_version
}
end
end
end

def format_dependencies(label, dependencies)
dependencies.map do |(package, version)|
"#{label} #{package}: #{version} (#{@file})"
def format_dependencies(label, dependencies)
dependencies.map do |(package, version)|
"#{label} #{package}: #{version} (#{@file})"
end
end
end

def format_updated_dependencies(dependencies)
dependencies.map do |(package, versions)|
"Updated #{package}: #{versions.fetch(:old)} -> #{versions.fetch(:new)} (#{@file})"
def format_updated_dependencies(dependencies)
dependencies.map do |(package, versions)|
"Updated #{package}: #{versions.fetch(:old)} -> #{versions.fetch(:new)} (#{@file})"
end
end
end
end
Loading

0 comments on commit 8805ce6

Please sign in to comment.