stove 1.1.2 → 2.0.0.beta.1

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