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