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.
@@ -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
@@ -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,7 @@
1
+ module Stove
2
+ module Formatter
3
+ require_relative 'formatter/base'
4
+ require_relative 'formatter/human'
5
+ require_relative 'formatter/silent'
6
+ end
7
+ 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
@@ -0,0 +1,9 @@
1
+ module Stove
2
+ module Formatter
3
+ class Human < Base
4
+ def upload(cookbook)
5
+ puts "Uploaded #{cookbook.name} (#{cookbook.version}) to '#{cookbook.url}'"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Stove
2
+ module Formatter
3
+ # Silence all output
4
+ class Silent < Base
5
+ Stove::Formatter::Base.formatter_methods.each do |name|
6
+ define_method(name) do |*args|; end
7
+ end
8
+ end
9
+ end
10
+ 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