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,23 @@
|
|
1
|
+
Given /^the environment variable (.+) is "(.+)"$/ do |variable, value|
|
2
|
+
set_env(variable, value)
|
3
|
+
end
|
4
|
+
|
5
|
+
Then /^the exit status will be "(.+)"$/ do |error|
|
6
|
+
code = Stove.const_get(error).exit_code
|
7
|
+
assert_exit_status(code)
|
8
|
+
end
|
9
|
+
|
10
|
+
When /^the CLI options are all off$/ do
|
11
|
+
class Stove::Cli
|
12
|
+
private
|
13
|
+
def options
|
14
|
+
@options ||= {
|
15
|
+
:git => false,
|
16
|
+
:jira => false,
|
17
|
+
:upload => false,
|
18
|
+
:changelog => false,
|
19
|
+
:log_level => :debug,
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Given /^the Community Site has the cookbooks?:$/ do |table|
|
2
|
+
table.raw.each do |name, version, category|
|
3
|
+
version ||= '0.0.0'
|
4
|
+
category ||= 'Other'
|
5
|
+
|
6
|
+
CommunityZero::Cookbook.create({
|
7
|
+
name: name,
|
8
|
+
version: version,
|
9
|
+
category: category,
|
10
|
+
})
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
Then /^the Community Site will( not)? have the cookbooks?:$/ do |negate, table|
|
15
|
+
table.raw.each do |name, version, category|
|
16
|
+
cookbook = CommunityZero::Store.find(name, version)
|
17
|
+
|
18
|
+
if negate
|
19
|
+
expect(cookbook).to be_nil
|
20
|
+
else
|
21
|
+
expect(cookbook).to_not be_nil
|
22
|
+
expect(cookbook.category).to eql(category) if category
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Given /^I have a cookbook named "(.+)"(?: (?:at|with) version "(.+)")?$/ do |name, version|
|
2
|
+
create_cookbook(name, version)
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^I have a cookbook named "(.+)"(?: (?:at|with) version "(.+)")? with git support$/ do |name, version|
|
6
|
+
create_cookbook(name, version, git: true)
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
# Create a new cookbook with the given name and version.
|
11
|
+
#
|
12
|
+
# @param [String] name
|
13
|
+
# @param [String] version (default: 0.0.0.0)
|
14
|
+
# @param [Hash] options
|
15
|
+
def create_cookbook(name, version, options = {})
|
16
|
+
create_dir(name)
|
17
|
+
cd(name)
|
18
|
+
write_file('CHANGELOG.md', "#{name} Cookbook CHANGELOG\n=====\n\nv0.0.0\n-----")
|
19
|
+
write_file('README.md', 'This is a README')
|
20
|
+
write_file('metadata.rb', "name '#{name}'\nversion '#{version || '0.0.0'}'")
|
21
|
+
|
22
|
+
if options[:git]
|
23
|
+
git_init(current_dir)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Then /^the git remote will( not)? have the commit "(.+)"$/ do |negate, message|
|
2
|
+
commits = git_commits(fake_git_remote)
|
3
|
+
|
4
|
+
if negate
|
5
|
+
expect(commits).to_not include(message)
|
6
|
+
else
|
7
|
+
expect(commits).to include(message)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Then /^the git remote will( not)? have the tag "(.+)"$/ do |negate, tag|
|
12
|
+
tags = git_tags(fake_git_remote)
|
13
|
+
|
14
|
+
if negate
|
15
|
+
expect(tags).to_not include(tag)
|
16
|
+
else
|
17
|
+
expect(tags).to include(tag)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'aruba/api'
|
2
|
+
require 'aruba/cucumber'
|
3
|
+
require 'aruba/in_process'
|
4
|
+
require 'rspec/expectations'
|
5
|
+
require 'stove'
|
6
|
+
|
7
|
+
require_relative '../../spec/support/community_site'
|
8
|
+
require_relative '../../spec/support/git'
|
9
|
+
|
10
|
+
World(Aruba::Api)
|
11
|
+
World(Stove::RSpec::Git)
|
12
|
+
|
13
|
+
Aruba::InProcess.main_class = Stove::Cli
|
14
|
+
Aruba.process = Aruba::InProcess
|
15
|
+
|
16
|
+
Stove.set_formatter(:silent)
|
17
|
+
Stove::RSpec::CommunitySite.start(port: 3390)
|
18
|
+
Stove::CommunitySite.base_uri(Stove::RSpec::CommunitySite.server_url)
|
19
|
+
Stove::CommunitySite.http_uri(Stove::RSpec::CommunitySite.server_url)
|
20
|
+
|
21
|
+
Before do
|
22
|
+
@dirs = [Dir.mktmpdir]
|
23
|
+
Stove::RSpec::CommunitySite.reset!
|
24
|
+
end
|
25
|
+
|
26
|
+
# The path to Aruba's "stuff"
|
27
|
+
def tmp_path
|
28
|
+
File.expand_path(@dirs.first.to_s)
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
Feature: Upload
|
2
|
+
As a stove user
|
3
|
+
In order to upload cookbooks
|
4
|
+
I want to tar and push to a Community Site
|
5
|
+
|
6
|
+
Background:
|
7
|
+
* I have a cookbook named "bacon"
|
8
|
+
* the CLI options are all off
|
9
|
+
|
10
|
+
Scenario: --no-upload
|
11
|
+
* I successfully run `bake 1.0.0 --no-upload`
|
12
|
+
* the Community Site will not have the cookbook:
|
13
|
+
| bacon | 1.0.0 |
|
14
|
+
|
15
|
+
Scenario: --upload (no category, no existing)
|
16
|
+
* I run `bake 1.0.0 --upload`
|
17
|
+
* the Community Site will not have the cookbook:
|
18
|
+
| bacon | 1.0.0 |
|
19
|
+
* the exit status will be "CookbookCategoryNotFound"
|
20
|
+
|
21
|
+
Scenario: --upload (no category, existing)
|
22
|
+
* the Community Site has the cookbook:
|
23
|
+
| bacon | 0.0.0 | Application |
|
24
|
+
* I successfully run `bake 1.0.0 --upload`
|
25
|
+
* the Community Site will have the cookbook:
|
26
|
+
| bacon | 1.0.0 | Application |
|
27
|
+
|
28
|
+
Scenario: --upload (category, no existing)
|
29
|
+
* I successfully run `bake 1.0.0 --upload --category Application`
|
30
|
+
* the Community Site will have the cookbook:
|
31
|
+
| bacon | 1.0.0 | Application |
|
32
|
+
|
33
|
+
Scenario: --upload (category, existing)
|
34
|
+
* the Community Site has the cookbook:
|
35
|
+
| bacon | 0.0.0 | Application |
|
36
|
+
* I successfully run `bake 1.0.0 --upload --category Application`
|
37
|
+
* the Community Site will have the cookbook:
|
38
|
+
| bacon | 1.0.0 | Application |
|
39
|
+
|
40
|
+
Scenario: --upload (existing version)
|
41
|
+
* the Community Site has the cookbook:
|
42
|
+
| bacon | 1.0.0 | Application |
|
43
|
+
* I run `bake 1.0.0 --upload`
|
44
|
+
* the exit status will be "UploadError"
|
data/lib/stove.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Stove
|
2
|
+
require_relative 'stove/config'
|
3
|
+
require_relative 'stove/git'
|
4
|
+
require_relative 'stove/logger'
|
5
|
+
|
6
|
+
require_relative 'stove/cli'
|
7
|
+
require_relative 'stove/community_site'
|
8
|
+
require_relative 'stove/cookbook'
|
9
|
+
require_relative 'stove/error'
|
10
|
+
require_relative 'stove/formatter'
|
11
|
+
require_relative 'stove/jira'
|
12
|
+
require_relative 'stove/mash'
|
13
|
+
require_relative 'stove/packager'
|
14
|
+
require_relative 'stove/uploader'
|
15
|
+
require_relative 'stove/version'
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def formatter
|
19
|
+
@formatter ||= Stove::Formatter::Human.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_formatter(name)
|
23
|
+
@formatter = Stove::Formatter::Base.formatters[name.to_sym].new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/stove/cli.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'stove'
|
3
|
+
|
4
|
+
module Stove
|
5
|
+
class Cli
|
6
|
+
def initialize(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel)
|
7
|
+
@argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
|
8
|
+
$stdout, @stderr = @stdout, @stderr
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute!
|
12
|
+
option_parser.parse!(@argv)
|
13
|
+
options[:new_version] = @argv.first
|
14
|
+
|
15
|
+
raise Stove::InvalidVersionError unless valid_version?(options[:new_version])
|
16
|
+
|
17
|
+
Stove::Cookbook.new(options).release!
|
18
|
+
@kernel.exit(0)
|
19
|
+
rescue => e
|
20
|
+
@stderr.puts "#{e.class}: #{e.message}"
|
21
|
+
|
22
|
+
if Stove::Logger.sev_threshold == ::Logger::DEBUG
|
23
|
+
@stderr.puts " #{e.backtrace.join("\n ")}"
|
24
|
+
end
|
25
|
+
|
26
|
+
@kernel.exit(e.respond_to?(:exit_code) ? e.exit_code : 500)
|
27
|
+
ensure
|
28
|
+
$stdout, $stderr = STDOUT, STDERR
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
# The option parser for handling command line flags.
|
33
|
+
#
|
34
|
+
# @return [OptionParser]
|
35
|
+
def option_parser
|
36
|
+
@option_parser ||= OptionParser.new do |opts|
|
37
|
+
opts.banner = "Usage: bake x.y.z"
|
38
|
+
|
39
|
+
opts.on('-l', '--log-level [LEVEL]', [:fatal, :error, :warn, :info, :debug], 'Ruby log level') do |v|
|
40
|
+
options[:log_level] = v
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('-c', '--category [CATEGORY]', String, 'The category for the cookbook (optional for existing cookbooks)') do |v|
|
44
|
+
options[:category] = v
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on('-p', '--path [PATH]', String, 'The path to the cookbook to release (default: PWD)') do |v|
|
48
|
+
options[:path] = v
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on('--[no-]git', 'Automatically tag and push to git (default: true)') do |v|
|
52
|
+
options[:git] = v
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on('-r', '--remote', String, 'The name of the git remote to push to') do |v|
|
56
|
+
options[:remote] = v
|
57
|
+
end
|
58
|
+
|
59
|
+
opts.on('-b', '--branch', String, 'The name of the git branch to push to') do |v|
|
60
|
+
options[:branch] = v
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on('--[no-]jira', 'Automatically populate the CHANGELOG from JIRA tickets and close them (default: false)') do |v|
|
64
|
+
options[:jira] = v
|
65
|
+
end
|
66
|
+
|
67
|
+
opts.on('--[no-]upload', 'Upload the cookbook to the Opscode Community Site (default: true)') do |v|
|
68
|
+
options[:upload] = v
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.on('--[no-]changelog', 'Automatically generate a CHANGELOG (default: true)') do |v|
|
72
|
+
options[:changelog] = v
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
76
|
+
puts opts
|
77
|
+
exit
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on_tail('-v', '--version', 'Show version') do
|
81
|
+
puts Stove::VERSION
|
82
|
+
exit(0)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# The options to pass to the cookbook. Includes default values
|
88
|
+
# that are manipulated by the option parser.
|
89
|
+
#
|
90
|
+
# @return [Hash]
|
91
|
+
def options
|
92
|
+
@options ||= {
|
93
|
+
:path => Dir.pwd,
|
94
|
+
:git => true,
|
95
|
+
:remote => 'origin',
|
96
|
+
:branch => 'master',
|
97
|
+
:jira => false,
|
98
|
+
:upload => true,
|
99
|
+
:changelog => true,
|
100
|
+
:log_level => :warn,
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
# Determine if the given string is a valid version string.
|
105
|
+
#
|
106
|
+
# @return [Boolean]
|
107
|
+
def valid_version?(version)
|
108
|
+
version.to_s =~ /^\d+\.\d+\.\d+$/
|
109
|
+
end
|
110
|
+
|
111
|
+
# Convert a string to it's logger constant.
|
112
|
+
#
|
113
|
+
# @return [Object]
|
114
|
+
def level_to_constant(level)
|
115
|
+
case level.to_s.strip.downcase.to_sym
|
116
|
+
when :fatal
|
117
|
+
::Logger::FATAL
|
118
|
+
when :error
|
119
|
+
::Logger::ERROR
|
120
|
+
when :warn
|
121
|
+
::Logger::WARN
|
122
|
+
when :info
|
123
|
+
::Logger::INFO
|
124
|
+
when :debug
|
125
|
+
::Logger::DEBUG
|
126
|
+
else
|
127
|
+
::Logger::INFO
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module Stove
|
4
|
+
class CommunitySite
|
5
|
+
include HTTParty
|
6
|
+
base_uri 'https://cookbooks.opscode.com/api/v1'
|
7
|
+
headers 'Content-Type' => 'application/json', 'Accept' => 'application/json'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# The URI for the web-based version of the site. (default:
|
11
|
+
# https://community.opscode.com).
|
12
|
+
#
|
13
|
+
# If a parameter is given, the {http_uri} is set to that value.
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
def http_uri(arg = nil)
|
17
|
+
if arg.nil?
|
18
|
+
@http_uri ||= 'https://community.opscode.com'
|
19
|
+
else
|
20
|
+
@http_uri = arg
|
21
|
+
@http_uri
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get and cache a community cookbook's JSON response from the given name
|
26
|
+
# and version.
|
27
|
+
#
|
28
|
+
# @example Find a cookbook by name
|
29
|
+
# CommunitySite.cookbook('apache2') #=> {...}
|
30
|
+
#
|
31
|
+
# @example Find a cookbook by name and version
|
32
|
+
# CommunitySite.cookbook('apache2', '1.0.0') #=> {...}
|
33
|
+
#
|
34
|
+
# @example Find a non-existent cookbook
|
35
|
+
# CommunitySite.cookbook('not-real') #=> CommunitySite::BadResponse
|
36
|
+
#
|
37
|
+
# @raise [CommunitySite::BadResponse]
|
38
|
+
# if the given cookbook (or cookbook version) does not exist on the community site
|
39
|
+
#
|
40
|
+
# @param [String] name
|
41
|
+
# the name of the cookbook on the community site
|
42
|
+
# @param [String] version (optional)
|
43
|
+
# the version of the cookbook to find
|
44
|
+
def cookbook(name, version = nil)
|
45
|
+
if version.nil?
|
46
|
+
get("/cookbooks/#{name}")
|
47
|
+
else
|
48
|
+
get("/cookbooks/#{name}/versions/#{format_version(version)}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
# Convert a version string (x.y.z) to a community-site friendly format
|
54
|
+
# (x_y_z).
|
55
|
+
#
|
56
|
+
# @example Convert a version to a version string
|
57
|
+
# format_version('1.2.3') #=> 1_2_3
|
58
|
+
#
|
59
|
+
# @param [#to_s] version
|
60
|
+
# the version string to convert
|
61
|
+
#
|
62
|
+
# @return [String]
|
63
|
+
def format_version(version)
|
64
|
+
version.gsub('.', '_')
|
65
|
+
end
|
66
|
+
|
67
|
+
# @override [HTTParty.get]
|
68
|
+
def get(path, options = {}, &block)
|
69
|
+
cache[path] ||= begin
|
70
|
+
Stove::Logger.debug "Getting #{path}"
|
71
|
+
response = super(path)
|
72
|
+
raise Stove::BadResponse.new(response) unless response.ok?
|
73
|
+
response.parsed_response
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# A small, unpersisted cache for storing responses
|
78
|
+
#
|
79
|
+
# @return [Hash]
|
80
|
+
def cache
|
81
|
+
@cache ||= {}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/stove/config.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Stove
|
2
|
+
class Config < ::Hash
|
3
|
+
class << self
|
4
|
+
def [](thing)
|
5
|
+
instance[thing]
|
6
|
+
end
|
7
|
+
|
8
|
+
def instance
|
9
|
+
@instance ||= load!
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def load!
|
14
|
+
JSON.parse(File.read(File.expand_path("~/.stove")))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Stove
|
5
|
+
class Cookbook
|
6
|
+
require_relative 'cookbook/metadata'
|
7
|
+
|
8
|
+
include Stove::Git
|
9
|
+
|
10
|
+
# The path to this cookbook.
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
# The name of the cookbook (must correspond to the name of the
|
16
|
+
# cookbook on the community site).
|
17
|
+
#
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
# The version of this cookbook (originally).
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
attr_reader :version
|
25
|
+
|
26
|
+
# The new version of the cookbook.
|
27
|
+
#
|
28
|
+
# @return [String]
|
29
|
+
attr_reader :new_version
|
30
|
+
|
31
|
+
# The metadata for this cookbook.
|
32
|
+
#
|
33
|
+
# @return [Stove::Cookbook::Metadata]
|
34
|
+
attr_reader :metadata
|
35
|
+
|
36
|
+
# The list of options passed to the cookbook.
|
37
|
+
#
|
38
|
+
# @return [Hash]
|
39
|
+
attr_reader :options
|
40
|
+
|
41
|
+
# Create a new wrapper around the cookbook object.
|
42
|
+
#
|
43
|
+
# @param [Hash] options
|
44
|
+
# the list of options
|
45
|
+
def initialize(options = {})
|
46
|
+
@path = options[:path] || Dir.pwd
|
47
|
+
@new_version = options[:new_version]
|
48
|
+
@options = options
|
49
|
+
|
50
|
+
load_metadata!
|
51
|
+
end
|
52
|
+
|
53
|
+
# The category for this cookbook on the community site.
|
54
|
+
#
|
55
|
+
# @return [String]
|
56
|
+
def category
|
57
|
+
@category ||= options[:category] || Stove::CommunitySite.cookbook(name)['category']
|
58
|
+
rescue
|
59
|
+
raise Stove::CookbookCategoryNotFound
|
60
|
+
end
|
61
|
+
|
62
|
+
# The URL for the cookbook on the Community Site.
|
63
|
+
#
|
64
|
+
# @return [String]
|
65
|
+
def url
|
66
|
+
"#{Stove::CommunitySite.http_uri}/cookbooks/#{name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Deterine if this cookbook version is released on the community site
|
70
|
+
def released?
|
71
|
+
@_released ||= begin
|
72
|
+
Stove::CommunitySite.cookbook(name, version)
|
73
|
+
true
|
74
|
+
rescue Stove::BadResponse
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# The unreleased JIRA tickets for this cookbook.
|
80
|
+
#
|
81
|
+
# @return [Hashie::Dash, Array]
|
82
|
+
def unreleased_tickets
|
83
|
+
@unreleased_tickets ||= Stove::JIRA.unreleased_tickets_for(name)
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
def release!
|
88
|
+
if options[:git]
|
89
|
+
validate_git_repo!
|
90
|
+
validate_git_clean!
|
91
|
+
end
|
92
|
+
|
93
|
+
version_bump
|
94
|
+
|
95
|
+
if options[:changelog]
|
96
|
+
update_changelog
|
97
|
+
end
|
98
|
+
|
99
|
+
if options[:git]
|
100
|
+
Dir.chdir(path) do
|
101
|
+
git "add metadata.rb"
|
102
|
+
git "add CHANGELOG.md"
|
103
|
+
git "commit -m 'Version bump to v#{version}'"
|
104
|
+
git "push #{options[:remote] || 'origin'} #{options[:branch] || 'master'}"
|
105
|
+
|
106
|
+
git "tag v#{version}"
|
107
|
+
git "push #{options[:remote] || 'origin'} v#{version}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
if options[:upload]
|
112
|
+
upload
|
113
|
+
end
|
114
|
+
|
115
|
+
if options[:jira]
|
116
|
+
resolve_jira_issues
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
def upload
|
122
|
+
return true unless options[:upload]
|
123
|
+
Stove::Uploader.new(self).upload!
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
# Load the metadata and set the @metadata instance variable.
|
128
|
+
#
|
129
|
+
# @raise [ArgumentError]
|
130
|
+
# if there is no metadata.rb
|
131
|
+
#
|
132
|
+
# @return [String]
|
133
|
+
# the path to the metadata file
|
134
|
+
def load_metadata!
|
135
|
+
metadata_path = File.expand_path(File.join(path, 'metadata.rb'))
|
136
|
+
|
137
|
+
@metadata = Stove::Cookbook::Metadata.from_file(metadata_path)
|
138
|
+
@name = @metadata.name
|
139
|
+
@version = @metadata.version
|
140
|
+
|
141
|
+
metadata_path
|
142
|
+
end
|
143
|
+
alias_method :reload_metadata!, :load_metadata!
|
144
|
+
|
145
|
+
# Create a new CHANGELOG in markdown format.
|
146
|
+
#
|
147
|
+
# @example given a cookbook named bacon
|
148
|
+
# cookbook.create_changelog #=> <<EOH
|
149
|
+
# bacon Cookbook CHANGELOG
|
150
|
+
# ------------------------
|
151
|
+
# EOH
|
152
|
+
#
|
153
|
+
# @return [String]
|
154
|
+
# the path to the new CHANGELOG
|
155
|
+
def create_changelog
|
156
|
+
destination = File.join(path, 'CHANGELOG.md')
|
157
|
+
|
158
|
+
# Be idempotent :)
|
159
|
+
return destination if File.exists?(destination)
|
160
|
+
|
161
|
+
header = "#{name} Cookbook CHANGELOG\n"
|
162
|
+
header << "#{'='*header.length}\n"
|
163
|
+
header << "This file is used to list changes made in each version of the #{cookbook.name} cookbook.\n\n"
|
164
|
+
|
165
|
+
File.open(destination, 'wb') do |file|
|
166
|
+
file.write(header)
|
167
|
+
end
|
168
|
+
|
169
|
+
destination
|
170
|
+
end
|
171
|
+
|
172
|
+
# Update the CHANGELOG with the new contents, but inserting
|
173
|
+
# the newest version's CHANGELOG at the top of the file (after
|
174
|
+
# the header)
|
175
|
+
def update_changelog
|
176
|
+
changelog = create_changelog
|
177
|
+
contents = File.readlines(changelog)
|
178
|
+
|
179
|
+
index = contents.find_index { |line| line =~ /(--)+/ } - 2
|
180
|
+
contents.insert(index, "\n" + generate_changelog)
|
181
|
+
|
182
|
+
Dir.mktmpdir do |dir|
|
183
|
+
tmpfile = File.join(dir, 'CHANGELOG.md')
|
184
|
+
File.open(tmpfile, 'w') { |f| f.write(contents.join('')) }
|
185
|
+
response = shellout("$EDITOR #{tmpfile}")
|
186
|
+
|
187
|
+
unless response.success?
|
188
|
+
Stove::Logger.debug response.stderr
|
189
|
+
raise Stove::Error, response.stderr
|
190
|
+
end
|
191
|
+
|
192
|
+
FileUtils.mv(tmpfile, File.join(path, 'CHANGELOG.md'))
|
193
|
+
end
|
194
|
+
rescue SystemExit, Interrupt
|
195
|
+
raise Stove::UserCanceledError
|
196
|
+
end
|
197
|
+
|
198
|
+
# Generate a CHANGELOG in markdown format.
|
199
|
+
#
|
200
|
+
# @param [String] version
|
201
|
+
# the version string in x.y.z format
|
202
|
+
#
|
203
|
+
# @return [String]
|
204
|
+
def generate_changelog
|
205
|
+
contents = []
|
206
|
+
contents << "v#{version}"
|
207
|
+
contents << '-'*(version.length+1)
|
208
|
+
|
209
|
+
if options[:jira]
|
210
|
+
by_type = unreleased_tickets.inject({}) do |hash, ticket|
|
211
|
+
issue_type = ticket.fields.current['issuetype']['name']
|
212
|
+
hash[issue_type] ||= []
|
213
|
+
hash[issue_type] << {
|
214
|
+
number: ticket.jira_key,
|
215
|
+
details: ticket.fields.current['summary'],
|
216
|
+
}
|
217
|
+
|
218
|
+
hash
|
219
|
+
end
|
220
|
+
|
221
|
+
by_type.each do |issue_type, tickets|
|
222
|
+
contents << "### #{issue_type}"
|
223
|
+
tickets.sort { |a,b| b[:number].to_i <=> a[:number].to_i }.each do |ticket|
|
224
|
+
contents << "- **[#{ticket[:number]}](#{Stove::JIRA::JIRA_URL}/browse/#{ticket[:number]})** - #{ticket[:details]}"
|
225
|
+
end
|
226
|
+
contents << ""
|
227
|
+
end
|
228
|
+
else
|
229
|
+
contents << "_Enter CHANGELOG for #{name} (#{version}) here_"
|
230
|
+
contents << ""
|
231
|
+
end
|
232
|
+
|
233
|
+
contents.join("\n")
|
234
|
+
end
|
235
|
+
|
236
|
+
# Bump the version in the metdata.rb to the specified
|
237
|
+
# parameter.
|
238
|
+
#
|
239
|
+
# @return [String]
|
240
|
+
# the new version string
|
241
|
+
def version_bump
|
242
|
+
return true if new_version.to_s == version.to_s
|
243
|
+
|
244
|
+
metadata_path = File.join(path, 'metadata.rb')
|
245
|
+
contents = File.read(metadata_path)
|
246
|
+
|
247
|
+
contents.sub!(/^version(\s+)('|")#{version.to_s}('|")/, "version\\1\\2#{new_version.to_s}\\3")
|
248
|
+
|
249
|
+
File.open(metadata_path, 'w') { |f| f.write(contents) }
|
250
|
+
reload_metadata!
|
251
|
+
end
|
252
|
+
|
253
|
+
# Resolve all the JIRA issues that have been merged.
|
254
|
+
def resolve_jira_issues
|
255
|
+
unreleased_tickets.collect do |ticket|
|
256
|
+
Thread.new { Stove::JIRA.comment_and_close(ticket, self) }
|
257
|
+
end.map(&:join)
|
258
|
+
end
|
259
|
+
|
260
|
+
# Validate that the current working directory is git repo.
|
261
|
+
#
|
262
|
+
# @raise [Stove::GitError::NotARepo]
|
263
|
+
# if this is not currently a git repo
|
264
|
+
def validate_git_repo!
|
265
|
+
Dir.chdir(path) do
|
266
|
+
raise Stove::GitError::NotARepo unless git_repo?
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Validate that the current.
|
271
|
+
#
|
272
|
+
# @raise [Stove::GitError::DirtyRepo]
|
273
|
+
# if the current working directory is not clean
|
274
|
+
def validate_git_clean!
|
275
|
+
Dir.chdir(path) do
|
276
|
+
raise Stove::GitError::DirtyRepo unless git_repo_clean?
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|