Skip to content

Commit

Permalink
Merge pull request #1 from mattlqx/cookbook-metadata
Browse files Browse the repository at this point in the history
new hook for checking chef cookbook metadata versions
  • Loading branch information
mattlqx authored Mar 21, 2019
2 parents f8dcb87 + 527f22d commit 2ecf67f
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@
)$
exclude: .*/test/.*\.rb$
verbose: true
- id: chef-cookbook-version
name: Ensure Chef cookbook version bump
description: Ensure Chef cookbook versions are bumped when contents are changed
entry: bin/cookbook-wrapper.rb
language: script
pass_filenames: true
types: ['file']
verbose: true
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ To lint Ruby changes in your repo, use the `rubocop` hook. The root of your repo

To lint Chef changes in your repo, use the `foodcritic` hook. The root of your repo must have a `Gemfile` that includes the desired version of foodcritic. It will be installed via Bundler prior to linting. Foodcritic will only be run against cookbooks with changes to Chef code; this does not include the libraries directory of a cookbook.

To check Chef cookbook version bumps, use the `chef-cookbook-version` hook. Each changed cookbook will have its `metadata.rb` or `metadata.json` checked to determine if its version has been increased. If not, it will automatically be fixed with the patch-level incremented.

To unit test Ruby changes in your repo, use the `rspec` hook. Each path in your repo with a `spec` directory should have a `Gemfile` that includes your desired version of rspec (or a derivative library). It will be installed via Bundler prior to testing. Rspec will only be run against the closest directory in a changed file's path with a spec dir.

- repo: https://github.com/mattlqx/pre-commit-ruby
rev: v1.1.0
rev: v1.2.0
hooks:
- id: rubocop
- id: foodcritic
- id: rspec
- id: chef-cookbook-version
132 changes: 132 additions & 0 deletions bin/cookbook-wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'fileutils'
require 'json'

def metadata_walk(path)
dir = File.directory?(path) ? path : File.dirname(path)
if File.exist?(File.join(dir, 'metadata.rb')) || File.exist?(File.join(dir, 'metadata.json'))
[dir, File.exist?(File.join(dir, 'metadata.json')) ? 'json' : 'rb']
elsif ['.', '/'].include?(dir)
false
else
metadata_walk(File.dirname(dir))
end
end

def bump_required?(file)
%w[
metadata.(rb|json)
Berksfile
Berksfile.lock
Policyfile.rb
Policyfile.lock.json
recipes/.*
attributes/.*
libraries/.*
files/.*
templates/.*
].each do |regex|
break true if file.match?("#{regex}$")
end == true
end

def higher?(old_version, new_version)
old_version = old_version.split('.')
new_version.split('.').each_with_index do |n, idx|
break true if n > old_version[idx]
end == true
end

# Simple metadata.rb reader
class MetadataReader
attr_reader :data
attr_reader :raw

def initialize(metadata, path)
@data = {}
@raw = metadata
instance_eval(metadata, path)
end

def [](key)
@data[key.to_sym]
end

def empty?
@data.empty?
end

def method_missing(sym, *args, &_block) # rubocop:disable Metrics/AbcSize, Style/MethodMissingSuper, Style/MissingRespondToMissing, Metrics/LineLength
return @data[sym] if args.empty?

if args.length > 1
@data[sym] ||= []
@data[sym] << args
elsif @data.key?(sym)
@data[sym] = [@data[sym]] unless @data[sym].is_a?(Array)
@data[sym] += args
else
@data[sym] = args.length == 1 ? args[0] : args
end
end
end

# Check each changed file for a metadata file in its heirarchy
success = true
autofix = false
changed_cookbooks = []
ARGV.each do |file|
cookbook, type = metadata_walk(file)
next if changed_cookbooks.include?(cookbook)
changed_cookbooks << [cookbook, type] if bump_required?(file)
end

exit(success) if changed_cookbooks.empty?

IO.popen('git fetch origin') { |_f| true }
branch = IO.popen('git symbolic-ref --short HEAD', &:read).chomp
changed_cookbooks.each do |cb_data|
cookbook = cb_data[0]
type = cb_data[1]
prefix = cookbook.start_with?('/') ? '' : './'
git_cmd = "git show origin/#{branch}:#{prefix}#{cookbook}/metadata.#{type}"

if type == 'rb'
old_metadata = MetadataReader.new(IO.popen(git_cmd, err: :close, &:read), "#{cookbook}/metadata.rb")
new_metadata = MetadataReader.new(IO.read("#{cookbook}/metadata.rb"), "#{cookbook}/metadata.rb")
else
old_metadata = JSON.parse(IO.popen(git_cmd, err: :close, &:read)) rescue {} # rubocop:disable Style/RescueModifier
new_metadata = JSON.parse(IO.read("#{cookbook}/metadata.json"))
end

if old_metadata.empty?
puts "#{cookbook} does not have a metadata file in git history. Skipping."
elsif new_metadata['version'] == old_metadata['version']
puts "#{cookbook} has changed and has not been version bumped."
success = false
autofix = true
elsif !higher?(old_metadata['version'], new_metadata['version'])
puts "#{cookbook} version is not higher than in branch on origin. Please fix manually."
success = false
autofix = false
end
next unless autofix

bumped_version = new_metadata['version'].split('.').map(&:to_i)
bumped_version[-1] += 1
bumped_version = bumped_version.join('.')
puts "Auto-fixing #{cookbook} version to next patch-level. (#{bumped_version})"
if type == 'rb'
new_metadata_content = new_metadata.raw.sub(
/^(\s+)*version(\s+)(['"])[0-9.]+['"](.*)$/, "\\1version\\2\\3#{bumped_version}\\3\\4"
)
IO.write("#{cookbook}/metadata.rb", new_metadata_content)
else
new_metadata['version'] = bumped_version
IO.write("#{cookbook}/metadata.json", JSON.dump(new_metadata))
end
end

exit(success)

0 comments on commit 2ecf67f

Please sign in to comment.