stove 1.0.0

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