emeril 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.cane +0 -0
- data/.gitignore +17 -0
- data/.tailor +106 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +3 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +261 -0
- data/Rakefile +33 -0
- data/emeril.gemspec +39 -0
- data/lib/emeril.rb +11 -0
- data/lib/emeril/category.rb +26 -0
- data/lib/emeril/git_tagger.rb +122 -0
- data/lib/emeril/logging.rb +17 -0
- data/lib/emeril/metadata_chopper.rb +38 -0
- data/lib/emeril/publisher.rb +130 -0
- data/lib/emeril/rake.rb +5 -0
- data/lib/emeril/rake_tasks.rb +41 -0
- data/lib/emeril/releaser.rb +80 -0
- data/lib/emeril/thor.rb +5 -0
- data/lib/emeril/thor_tasks.rb +45 -0
- data/lib/emeril/version.rb +6 -0
- data/spec/emeril/category_spec.rb +28 -0
- data/spec/emeril/git_tagger_spec.rb +147 -0
- data/spec/emeril/metadata_chopper_spec.rb +70 -0
- data/spec/emeril/publisher_spec.rb +223 -0
- data/spec/emeril/releaser_spec.rb +141 -0
- data/spec/fixtures/vcr_cassettes/known_cookbook.yml +46 -0
- data/spec/fixtures/vcr_cassettes/nonexistant_cookbook.yml +38 -0
- data/spec/spec_helper.rb +27 -0
- metadata +312 -0
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'cane/rake_task'
|
4
|
+
require 'tailor/rake_task'
|
5
|
+
|
6
|
+
Rake::TestTask.new(:unit) do |t|
|
7
|
+
t.libs.push "lib"
|
8
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
9
|
+
t.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Run all test suites"
|
13
|
+
task :test => [:unit]
|
14
|
+
|
15
|
+
desc "Run cane to check quality metrics"
|
16
|
+
Cane::RakeTask.new do |cane|
|
17
|
+
cane.canefile = './.cane'
|
18
|
+
end
|
19
|
+
|
20
|
+
Tailor::RakeTask.new
|
21
|
+
|
22
|
+
desc "Display LOC stats"
|
23
|
+
task :stats do
|
24
|
+
puts "\n## Production Code Stats"
|
25
|
+
sh "countloc -r lib"
|
26
|
+
puts "\n## Test Code Stats"
|
27
|
+
sh "countloc -r spec"
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Run all quality tasks"
|
31
|
+
task :quality => [:cane, :tailor, :stats]
|
32
|
+
|
33
|
+
task :default => [:test, :quality]
|
data/emeril.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'emeril/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "emeril"
|
8
|
+
spec.version = Emeril::VERSION
|
9
|
+
spec.authors = ["Fletcher Nichol"]
|
10
|
+
spec.email = ["fnichol@nichol.ca"]
|
11
|
+
spec.description = %q{Release Chef cookbooks}
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = "https://github.com/fnichol/emeril"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = []
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = '>= 1.9.2'
|
22
|
+
|
23
|
+
spec.add_dependency 'chef'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'minitest'
|
28
|
+
spec.add_development_dependency 'guard-minitest'
|
29
|
+
spec.add_development_dependency 'mocha'
|
30
|
+
spec.add_development_dependency 'fakefs'
|
31
|
+
spec.add_development_dependency 'vcr'
|
32
|
+
spec.add_development_dependency 'webmock'
|
33
|
+
|
34
|
+
spec.add_development_dependency 'cane'
|
35
|
+
spec.add_development_dependency 'guard-cane'
|
36
|
+
spec.add_development_dependency 'tailor'
|
37
|
+
spec.add_development_dependency 'simplecov'
|
38
|
+
spec.add_development_dependency 'countloc'
|
39
|
+
end
|
data/lib/emeril.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Emeril
|
7
|
+
|
8
|
+
# A category for a cookbook on the Community Site.
|
9
|
+
#
|
10
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
11
|
+
class Category
|
12
|
+
|
13
|
+
# Returns the category for the given cookbook on the Community Site or
|
14
|
+
# nil if it is not present.
|
15
|
+
#
|
16
|
+
# @param [String] cookbook a cookbook name
|
17
|
+
# @return [String,nil] the cookbook category or nil if it is not present
|
18
|
+
# on the Community site
|
19
|
+
#
|
20
|
+
def self.for_cookbook(cookbook)
|
21
|
+
path = "/api/v1/cookbooks/#{cookbook}"
|
22
|
+
response = Net::HTTP.get_response("cookbooks.opscode.com", path)
|
23
|
+
JSON.parse(response.body)['category']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'emeril/logging'
|
4
|
+
|
5
|
+
module Emeril
|
6
|
+
|
7
|
+
# Exception class raised when a git repo is not clean.
|
8
|
+
#
|
9
|
+
class GitNotCleanError < StandardError ; end
|
10
|
+
|
11
|
+
# Exception class raised when a git push does not return successfully.
|
12
|
+
#
|
13
|
+
class GitPushError < StandardError ; end
|
14
|
+
|
15
|
+
# Applies a version tag on a git repository and pushes it to the origin
|
16
|
+
# remote.
|
17
|
+
#
|
18
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
19
|
+
#
|
20
|
+
class GitTagger
|
21
|
+
|
22
|
+
include Logging
|
23
|
+
|
24
|
+
# Creates a new instance.
|
25
|
+
#
|
26
|
+
# @param [Hash] options configuration for a git tagger
|
27
|
+
# @option options [Logger] an optional logger instance
|
28
|
+
# @option options [String] source_path the path to a git repository
|
29
|
+
# @option options [String] tag_prefix a prefix for a git tag version string
|
30
|
+
# @option options [String] version (required) a version string
|
31
|
+
# @raise [ArgumentError] if any required options are not set
|
32
|
+
#
|
33
|
+
def initialize(options = {})
|
34
|
+
@logger = options[:logger]
|
35
|
+
@source_path = options.fetch(:source_path, Dir.pwd)
|
36
|
+
@tag_prefix = case options[:tag_prefix]
|
37
|
+
when nil then
|
38
|
+
DEFAULT_TAG_PREFIX
|
39
|
+
when false
|
40
|
+
""
|
41
|
+
else
|
42
|
+
options[:tag_prefix]
|
43
|
+
end
|
44
|
+
@version = options.fetch(:version) do
|
45
|
+
raise ArgumentError, ":version must be set"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Applies a version tag on a git repository and pushes it to the origin
|
50
|
+
# remote.
|
51
|
+
#
|
52
|
+
def run
|
53
|
+
guard_clean
|
54
|
+
tag_version { git_push } unless already_tagged?
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
DEFAULT_TAG_PREFIX = "v".freeze
|
60
|
+
|
61
|
+
attr_reader :logger, :source_path, :tag_prefix, :version
|
62
|
+
|
63
|
+
def already_tagged?
|
64
|
+
if sh_with_code('git tag')[0].split(/\n/).include?(version_tag)
|
65
|
+
info("Tag #{version_tag} has already been created.")
|
66
|
+
true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def clean?
|
71
|
+
sh_with_code("git status --porcelain")[0].empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
def git_push
|
75
|
+
perform_git_push
|
76
|
+
perform_git_push ' --tags'
|
77
|
+
info("Pushed git commits and tags.")
|
78
|
+
end
|
79
|
+
|
80
|
+
def guard_clean
|
81
|
+
clean? or raise GitNotCleanError,
|
82
|
+
"There are files that need to be committed first."
|
83
|
+
end
|
84
|
+
|
85
|
+
def perform_git_push(options = '')
|
86
|
+
cmd = "git push origin master #{options}"
|
87
|
+
out, code = sh_with_code(cmd)
|
88
|
+
if code != 0
|
89
|
+
raise GitPushError,
|
90
|
+
"Couldn't git push. `#{cmd}' failed with the following output:" +
|
91
|
+
"\n\n#{out}\n"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def sh_with_code(cmd, &block)
|
96
|
+
cmd << " 2>&1"
|
97
|
+
outbuf = ''
|
98
|
+
debug(cmd)
|
99
|
+
Dir.chdir(source_path) {
|
100
|
+
outbuf = `#{cmd}`
|
101
|
+
if $? == 0
|
102
|
+
block.call(outbuf) if block
|
103
|
+
end
|
104
|
+
}
|
105
|
+
[outbuf, $?]
|
106
|
+
end
|
107
|
+
|
108
|
+
def tag_version
|
109
|
+
sh_with_code(%{git tag -a -m \"Version #{version}\" #{version_tag}})
|
110
|
+
info("Tagged #{version_tag}.")
|
111
|
+
yield if block_given?
|
112
|
+
rescue
|
113
|
+
error("Untagging #{version_tag} due to error.")
|
114
|
+
sh_with_code("git tag -d #{version_tag}")
|
115
|
+
raise
|
116
|
+
end
|
117
|
+
|
118
|
+
def version_tag
|
119
|
+
"#{tag_prefix}#{version}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module Emeril
|
4
|
+
|
5
|
+
# A mixin providing log methods that gracefully fail if no logger is present.
|
6
|
+
#
|
7
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
8
|
+
#
|
9
|
+
module Logging
|
10
|
+
|
11
|
+
%w{debug info warn error fatal}.map(&:to_sym).each do |meth|
|
12
|
+
define_method(meth) do |*args|
|
13
|
+
logger && logger.public_send(meth, *args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module Emeril
|
4
|
+
|
5
|
+
# Exception class raised when there is a metadata.rb parsing issue.
|
6
|
+
#
|
7
|
+
class MetadataParseError < StandardError ; end
|
8
|
+
|
9
|
+
# A rather insane and questionable class to quickly consume a metadata.rb
|
10
|
+
# file and return the cookbook name and version attributes.
|
11
|
+
#
|
12
|
+
# @see https://twitter.com/fnichol/status/281650077901144064
|
13
|
+
# @see https://gist.github.com/4343327
|
14
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
15
|
+
#
|
16
|
+
class MetadataChopper < Hash
|
17
|
+
|
18
|
+
# Creates a new instances and loads in the contents of the metdata.rb
|
19
|
+
# file. If you value your life, you may want to avoid reading the
|
20
|
+
# implementation.
|
21
|
+
#
|
22
|
+
# @param metadata_file [String] path to a metadata.rb file
|
23
|
+
#
|
24
|
+
def initialize(metadata_file)
|
25
|
+
eval(IO.read(metadata_file), nil, metadata_file)
|
26
|
+
%w{name version}.map(&:to_sym).each do |attr|
|
27
|
+
if self[attr].nil?
|
28
|
+
raise MetadataParseError,
|
29
|
+
"Missing attribute `#{attr}' must be set in #{metadata_file}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing(meth, *args, &block)
|
35
|
+
self[meth] = args.first
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'chef/cookbook_uploader'
|
4
|
+
require 'chef/cookbook_loader'
|
5
|
+
require 'chef/cookbook_site_streaming_uploader'
|
6
|
+
require 'chef/knife/cookbook_site_share'
|
7
|
+
require 'chef/knife/core/ui'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'tmpdir'
|
10
|
+
|
11
|
+
require 'emeril/logging'
|
12
|
+
|
13
|
+
module Emeril
|
14
|
+
|
15
|
+
# Takes a path to a cookbook and pushes it up to the Community Site.
|
16
|
+
#
|
17
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
18
|
+
#
|
19
|
+
class Publisher
|
20
|
+
|
21
|
+
include Logging
|
22
|
+
|
23
|
+
# Creates a new instance.
|
24
|
+
#
|
25
|
+
# @param [Hash] options configuration for a publisher
|
26
|
+
# @option options [Logger] an optional logger instance
|
27
|
+
# @option options [String] source_path the path to a git repository
|
28
|
+
# @option options [String] name (required) the name of the cookbook
|
29
|
+
# @option options [String] category a Community Site category for the
|
30
|
+
# cookbook
|
31
|
+
# @option options [Chef::Knife] knife_class an alternate Knife plugin class
|
32
|
+
# to create, configure, and invoke
|
33
|
+
# @raise [ArgumentError] if any required options are not set
|
34
|
+
#
|
35
|
+
def initialize(options = {})
|
36
|
+
@logger = options[:logger]
|
37
|
+
@source_path = options.fetch(:source_path, Dir.pwd)
|
38
|
+
@name = options.fetch(:name) { raise ArgumentError, ":name must be set" }
|
39
|
+
@category = options[:category]
|
40
|
+
@knife_class = options.fetch(:knife_class, SharePlugin)
|
41
|
+
validate_chef_config!
|
42
|
+
end
|
43
|
+
|
44
|
+
# Prepares a sandbox copy of the cookbook and uploads it to the Community
|
45
|
+
# Site.
|
46
|
+
#
|
47
|
+
def run
|
48
|
+
sandbox_path = sandbox_cookbook
|
49
|
+
share = knife_class.new
|
50
|
+
share.ui = logging_ui(share.ui)
|
51
|
+
share.config[:cookbook_path] = sandbox_path
|
52
|
+
share.name_args = [name, category]
|
53
|
+
share.run
|
54
|
+
ensure
|
55
|
+
FileUtils.remove_dir(sandbox_path)
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
attr_reader :logger, :source_path, :name, :category, :knife_class
|
61
|
+
|
62
|
+
def validate_chef_config!
|
63
|
+
%w{node_name client_key}.map(&:to_sym).each do |attr|
|
64
|
+
raise "Chef::Config[:#{attr}] must be set" if ::Chef::Config[attr].nil?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def sandbox_cookbook
|
69
|
+
path = Dir.mktmpdir
|
70
|
+
target = File.join(path, name)
|
71
|
+
debug("Creating cookbook sanbox directory at #{target}")
|
72
|
+
FileUtils.mkdir_p(target)
|
73
|
+
FileUtils.cp_r(cookbook_files, target)
|
74
|
+
path
|
75
|
+
end
|
76
|
+
|
77
|
+
def cookbook_files
|
78
|
+
entries = %w{
|
79
|
+
README.* CHANGELOG.* metadata.{json,rb}
|
80
|
+
attributes files libraries providers recipes resources templates
|
81
|
+
}
|
82
|
+
|
83
|
+
Dir.glob("#{source_path}/{#{entries.join(',')}}")
|
84
|
+
end
|
85
|
+
|
86
|
+
def logging_ui(ui)
|
87
|
+
LoggingUI.new(ui.stdout, ui.stderr, ui.stdin, ui.config, logger)
|
88
|
+
end
|
89
|
+
|
90
|
+
# A custom knife UI that sends logging methods to a logger, if it exists.
|
91
|
+
#
|
92
|
+
class LoggingUI < :: Chef::Knife::UI
|
93
|
+
|
94
|
+
def initialize(stdout, stderr, stdin, config, logger)
|
95
|
+
super(stdout, stderr, stdin, config)
|
96
|
+
@logger = logger
|
97
|
+
end
|
98
|
+
|
99
|
+
def msg(message)
|
100
|
+
logger ? logger.info(message) : super
|
101
|
+
end
|
102
|
+
|
103
|
+
def err(message)
|
104
|
+
logger ? logger.error(message) : super
|
105
|
+
end
|
106
|
+
|
107
|
+
def warn(message)
|
108
|
+
logger ? logger.warn(message) : super
|
109
|
+
end
|
110
|
+
|
111
|
+
def fatal(message)
|
112
|
+
logger ? logger.fatal(message) : super
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
attr_reader :logger
|
118
|
+
end
|
119
|
+
|
120
|
+
# A custom cookbook site share knife plugin that intercepts Kernel#exit
|
121
|
+
# calls and converts them to an exception raise.
|
122
|
+
#
|
123
|
+
class SharePlugin < ::Chef::Knife::CookbookSiteShare
|
124
|
+
|
125
|
+
def exit(code)
|
126
|
+
raise "Knife Plugin exited with error code: #{code}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/emeril/rake.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'rake/tasklib'
|
4
|
+
require 'chef/knife'
|
5
|
+
|
6
|
+
require 'emeril'
|
7
|
+
|
8
|
+
module Emeril
|
9
|
+
|
10
|
+
# Emeril Rake task generator.
|
11
|
+
#
|
12
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
13
|
+
#
|
14
|
+
class RakeTasks < ::Rake::TaskLib
|
15
|
+
|
16
|
+
attr_accessor :config
|
17
|
+
|
18
|
+
# Creates Emeril Rake tasks and allows the callee to configure it.
|
19
|
+
#
|
20
|
+
# @yield [self] gives itself to the block
|
21
|
+
#
|
22
|
+
def initialize
|
23
|
+
@config = { :logger => Chef::Log }
|
24
|
+
yield self if block_given?
|
25
|
+
define
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def define
|
31
|
+
metadata = Emeril::MetadataChopper.new("metadata.rb")
|
32
|
+
|
33
|
+
desc "Create git tag for #{metadata[:name]}-#{metadata[:version]}" +
|
34
|
+
" and push to the Community Site"
|
35
|
+
task "release" do
|
36
|
+
Chef::Knife.new.configure_chef
|
37
|
+
Emeril::Releaser.new(@config).run
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|