stove 1.1.2 → 2.0.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/CHANGELOG.md +16 -0
- data/README.md +41 -29
- data/Rakefile +15 -0
- data/bin/bake +1 -2
- data/features/actions/bump.feature +22 -0
- data/features/actions/changelog.feature +45 -0
- data/features/actions/dev.feature +18 -0
- data/features/actions/upload.feature +48 -0
- data/features/plugins/git.feature +24 -0
- data/features/rake.feature +1 -2
- data/features/step_definitions/cli_steps.rb +1 -27
- data/features/step_definitions/{community_site_steps.rb → community_steps.rb} +9 -5
- data/features/step_definitions/config_steps.rb +24 -0
- data/features/step_definitions/cookbook_steps.rb +28 -6
- data/features/step_definitions/cucumber_steps.rb +12 -0
- data/features/step_definitions/git_steps.rb +10 -7
- data/features/support/env.rb +12 -28
- data/features/support/stove/git.rb +48 -0
- data/lib/stove.rb +102 -19
- data/lib/stove/actions/base.rb +21 -0
- data/lib/stove/actions/bump.rb +25 -0
- data/lib/stove/actions/changelog.rb +71 -0
- data/lib/stove/actions/dev.rb +22 -0
- data/lib/stove/actions/finish.rb +8 -0
- data/lib/stove/actions/start.rb +7 -0
- data/lib/stove/actions/upload.rb +27 -0
- data/lib/stove/cli.rb +107 -79
- data/lib/stove/community.rb +124 -0
- data/lib/stove/config.rb +62 -13
- data/lib/stove/cookbook.rb +76 -238
- data/lib/stove/cookbook/metadata.rb +16 -11
- data/lib/stove/error.rb +13 -107
- data/lib/stove/filter.rb +59 -0
- data/lib/stove/jira.rb +74 -30
- data/lib/stove/middlewares/chef_authentication.rb +60 -0
- data/lib/stove/middlewares/exceptions.rb +17 -0
- data/lib/stove/mixins/filterable.rb +11 -0
- data/lib/stove/mixins/insideable.rb +13 -0
- data/lib/stove/mixins/instanceable.rb +23 -0
- data/lib/stove/mixins/loggable.rb +32 -0
- data/lib/stove/mixins/optionable.rb +41 -0
- data/lib/stove/mixins/validatable.rb +7 -0
- data/lib/stove/packager.rb +23 -22
- data/lib/stove/plugins/base.rb +35 -0
- data/lib/stove/plugins/git.rb +71 -0
- data/lib/stove/plugins/github.rb +108 -0
- data/lib/stove/plugins/jira.rb +72 -0
- data/lib/stove/rake_task.rb +56 -37
- data/lib/stove/runner.rb +84 -0
- data/lib/stove/util.rb +56 -0
- data/lib/stove/validator.rb +67 -0
- data/lib/stove/version.rb +1 -1
- data/locales/en.yml +231 -0
- data/stove.gemspec +11 -11
- metadata +85 -67
- data/features/changelog.feature +0 -22
- data/features/cli.feature +0 -11
- data/features/devodd.feature +0 -19
- data/features/git.feature +0 -34
- data/features/upload.feature +0 -40
- data/lib/stove/community_site.rb +0 -85
- data/lib/stove/formatter.rb +0 -7
- data/lib/stove/formatter/base.rb +0 -32
- data/lib/stove/formatter/human.rb +0 -9
- data/lib/stove/formatter/silent.rb +0 -10
- data/lib/stove/git.rb +0 -82
- data/lib/stove/github.rb +0 -43
- data/lib/stove/logger.rb +0 -56
- data/lib/stove/uploader.rb +0 -64
- data/spec/support/community_site.rb +0 -33
- data/spec/support/git.rb +0 -52
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Stove
|
4
|
+
module Mixin::Instanceable
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:include, Singleton)
|
7
|
+
base.send(:undef_method, :inspect, :to_s)
|
8
|
+
base.send(:extend, ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.extended(base)
|
12
|
+
base.send(:include, Singleton)
|
13
|
+
base.send(:undef_method, :inspect, :to_s)
|
14
|
+
base.send(:extend, ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def method_missing(m, *args, &block)
|
19
|
+
instance.send(m, *args, &block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'log4r'
|
2
|
+
|
3
|
+
module Stove
|
4
|
+
module Mixin::Loggable
|
5
|
+
def self.extended(base)
|
6
|
+
base.send(:include, InstanceMethods)
|
7
|
+
base.send(:extend, ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.send(:include, InstanceMethods)
|
12
|
+
base.send(:extend, ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def log
|
17
|
+
return @log if @log
|
18
|
+
|
19
|
+
@log = Log4r::Logger.new(self.name)
|
20
|
+
@log.outputters = Log4r::Outputter.stdout
|
21
|
+
@log.level = 1
|
22
|
+
@log
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module InstanceMethods
|
27
|
+
def log
|
28
|
+
self.class.log
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Stove
|
2
|
+
module Mixin::Optionable
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:extend, ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.extended(base)
|
8
|
+
base.send(:extend, ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
#
|
13
|
+
# This is a magical method. It does three things:
|
14
|
+
#
|
15
|
+
# 1. Defines a class method getter and setter for the given option
|
16
|
+
# 2. Defines an instance method that delegates to the class method
|
17
|
+
# 3. (Optionally) sets the initial value
|
18
|
+
#
|
19
|
+
# @param [String, Symbol] name
|
20
|
+
# the name of the option
|
21
|
+
# @param [Object] initial
|
22
|
+
# the initial value to set (optional)
|
23
|
+
#
|
24
|
+
def option(name, initial = UNSET_VALUE)
|
25
|
+
define_singleton_method(name) do |value = UNSET_VALUE|
|
26
|
+
if value == UNSET_VALUE
|
27
|
+
instance_variable_get("@#{name}")
|
28
|
+
else
|
29
|
+
instance_variable_set("@#{name}", value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
define_method(name) { self.class.send(name) }
|
34
|
+
|
35
|
+
unless initial == UNSET_VALUE
|
36
|
+
send(name, initial)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/stove/packager.rb
CHANGED
@@ -49,34 +49,35 @@ module Stove
|
|
49
49
|
end
|
50
50
|
|
51
51
|
private
|
52
|
-
def pack!
|
53
|
-
destination = Tempfile.new(cookbook.name).path
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
FileUtils.mkdir_p(sandbox)
|
53
|
+
def pack!
|
54
|
+
destination = Tempfile.new(cookbook.name).path
|
58
55
|
|
59
|
-
|
60
|
-
|
61
|
-
|
56
|
+
# Sandbox
|
57
|
+
sandbox = Dir.mktmpdir
|
58
|
+
FileUtils.mkdir_p(sandbox)
|
62
59
|
|
63
|
-
|
64
|
-
|
60
|
+
# Containing folder
|
61
|
+
container = File.join(sandbox, cookbook.name)
|
62
|
+
FileUtils.mkdir_p(container)
|
65
63
|
|
66
|
-
|
67
|
-
|
68
|
-
f.write(cookbook.metadata.to_json)
|
69
|
-
end
|
64
|
+
# Copy filles
|
65
|
+
FileUtils.cp_r(cookbook_files, container)
|
70
66
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
tgz = Zlib::GzipWriter.new(File.open(destination, 'wb'))
|
76
|
-
Archive::Tar::Minitar.pack(relative_path, tgz)
|
77
|
-
end
|
67
|
+
# Generate metadata
|
68
|
+
File.open(File.join(container, 'metadata.json'), 'w') do |f|
|
69
|
+
f.write(cookbook.metadata.to_json)
|
70
|
+
end
|
78
71
|
|
79
|
-
|
72
|
+
Dir.chdir(sandbox) do |dir|
|
73
|
+
# This is super fucking annoying. The community site should really
|
74
|
+
# be better at reading tarballs
|
75
|
+
relative_path = container.gsub(sandbox + '/', '') + '/'
|
76
|
+
tgz = Zlib::GzipWriter.new(File.open(destination, 'wb'))
|
77
|
+
Archive::Tar::Minitar.pack(relative_path, tgz)
|
80
78
|
end
|
79
|
+
|
80
|
+
return destination
|
81
|
+
end
|
81
82
|
end
|
82
83
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Stove
|
2
|
+
class Plugin::Base
|
3
|
+
extend Mixin::Filterable
|
4
|
+
extend Mixin::Loggable
|
5
|
+
extend Mixin::Optionable
|
6
|
+
extend Mixin::Validatable
|
7
|
+
|
8
|
+
option :id
|
9
|
+
option :description
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def onload(&block)
|
13
|
+
if block
|
14
|
+
@onload = block
|
15
|
+
else
|
16
|
+
@onload
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :cookbook
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
def initialize(cookbook, options = {})
|
25
|
+
@cookbook, @options = cookbook, options
|
26
|
+
instance_eval(&onload)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def onload
|
32
|
+
self.class.onload || Proc.new {}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Stove
|
2
|
+
class Plugin::Git < Plugin::Base
|
3
|
+
id 'git'
|
4
|
+
description 'Tag and push to a git remote'
|
5
|
+
|
6
|
+
validate(:repository) do
|
7
|
+
File.directory?(File.join(Dir.pwd, '.git'))
|
8
|
+
end
|
9
|
+
|
10
|
+
validate(:clean) do
|
11
|
+
git_null('status -s').strip.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
validate(:up_to_date) do
|
15
|
+
git_null('fetch')
|
16
|
+
local = git_null("rev-parse #{options[:branch]}").strip
|
17
|
+
remote = git_null("rev-parse #{options[:remote]}/#{options[:branch]}").strip
|
18
|
+
|
19
|
+
log.debug("Local SHA: #{local}")
|
20
|
+
log.debug("Remote SHA: #{remote}")
|
21
|
+
|
22
|
+
local == remote
|
23
|
+
end
|
24
|
+
|
25
|
+
after(:bump, 'Performing version bump') do
|
26
|
+
git %|add metadata.rb|
|
27
|
+
git %|commit -m "Version bump to #{cookbook.version}"|
|
28
|
+
end
|
29
|
+
|
30
|
+
after(:changelog, 'Committing CHANGELOG') do
|
31
|
+
git %|add CHANGELOG.md|
|
32
|
+
git %|commit -m "Publish #{cookbook.version} Changelog"|
|
33
|
+
end
|
34
|
+
|
35
|
+
before(:upload, 'Tagging new release') do
|
36
|
+
git %|tag #{cookbook.tag_version}|
|
37
|
+
git %|push #{options[:remote]} #{cookbook.tag_version}|
|
38
|
+
end
|
39
|
+
|
40
|
+
after(:dev, 'Bumping devodd release') do
|
41
|
+
git %|add metadata.rb|
|
42
|
+
git %|commit -m "Version bump to #{cookbook.version} (for development)"|
|
43
|
+
end
|
44
|
+
|
45
|
+
before(:finish, 'Pushing to git remote(s)') do
|
46
|
+
git %|push #{options[:remote]} #{options[:branch]}|
|
47
|
+
end
|
48
|
+
|
49
|
+
def git(command, errors = true)
|
50
|
+
log.debug("Running `git #{command}', errors: #{errors}")
|
51
|
+
response = %x|cd "#{cookbook.path}" && git #{command}|
|
52
|
+
|
53
|
+
if errors && !$?.success?
|
54
|
+
raise Error::GitFailed.new(command: command)
|
55
|
+
end
|
56
|
+
|
57
|
+
response
|
58
|
+
end
|
59
|
+
|
60
|
+
def git_null(command)
|
61
|
+
null = case RbConfig::CONFIG['host_os']
|
62
|
+
when /mswin|mingw|cygwin/
|
63
|
+
'NUL'
|
64
|
+
else
|
65
|
+
'/dev/null'
|
66
|
+
end
|
67
|
+
|
68
|
+
git("#{command} 2>#{null}", false)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Stove
|
2
|
+
class Plugin::GitHub < Plugin::Base
|
3
|
+
id 'github'
|
4
|
+
description 'Publish the release to GitHub'
|
5
|
+
|
6
|
+
onload do
|
7
|
+
require 'faraday'
|
8
|
+
require 'faraday_middleware'
|
9
|
+
require 'octokit'
|
10
|
+
end
|
11
|
+
|
12
|
+
validate(:git) do
|
13
|
+
options[:git]
|
14
|
+
end
|
15
|
+
|
16
|
+
validate(:configuration) do
|
17
|
+
Config.has_key?(:github)
|
18
|
+
end
|
19
|
+
|
20
|
+
validate(:access_token) do
|
21
|
+
Config[:github].has_key?(:access_token)
|
22
|
+
end
|
23
|
+
|
24
|
+
after(:upload, 'Publishing the release to GitHub') do
|
25
|
+
release = client.create_release(repository, cookbook.tag_version,
|
26
|
+
name: cookbook.tag_version,
|
27
|
+
body: cookbook.changeset,
|
28
|
+
)
|
29
|
+
asset = client.upload_asset("repos/#{repository}/releases/#{release.id}", cookbook.tarball,
|
30
|
+
content_type: 'application/x-gzip',
|
31
|
+
name: filename,
|
32
|
+
)
|
33
|
+
client.update_release_asset("repos/#{repository}/releases/assets/#{asset.id}",
|
34
|
+
name: filename,
|
35
|
+
label: 'Download Cookbook',
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def client
|
40
|
+
return @client if @client
|
41
|
+
|
42
|
+
config = {}.tap do |h|
|
43
|
+
h[:middleware] = middleware
|
44
|
+
h[:access_token] = Config[:github][:access_token]
|
45
|
+
h[:api_endpoint] = Config[:github][:api_endpoint] if Config[:github][:api_endpoint]
|
46
|
+
end
|
47
|
+
|
48
|
+
@client = Octokit::Client.new(config)
|
49
|
+
@client
|
50
|
+
end
|
51
|
+
|
52
|
+
def changeset
|
53
|
+
@changeset ||= cookbook.changeset.split("\n")[2..-1].join("\n").strip
|
54
|
+
end
|
55
|
+
|
56
|
+
def repository
|
57
|
+
@repository ||= Octokit::Repository.from_url(repo_url)
|
58
|
+
end
|
59
|
+
|
60
|
+
def filename
|
61
|
+
@filename ||= "#{cookbook.name}-#{cookbook.version}.tar.gz"
|
62
|
+
end
|
63
|
+
|
64
|
+
def middleware
|
65
|
+
Faraday::Builder.new do |builder|
|
66
|
+
# Handle any common errors
|
67
|
+
builder.use Stove::Middleware::Exceptions
|
68
|
+
builder.use Octokit::Response::RaiseError
|
69
|
+
|
70
|
+
# Log all requests and responses (useful for development)
|
71
|
+
builder.response :logger, log
|
72
|
+
|
73
|
+
# Raise errors on 40x and 50x responses
|
74
|
+
builder.response :raise_error
|
75
|
+
|
76
|
+
# Use the default adapter (Net::HTTP)
|
77
|
+
builder.adapter :net_http
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# The URL for this repository on GitHub. This method automatically
|
83
|
+
# translates SSH and git:// URLs to https:// URLs.
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
#
|
87
|
+
def repo_url
|
88
|
+
return @repo_url if @repo_url
|
89
|
+
|
90
|
+
path = File.join('.git', 'config')
|
91
|
+
log.debug("Calculating repo_url from `#{path}'")
|
92
|
+
|
93
|
+
config = File.read(path)
|
94
|
+
log.debug("Config contents:\n#{config}")
|
95
|
+
|
96
|
+
config =~ /\[remote "#{options[:remote]}"\]\n\s+url = (.+)$/
|
97
|
+
log.debug("Match: #{$1.inspect}")
|
98
|
+
|
99
|
+
@repo_url = $1.to_s
|
100
|
+
.strip
|
101
|
+
.gsub(/\.git$/, '')
|
102
|
+
.gsub(':', '/')
|
103
|
+
.gsub('@', '://')
|
104
|
+
.gsub('git://', 'https://')
|
105
|
+
@repo_url
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Stove
|
2
|
+
class Plugin::JIRA < Plugin::Base
|
3
|
+
id 'jira'
|
4
|
+
description 'Resolve JIRA issues'
|
5
|
+
|
6
|
+
validate(:configuration) do
|
7
|
+
Config.has_key?(:jira)
|
8
|
+
end
|
9
|
+
|
10
|
+
validate(:username) do
|
11
|
+
Config[:jira].has_key?(:username)
|
12
|
+
end
|
13
|
+
|
14
|
+
validate(:password) do
|
15
|
+
Config[:jira].has_key?(:password)
|
16
|
+
end
|
17
|
+
|
18
|
+
before(:changelog, 'Generate JIRA changeset') do
|
19
|
+
by_type = unreleased_issues.inject({}) do |hash, issue|
|
20
|
+
type = issue['fields']['issuetype']['name']
|
21
|
+
hash[type] ||= []
|
22
|
+
hash[type] << {
|
23
|
+
key: issue['key'],
|
24
|
+
summary: issue['fields']['summary'],
|
25
|
+
}
|
26
|
+
|
27
|
+
hash
|
28
|
+
end
|
29
|
+
|
30
|
+
# Calculate the JIRA path based off of the JIRA base_url
|
31
|
+
jira_base = URI.parse(JIRA.base_url)
|
32
|
+
jira_base.path = ''
|
33
|
+
jira_base = jira_base.to_s
|
34
|
+
log.debug("JIRA base is `#{jira_base}'")
|
35
|
+
|
36
|
+
contents = []
|
37
|
+
|
38
|
+
by_type.each do |type, issues|
|
39
|
+
contents << "### #{type}"
|
40
|
+
issues.sort { |a, b| b[:key].to_i <=> a[:key].to_i }.each do |issue|
|
41
|
+
url = "#{jira_base}/browse/#{issue[:key]}"
|
42
|
+
contents << "- **[#{issue[:key]}](#{url})** - #{issue[:summary]}"
|
43
|
+
end
|
44
|
+
contents << ''
|
45
|
+
end
|
46
|
+
|
47
|
+
cookbook.changeset = contents.join("\n")
|
48
|
+
end
|
49
|
+
|
50
|
+
after(:upload, 'Resolving JIRA issues') do
|
51
|
+
unreleased_issues.collect do |issue|
|
52
|
+
Thread.new do
|
53
|
+
JIRA.close_and_comment(issue['key'], "Released in #{cookbook.version}")
|
54
|
+
end
|
55
|
+
end.map(&:join)
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# The list of unreleased tickets on JIRA.
|
60
|
+
#
|
61
|
+
# @return [Array<Hash>]
|
62
|
+
#
|
63
|
+
def unreleased_issues
|
64
|
+
@unreleased_issues ||= JIRA.search(
|
65
|
+
project: 'COOK',
|
66
|
+
resolution: 'Fixed',
|
67
|
+
status: 'Fix Committed',
|
68
|
+
component: cookbook.name,
|
69
|
+
)['issues']
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|