stove 1.0.0
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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +5 -0
- data/Gemfile +2 -0
- data/LICENSE +202 -0
- data/README.md +117 -0
- data/Rakefile +1 -0
- data/bin/bake +5 -0
- data/features/changelog.feature +26 -0
- data/features/cli.feature +15 -0
- data/features/git.feature +32 -0
- data/features/step_definitions/cli_steps.rb +23 -0
- data/features/step_definitions/community_site_steps.rb +25 -0
- data/features/step_definitions/cookbook_steps.rb +25 -0
- data/features/step_definitions/git_steps.rb +19 -0
- data/features/support/env.rb +29 -0
- data/features/upload.feature +44 -0
- data/lib/stove.rb +26 -0
- data/lib/stove/cli.rb +131 -0
- data/lib/stove/community_site.rb +85 -0
- data/lib/stove/config.rb +18 -0
- data/lib/stove/cookbook.rb +280 -0
- data/lib/stove/cookbook/metadata.rb +190 -0
- data/lib/stove/error.rb +106 -0
- data/lib/stove/formatter.rb +7 -0
- data/lib/stove/formatter/base.rb +32 -0
- data/lib/stove/formatter/human.rb +9 -0
- data/lib/stove/formatter/silent.rb +10 -0
- data/lib/stove/git.rb +74 -0
- data/lib/stove/jira.rb +44 -0
- data/lib/stove/logger.rb +35 -0
- data/lib/stove/mash.rb +25 -0
- data/lib/stove/packager.rb +82 -0
- data/lib/stove/uploader.rb +73 -0
- data/lib/stove/version.rb +3 -0
- data/spec/support/community_site.rb +33 -0
- data/spec/support/git.rb +51 -0
- data/stove.gemspec +34 -0
- metadata +251 -0
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'solve'
|
3
|
+
|
4
|
+
module Stove
|
5
|
+
class Cookbook
|
6
|
+
# Borrowed and modified from:
|
7
|
+
# {https://raw.github.com/opscode/chef/11.4.0/lib/chef/cookbook/metadata.rb}
|
8
|
+
#
|
9
|
+
# Copyright:: Copyright 2008-2010 Opscode, Inc.
|
10
|
+
#
|
11
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
12
|
+
# you may not use this file except in compliance with the License.
|
13
|
+
# You may obtain a copy of the License at
|
14
|
+
#
|
15
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
16
|
+
#
|
17
|
+
# Unless required by applicable law or agreed to in writing, software
|
18
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
19
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
20
|
+
# See the License for the specific language governing permissions and
|
21
|
+
# limitations under the License.
|
22
|
+
#
|
23
|
+
# == Chef::Cookbook::Metadata
|
24
|
+
# Chef::Cookbook::Metadata provides a convenient DSL for declaring metadata
|
25
|
+
# about Chef Cookbooks.
|
26
|
+
class Metadata
|
27
|
+
class << self
|
28
|
+
def from_file(path)
|
29
|
+
new.from_file(path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def def_attribute(field)
|
33
|
+
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
34
|
+
def #{field}(arg = nil)
|
35
|
+
set_or_return(:#{field}, arg)
|
36
|
+
end
|
37
|
+
EOM
|
38
|
+
end
|
39
|
+
|
40
|
+
def def_meta_cookbook(field, instance_variable)
|
41
|
+
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
42
|
+
def #{field}(thing, *args)
|
43
|
+
version = args.first
|
44
|
+
@#{instance_variable}[thing] = Solve::Constraint.new(version).to_s
|
45
|
+
@#{instance_variable}[thing]
|
46
|
+
end
|
47
|
+
EOM
|
48
|
+
end
|
49
|
+
|
50
|
+
def def_meta_setter(field, instance_variable)
|
51
|
+
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
52
|
+
def #{field}(name, description)
|
53
|
+
@#{instance_variable}[name] = description
|
54
|
+
@#{instance_variable}
|
55
|
+
end
|
56
|
+
EOM
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
COMPARISON_FIELDS = [
|
61
|
+
:name, :description, :long_description, :maintainer,
|
62
|
+
:maintainer_email, :license, :platforms, :dependencies,
|
63
|
+
:recommendations, :suggestions, :conflicting, :providing,
|
64
|
+
:replacing, :attributes, :groupings, :recipes, :version
|
65
|
+
]
|
66
|
+
|
67
|
+
def_attribute :name
|
68
|
+
def_attribute :maintainer
|
69
|
+
def_attribute :maintainer_email
|
70
|
+
def_attribute :license
|
71
|
+
def_attribute :description
|
72
|
+
def_attribute :long_description
|
73
|
+
|
74
|
+
def_meta_cookbook :supports, :platforms
|
75
|
+
def_meta_cookbook :depends, :dependencies
|
76
|
+
def_meta_cookbook :recommends, :recommendations
|
77
|
+
def_meta_cookbook :suggests, :suggestions
|
78
|
+
def_meta_cookbook :conflicts, :conflicting
|
79
|
+
def_meta_cookbook :provides, :providing
|
80
|
+
def_meta_cookbook :replaces, :replacing
|
81
|
+
|
82
|
+
def_meta_setter :recipe, :recipes
|
83
|
+
def_meta_setter :grouping, :groupings
|
84
|
+
def_meta_setter :attribute, :attributes
|
85
|
+
|
86
|
+
attr_reader :cookbook
|
87
|
+
attr_reader :platforms
|
88
|
+
attr_reader :dependencies
|
89
|
+
attr_reader :recommendations
|
90
|
+
attr_reader :suggestions
|
91
|
+
attr_reader :conflicting
|
92
|
+
attr_reader :providing
|
93
|
+
attr_reader :replacing
|
94
|
+
attr_reader :attributes
|
95
|
+
attr_reader :groupings
|
96
|
+
attr_reader :recipes
|
97
|
+
attr_reader :version
|
98
|
+
|
99
|
+
def initialize(cookbook = nil, maintainer = 'YOUR_COMPANY_NAME', maintainer_email = 'YOUR_EMAIL', license = 'none')
|
100
|
+
@cookbook = cookbook
|
101
|
+
@name = cookbook ? cookbook.name : ''
|
102
|
+
@long_description = ''
|
103
|
+
@platforms = Stove::Mash.new
|
104
|
+
@dependencies = Stove::Mash.new
|
105
|
+
@recommendations = Stove::Mash.new
|
106
|
+
@suggestions = Stove::Mash.new
|
107
|
+
@conflicting = Stove::Mash.new
|
108
|
+
@providing = Stove::Mash.new
|
109
|
+
@replacing = Stove::Mash.new
|
110
|
+
@attributes = Stove::Mash.new
|
111
|
+
@groupings = Stove::Mash.new
|
112
|
+
@recipes = Stove::Mash.new
|
113
|
+
|
114
|
+
self.maintainer(maintainer)
|
115
|
+
self.maintainer_email(maintainer_email)
|
116
|
+
self.license(license)
|
117
|
+
self.description('A fabulous new cookbook')
|
118
|
+
self.version('0.0.0')
|
119
|
+
|
120
|
+
if cookbook
|
121
|
+
@recipes = cookbook.fully_qualified_recipe_names.inject({}) do |r, e|
|
122
|
+
e = self.name if e =~ /::default$/
|
123
|
+
r[e] = ""
|
124
|
+
self.provides e
|
125
|
+
r
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def from_file(path)
|
131
|
+
path = path.to_s
|
132
|
+
|
133
|
+
if File.exist?(path) && File.readable?(path)
|
134
|
+
self.instance_eval(IO.read(path), path, 1)
|
135
|
+
self
|
136
|
+
else
|
137
|
+
raise Stove::MetadataNotFound.new(path)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def ==(other)
|
142
|
+
COMPARISON_FIELDS.inject(true) do |equal_so_far, field|
|
143
|
+
equal_so_far && other.respond_to?(field) && (other.send(field) == send(field))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def version(arg = nil)
|
148
|
+
@version = Solve::Version.new(arg) if arg
|
149
|
+
@version.to_s
|
150
|
+
end
|
151
|
+
|
152
|
+
def to_hash
|
153
|
+
{
|
154
|
+
'name' => self.name,
|
155
|
+
'version' => self.version,
|
156
|
+
'description' => self.description,
|
157
|
+
'long_description' => self.long_description,
|
158
|
+
'maintainer' => self.maintainer,
|
159
|
+
'maintainer_email' => self.maintainer_email,
|
160
|
+
'license' => self.license,
|
161
|
+
'platforms' => self.platforms,
|
162
|
+
'dependencies' => self.dependencies,
|
163
|
+
'recommendations' => self.recommendations,
|
164
|
+
'suggestions' => self.suggestions,
|
165
|
+
'conflicting' => self.conflicting,
|
166
|
+
'providing' => self.providing,
|
167
|
+
'replacing' => self.replacing,
|
168
|
+
'attributes' => self.attributes,
|
169
|
+
'groupings' => self.groupings,
|
170
|
+
'recipes' => self.recipes,
|
171
|
+
}
|
172
|
+
end
|
173
|
+
|
174
|
+
def to_json(*args)
|
175
|
+
JSON.pretty_generate(self.to_hash)
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
def set_or_return(symbol, arg)
|
180
|
+
iv_symbol = "@#{symbol.to_s}".to_sym
|
181
|
+
|
182
|
+
if arg.nil? && self.instance_variable_defined?(iv_symbol)
|
183
|
+
self.instance_variable_get(iv_symbol)
|
184
|
+
else
|
185
|
+
self.instance_variable_set(iv_symbol, arg)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/lib/stove/error.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module Stove
|
2
|
+
class Error < StandardError
|
3
|
+
class << self
|
4
|
+
def set_exit_code(code)
|
5
|
+
define_method(:exit_code) { code }
|
6
|
+
define_singleton_method(:exit_code) { code }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
set_exit_code 100
|
11
|
+
end
|
12
|
+
|
13
|
+
class InvalidVersionError < Error
|
14
|
+
set_exit_code 101
|
15
|
+
|
16
|
+
def message
|
17
|
+
'You must specify a valid version!'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MetadataNotFound < Error
|
22
|
+
set_exit_code 102
|
23
|
+
|
24
|
+
def initialize(filepath)
|
25
|
+
@filepath = File.expand_path(filepath) rescue filepath
|
26
|
+
end
|
27
|
+
|
28
|
+
def message
|
29
|
+
"No metadata.rb found at: '#{@filepath}'"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class CookbookCategoryNotFound < Error
|
34
|
+
set_exit_code 110
|
35
|
+
|
36
|
+
def message
|
37
|
+
'The cookbook\'s category could not be inferred from the community site. ' <<
|
38
|
+
'If this is a new cookbook, you must specify the category with the ' <<
|
39
|
+
'--category flag.'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class UserCanceledError < Error
|
44
|
+
set_exit_code 120
|
45
|
+
|
46
|
+
def message
|
47
|
+
'Action canceled by user!'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class GitError < Error
|
52
|
+
set_exit_code 130
|
53
|
+
|
54
|
+
def message
|
55
|
+
'Git Error: ' + super
|
56
|
+
end
|
57
|
+
|
58
|
+
class NotARepo < GitError
|
59
|
+
set_exit_code 131
|
60
|
+
|
61
|
+
def message
|
62
|
+
'Not a git repo!'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class DirtyRepo < GitError
|
67
|
+
set_exit_code 132
|
68
|
+
|
69
|
+
def message
|
70
|
+
'You have untracked files!'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class UploadError < Error
|
76
|
+
set_exit_code 140
|
77
|
+
|
78
|
+
def initialize(response)
|
79
|
+
@response = response
|
80
|
+
end
|
81
|
+
|
82
|
+
def message
|
83
|
+
"The following errors occured when uploading:\n" <<
|
84
|
+
@response.parsed_response['error_messages'].map do |error|
|
85
|
+
" - #{error}"
|
86
|
+
end.join("\n")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class BadResponse < Error
|
91
|
+
set_exit_code 150
|
92
|
+
|
93
|
+
def initialize(response)
|
94
|
+
@response = response
|
95
|
+
end
|
96
|
+
|
97
|
+
def message
|
98
|
+
"The following errors occured when making the request:\n" <<
|
99
|
+
@response.parsed_response
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class AbstractFunction < Error
|
104
|
+
set_exit_code 160
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Stove
|
2
|
+
module Formatter
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
def inherited(base)
|
6
|
+
key = base.to_s.split('::').last.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym
|
7
|
+
formatters[key] = base
|
8
|
+
end
|
9
|
+
|
10
|
+
def formatter_method(*methods)
|
11
|
+
methods.each do |name|
|
12
|
+
formatter_methods << name
|
13
|
+
|
14
|
+
define_method(name) do |*args|
|
15
|
+
raise Stove::AbstractFunction
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def formatters
|
21
|
+
@formatters ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def formatter_methods
|
25
|
+
@formatter_methods ||= []
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
formatter_method :upload
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/stove/git.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Stove
|
4
|
+
module Git
|
5
|
+
# Run a git command.
|
6
|
+
#
|
7
|
+
# @param [String] command
|
8
|
+
# the command to run
|
9
|
+
#
|
10
|
+
# @return [String]
|
11
|
+
# the stdout from the command
|
12
|
+
def git(command)
|
13
|
+
Stove::Logger.debug "shellout 'git #{command}'"
|
14
|
+
response = shellout("git #{command}")
|
15
|
+
|
16
|
+
Stove::Logger.debug response.stdout
|
17
|
+
|
18
|
+
unless response.success?
|
19
|
+
Stove::Logger.debug response.stderr
|
20
|
+
raise Stove::GitError, response.stderr
|
21
|
+
end
|
22
|
+
|
23
|
+
response.stdout.strip
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return true if the current working directory is a valid
|
27
|
+
# git repot, false otherwise.
|
28
|
+
#
|
29
|
+
# @return [Boolean]
|
30
|
+
def git_repo?
|
31
|
+
git('rev-parse --show-toplevel')
|
32
|
+
true
|
33
|
+
rescue
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return true if the current working directory is clean,
|
38
|
+
# false otherwise
|
39
|
+
#
|
40
|
+
# @return [Boolean]
|
41
|
+
def git_repo_clean?
|
42
|
+
!!git('status -s').strip.empty?
|
43
|
+
rescue
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
def shellout(command)
|
48
|
+
out, err = Tempfile.new('shellout.stdout'), Tempfile.new('shellout.stderr')
|
49
|
+
|
50
|
+
begin
|
51
|
+
pid = Process.spawn(command, out: out.to_i, err: err.to_i)
|
52
|
+
pid, status = Process.waitpid2(pid)
|
53
|
+
|
54
|
+
# Check if we're getting back a process status because win32-process 6.x was a fucking MURDERER.
|
55
|
+
# https://github.com/djberg96/win32-process/blob/master/lib/win32/process.rb#L494-L519
|
56
|
+
exitstatus = status.is_a?(Process::Status) ? status.exitstatus : status
|
57
|
+
rescue Errno::ENOENT => e
|
58
|
+
err.write('')
|
59
|
+
err.write('Command not found: ' + command)
|
60
|
+
end
|
61
|
+
|
62
|
+
out.close
|
63
|
+
err.close
|
64
|
+
|
65
|
+
OpenStruct.new({
|
66
|
+
exitstatus: exitstatus,
|
67
|
+
stdout: File.read(out).strip,
|
68
|
+
stderr: File.read(err).strip,
|
69
|
+
success?: exitstatus == 0,
|
70
|
+
error?: exitstatus == 0,
|
71
|
+
})
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/stove/jira.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'jiralicious'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Stove
|
5
|
+
class JIRA
|
6
|
+
JIRA_URL = 'https://tickets.opscode.com'
|
7
|
+
|
8
|
+
Jiralicious.configure do |config|
|
9
|
+
config.username = Stove::Config['jira_username']
|
10
|
+
config.password = Stove::Config['jira_password']
|
11
|
+
config.uri = JIRA_URL
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def unreleased_tickets_for(component)
|
16
|
+
jql = [
|
17
|
+
'project = COOK',
|
18
|
+
'resolution = Fixed',
|
19
|
+
'status = "Fix Committed"',
|
20
|
+
'component = ' + component.inspect
|
21
|
+
].join(' AND ')
|
22
|
+
Stove::Logger.debug "JQL: #{jql.inspect}"
|
23
|
+
|
24
|
+
Jiralicious.search(jql).issues
|
25
|
+
end
|
26
|
+
|
27
|
+
# Comment and close a particular issue.
|
28
|
+
#
|
29
|
+
# @param [Jiralicious::Issue] ticket
|
30
|
+
# the JIRA ticket
|
31
|
+
# @param [Stove::Cookbook] cookbook
|
32
|
+
# the cookbook to release
|
33
|
+
def comment_and_close(ticket, cookbook)
|
34
|
+
comment = "Released in [#{cookbook.version}|#{cookbook.url}]"
|
35
|
+
|
36
|
+
transition = Jiralicious::Issue::Transitions.find(ticket.jira_key).find do |key, value|
|
37
|
+
!value.is_a?(String) && value.name == 'Close'
|
38
|
+
end.last
|
39
|
+
|
40
|
+
Jiralicious::Issue::Transitions.go(ticket.jira_key, transition.id, { comment: comment })
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|