stove 1.1.2 → 2.0.0.beta.1

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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +41 -29
  5. data/Rakefile +15 -0
  6. data/bin/bake +1 -2
  7. data/features/actions/bump.feature +22 -0
  8. data/features/actions/changelog.feature +45 -0
  9. data/features/actions/dev.feature +18 -0
  10. data/features/actions/upload.feature +48 -0
  11. data/features/plugins/git.feature +24 -0
  12. data/features/rake.feature +1 -2
  13. data/features/step_definitions/cli_steps.rb +1 -27
  14. data/features/step_definitions/{community_site_steps.rb → community_steps.rb} +9 -5
  15. data/features/step_definitions/config_steps.rb +24 -0
  16. data/features/step_definitions/cookbook_steps.rb +28 -6
  17. data/features/step_definitions/cucumber_steps.rb +12 -0
  18. data/features/step_definitions/git_steps.rb +10 -7
  19. data/features/support/env.rb +12 -28
  20. data/features/support/stove/git.rb +48 -0
  21. data/lib/stove.rb +102 -19
  22. data/lib/stove/actions/base.rb +21 -0
  23. data/lib/stove/actions/bump.rb +25 -0
  24. data/lib/stove/actions/changelog.rb +71 -0
  25. data/lib/stove/actions/dev.rb +22 -0
  26. data/lib/stove/actions/finish.rb +8 -0
  27. data/lib/stove/actions/start.rb +7 -0
  28. data/lib/stove/actions/upload.rb +27 -0
  29. data/lib/stove/cli.rb +107 -79
  30. data/lib/stove/community.rb +124 -0
  31. data/lib/stove/config.rb +62 -13
  32. data/lib/stove/cookbook.rb +76 -238
  33. data/lib/stove/cookbook/metadata.rb +16 -11
  34. data/lib/stove/error.rb +13 -107
  35. data/lib/stove/filter.rb +59 -0
  36. data/lib/stove/jira.rb +74 -30
  37. data/lib/stove/middlewares/chef_authentication.rb +60 -0
  38. data/lib/stove/middlewares/exceptions.rb +17 -0
  39. data/lib/stove/mixins/filterable.rb +11 -0
  40. data/lib/stove/mixins/insideable.rb +13 -0
  41. data/lib/stove/mixins/instanceable.rb +23 -0
  42. data/lib/stove/mixins/loggable.rb +32 -0
  43. data/lib/stove/mixins/optionable.rb +41 -0
  44. data/lib/stove/mixins/validatable.rb +7 -0
  45. data/lib/stove/packager.rb +23 -22
  46. data/lib/stove/plugins/base.rb +35 -0
  47. data/lib/stove/plugins/git.rb +71 -0
  48. data/lib/stove/plugins/github.rb +108 -0
  49. data/lib/stove/plugins/jira.rb +72 -0
  50. data/lib/stove/rake_task.rb +56 -37
  51. data/lib/stove/runner.rb +84 -0
  52. data/lib/stove/util.rb +56 -0
  53. data/lib/stove/validator.rb +67 -0
  54. data/lib/stove/version.rb +1 -1
  55. data/locales/en.yml +231 -0
  56. data/stove.gemspec +11 -11
  57. metadata +85 -67
  58. data/features/changelog.feature +0 -22
  59. data/features/cli.feature +0 -11
  60. data/features/devodd.feature +0 -19
  61. data/features/git.feature +0 -34
  62. data/features/upload.feature +0 -40
  63. data/lib/stove/community_site.rb +0 -85
  64. data/lib/stove/formatter.rb +0 -7
  65. data/lib/stove/formatter/base.rb +0 -32
  66. data/lib/stove/formatter/human.rb +0 -9
  67. data/lib/stove/formatter/silent.rb +0 -10
  68. data/lib/stove/git.rb +0 -82
  69. data/lib/stove/github.rb +0 -43
  70. data/lib/stove/logger.rb +0 -56
  71. data/lib/stove/uploader.rb +0 -64
  72. data/spec/support/community_site.rb +0 -33
  73. data/spec/support/git.rb +0 -52
@@ -0,0 +1,13 @@
1
+ module Stove
2
+ module Mixin::Insideable
3
+ #
4
+ # Execute the command inside the cookbook.
5
+ #
6
+ # @param [Cookbook]
7
+ # the cookbook to execute inside of
8
+ #
9
+ def inside(cookbook, &block)
10
+ Dir.chdir(cookbook.path, &block)
11
+ end
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ module Stove
2
+ module Mixin::Validatable
3
+ def validate(id, &block)
4
+ Runner.validations << Validator.new(self, id, &block)
5
+ end
6
+ end
7
+ end
@@ -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
- # Sandbox
56
- sandbox = Dir.mktmpdir
57
- FileUtils.mkdir_p(sandbox)
53
+ def pack!
54
+ destination = Tempfile.new(cookbook.name).path
58
55
 
59
- # Containing folder
60
- container = File.join(sandbox, cookbook.name)
61
- FileUtils.mkdir_p(container)
56
+ # Sandbox
57
+ sandbox = Dir.mktmpdir
58
+ FileUtils.mkdir_p(sandbox)
62
59
 
63
- # Copy filles
64
- FileUtils.cp_r(cookbook_files, container)
60
+ # Containing folder
61
+ container = File.join(sandbox, cookbook.name)
62
+ FileUtils.mkdir_p(container)
65
63
 
66
- # Generate metadata
67
- File.open(File.join(container, 'metadata.json'), 'w') do |f|
68
- f.write(cookbook.metadata.to_json)
69
- end
64
+ # Copy filles
65
+ FileUtils.cp_r(cookbook_files, container)
70
66
 
71
- Dir.chdir(sandbox) do |dir|
72
- # This is super fucking annoying. The community site should really
73
- # be better at reading tarballs
74
- relative_path = container.gsub(sandbox + '/', '') + '/'
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
- return destination
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