stove 1.1.2 → 2.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|