releasinator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/validator.rb ADDED
@@ -0,0 +1,312 @@
1
+ require 'colorize'
2
+ require 'fileutils'
3
+ require_relative 'command_processor'
4
+ require_relative 'downstream_repo'
5
+ require_relative 'github_repo'
6
+ require_relative 'printer'
7
+ require_relative 'validator_changelog'
8
+
9
+ module Releasinator
10
+ class Validator
11
+
12
+ def initialize(releasinator_config)
13
+ @releasinator_config = releasinator_config
14
+ end
15
+
16
+ def get_changelog_contents(base_dir)
17
+ Dir.chdir(base_dir) do
18
+ open('CHANGELOG.md').read
19
+ end
20
+ end
21
+
22
+ def validate_git_version
23
+ version_output = CommandProcessor.command("git version")
24
+ # version where the parallel git fetch features were added
25
+ expected_git_version = "2.8.0"
26
+ actual_git_version = version_output.split[2]
27
+
28
+ if Gem::Version.new(expected_git_version) > Gem::Version.new(actual_git_version)
29
+ Printer.fail("Actual git version " + actual_git_version.bold + " is smaller than expected git version " + expected_git_version.bold)
30
+ abort()
31
+ else
32
+ Printer.success("Git version " + actual_git_version.bold + " found, and is higher than or equal to expected git version " + expected_git_version.bold)
33
+ end
34
+ end
35
+
36
+ def validate_changelog(base_dir, downstream_dir)
37
+ validate_exist(base_dir, "CHANGELOG.md", downstream_dir, ["release_notes.md"])
38
+
39
+ changelog_contents = get_changelog_contents(base_dir)
40
+ ValidatorChangelog.new.validate_changelog_contents(changelog_contents)
41
+ end
42
+
43
+ def validate_is_type(obj, type)
44
+ if !obj.is_a? type
45
+ Printer.fail("#{obj} is not a #{type}.")
46
+ abort()
47
+ end
48
+ end
49
+
50
+ def validate_method_convention(hash)
51
+ hash.each do |key, value|
52
+ if key.to_s.end_with? "_methods"
53
+ # validate that anything ending in _methods is a list of methods
54
+ if !value.respond_to? :each
55
+ Printer.fail("#{key} is not a list.")
56
+ abort()
57
+ end
58
+ value.each do |list_item|
59
+ validate_is_type list_item, Method
60
+ end
61
+ elsif key.to_s.end_with? "_method"
62
+ # anything ending in _method is a method
63
+ validate_is_type value, Method
64
+ else
65
+ # ignore everything else
66
+ end
67
+ end
68
+ end
69
+
70
+ def validate_required_configatron_key(key)
71
+ if !@releasinator_config.has_key?(key)
72
+ Printer.fail("No #{key} found in configatron.")
73
+ abort()
74
+ end
75
+ end
76
+
77
+ def validate_config()
78
+ validate_required_configatron_key(:product_name)
79
+ validate_required_configatron_key(:prerelease_checklist_items)
80
+ validate_required_configatron_key(:build_method)
81
+ validate_required_configatron_key(:publish_to_package_manager_method)
82
+ validate_required_configatron_key(:wait_for_package_manager_method)
83
+ validate_required_configatron_key(:release_to_github)
84
+
85
+ validate_method_convention(@releasinator_config)
86
+
87
+ if @releasinator_config.has_key? :downstream_repos
88
+ @releasinator_config[:downstream_repos].each do |downsteam_repo|
89
+ validate_is_type downsteam_repo, DownstreamRepo
90
+
91
+ validate_method_convention(downsteam_repo.options)
92
+ end
93
+ end
94
+ end
95
+
96
+ def validate_github_permissions(repo_url)
97
+ github_repo = GitHubRepo.new(repo_url)
98
+ github_client = github_repo.client
99
+
100
+ begin
101
+ # get the list of collaborators.
102
+ puts "Checking collaborators on #{repo_url}." if @releasinator_config[:verbose]
103
+ github_collaborators = github_client.collaborators "#{github_repo.org}/#{github_repo.repo}"
104
+ if ! github_collaborators
105
+ Printer.fail("request failed with code:#{res.code}\nbody:#{res.body}")
106
+ abort()
107
+ end
108
+ puts github_collaborators.inspect if @releasinator_config[:trace]
109
+ Printer.success("User has push permissions on #{repo_url}.")
110
+ rescue => error
111
+ #This will fail if the user does not have push permissions.
112
+ Printer.fail(error.inspect)
113
+ abort()
114
+ end
115
+ end
116
+
117
+ def validate_gitignore(line, is_downstream_present)
118
+ if !File.exist?(".gitignore")
119
+ FileUtils.touch('.gitignore')
120
+ CommandProcessor.command("git add . && git commit -m \"#{@releasinator_config[:releasinator_name]}: add .gitignore\"")
121
+ end
122
+
123
+ if is_downstream_present
124
+ if !line_match_in_file?(line, ".gitignore")
125
+ is_git_already_clean = GitUtil.new().is_clean_git?
126
+ File.open('.gitignore', 'a') do |f|
127
+ f.puts "# #{@releasinator_config[:releasinator_name]}"
128
+ f.puts line
129
+ end
130
+
131
+ if is_git_already_clean
132
+ CommandProcessor.command("git add . && git commit -m \"#{@releasinator_config[:releasinator_name]}: add downstream dir to .gitignore\"")
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ def line_match_in_file?(contains_string, filename)
139
+ File.open("#{filename}", "r") do |f|
140
+ f.each_line do |line|
141
+ if line.match /^#{Regexp.escape(contains_string)}$/
142
+ Printer.success("#{filename} contains #{contains_string}")
143
+ return true
144
+ end
145
+ end
146
+ end
147
+ false
148
+ end
149
+
150
+ def validate_referenced_in_readme(base_dir, filename)
151
+ Dir.chdir(base_dir) do
152
+ File.open("README.md", "r") do |f|
153
+ f.each_line do |line|
154
+ if line.include? "(#{filename})"
155
+ Printer.success("#{filename} referenced in #{base_dir}/README.md")
156
+ return
157
+ end
158
+ end
159
+ end
160
+ end
161
+ Printer.fail("Please link to the #{filename} file somewhere in #{base_dir}/README.md.")
162
+ abort()
163
+ end
164
+
165
+ def validate_exist(base_dir, expected_file_name, downstream_dir, alternate_names=[])
166
+ Dir.chdir(base_dir) do
167
+ if !File.exist?(expected_file_name)
168
+ puts "#{base_dir}/#{expected_file_name} not found. Searching for similar files.".yellow
169
+
170
+ # search for files that are somewhat similar to the file being searched, ignoring case
171
+ filename_prefix = expected_file_name[0,5]
172
+ similar_files = CommandProcessor.command("find . -type f -not -path \"./#{downstream_dir}/*\" -iname '#{filename_prefix}*'| sed 's|./||'").strip
173
+ num_similar_files = similar_files.split.count
174
+ puts similar_files
175
+ if num_similar_files == 1
176
+ Printer.check_proceed("Found a single similar file: #{similar_files}. Do you want to rename this to the expected #{expected_file_name}?","Please place #{base_dir}/#{expected_file_name}")
177
+ rename_file(similar_files, expected_file_name)
178
+ elsif num_similar_files > 1
179
+ Printer.fail("Found more than 1 file similar to #{expected_file_name}. Please rename one, and optionally remove the others to not confuse users.")
180
+ abort()
181
+ elsif !rename_alternate_name(expected_file_name, alternate_names)
182
+ Printer.fail("Please place #{base_dir}/#{expected_file_name}.")
183
+ abort()
184
+ end
185
+ end
186
+ Printer.success("#{base_dir}/#{expected_file_name} found.")
187
+ end
188
+ end
189
+
190
+ def validate_clean_git
191
+ untracked_files = GitUtil.untracked_files
192
+ diff = GitUtil.diff
193
+ diff_cached = GitUtil.cached
194
+
195
+ if '' != untracked_files
196
+ puts untracked_files.red if @releasinator_config[:verbose]
197
+ error = true
198
+ Printer.fail("Untracked files found.")
199
+ else
200
+ Printer.success("No untracked files found.")
201
+ end
202
+
203
+ if '' != diff
204
+ puts diff.red if @releasinator_config[:verbose]
205
+ error = true
206
+ Printer.fail("Unstaged changes found.")
207
+ else
208
+ Printer.success("No unstaged changes found.")
209
+ end
210
+
211
+ if '' != diff_cached
212
+ puts diff_cached.red if @releasinator_config[:verbose]
213
+ error = true
214
+ Printer.fail("Uncommitted changes found.")
215
+ else
216
+ Printer.success("No uncommitted changes found.")
217
+ end
218
+
219
+ abort() if error
220
+ end
221
+
222
+ class Submodule
223
+ attr_reader :name, :path, :url
224
+
225
+ def initialize(name, path, url)
226
+ @name=name
227
+ @path=path
228
+ @url=url
229
+ end
230
+ end
231
+
232
+ def validate_submodules
233
+ if File.exist?(".gitmodules")
234
+ submodules = Array.new
235
+
236
+ current_name = nil
237
+ current_path = nil
238
+ current_url = nil
239
+ File.open(".gitmodules", "r") do |f|
240
+ f.each_line do |line|
241
+
242
+ if line.include? "\""
243
+ current_name = line.strip.split(' ').last.to_s.split("\"").at(1)
244
+ elsif line.include? "path = "
245
+ current_path = line.strip.split(' ').last.to_s
246
+ elsif line.include? "url = "
247
+ current_url = line.strip.split(' ').last.to_s
248
+ submodules << Submodule.new(current_name, current_path, current_url)
249
+ end
250
+ end
251
+ end
252
+
253
+ Printer.success("Found " + submodules.count.to_s.bold + " submodules in .gitmodules.")
254
+ submodules.each do |submodule|
255
+ Dir.chdir(submodule.path) do
256
+ validate_matches_branch("master", "Submodule")
257
+ end
258
+ end
259
+ else
260
+ Printer.success("No submodules found.")
261
+ end
262
+ end
263
+
264
+ def validate_matches_branch(branch_name, console_prefix="Root")
265
+ current_dir = Dir.pwd
266
+
267
+ # Don't fetch the submodules, as they should already be fetched by the initial recursive fetch.
268
+ if console_prefix == "Root"
269
+ puts "fetching #{current_dir}" if @releasinator_config[:verbose]
270
+
271
+ # silently fails if it can't connect because sometimes we want to release even if
272
+ # corp GitHub is down.
273
+ `git fetch --recurse-submodules -j9`
274
+ end
275
+
276
+ validate_clean_git()
277
+
278
+ head_sha1 = `git rev-parse --verify head`.strip
279
+ origin_branch_sha1 = `git rev-parse --verify origin/#{branch_name}`.strip
280
+ if head_sha1 != origin_branch_sha1
281
+ abort_string = "#{console_prefix} #{current_dir} at #{head_sha1}, but origin/#{branch_name} is #{origin_branch_sha1}."\
282
+ "\nIf you received this error on the root project, you may need to:"\
283
+ "\n 1. pull the latest changes from the remote,"\
284
+ "\n 2. push changes up to the remote,"\
285
+ "\n 3. back out a current release in progress."
286
+ Printer.fail(abort_string)
287
+ abort()
288
+ else
289
+ Printer.success("#{console_prefix} #{current_dir} matches origin/#{branch_name}.")
290
+ end
291
+ end
292
+
293
+ def rename_file(old_name, new_name)
294
+ puts "Renaming #{old_name} to expected filename: #{new_name}".yellow
295
+ CommandProcessor.command("mv #{old_name} #{new_name}")
296
+ # fix any references to file in readme
297
+ replace_string("README.md", "(#{old_name})", "(#{new_name})")
298
+ CommandProcessor.command("git add . && git commit -m \"#{@config[:releasinator_name]}: rename #{old_name} to #{new_name}\"")
299
+ end
300
+
301
+ def rename_alternate_name(expected_file_name, alternate_names)
302
+ alternate_names.each do |name|
303
+ if '' != CommandProcessor.command("ls #{name}")
304
+ puts "Found similar file: #{name}."
305
+ rename_file(name, expected_file_name)
306
+ return true
307
+ end
308
+ end
309
+ false
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,82 @@
1
+ require 'colorize'
2
+ require 'Vandamme'
3
+ require 'semantic'
4
+ require_relative "current_release"
5
+ require_relative 'printer'
6
+
7
+ module Releasinator
8
+ class ValidatorChangelog
9
+
10
+ def validate_semver(changelog_hash, prefix="")
11
+ newer_version = nil
12
+ changelog_hash.each do |key,value|
13
+ if !key.start_with? prefix
14
+ Printer.fail("version #{key} does not start with prefix '#{prefix}'.")
15
+ abort()
16
+ end
17
+ older_version = Semantic::Version.new key[prefix.length..-1]
18
+
19
+ if nil != newer_version
20
+ version_comp = newer_version <=> older_version
21
+ if version_comp < 1
22
+ Printer.fail("Semver releases out of order: #{older_version} should be smaller than #{newer_version}")
23
+ abort()
24
+ end
25
+
26
+ error_suffix = "version increment error - comparing #{newer_version} to #{older_version} does not pass semver validation."
27
+ # validate the next sequence in semver
28
+ if newer_version.major == older_version.major
29
+ if newer_version.minor == older_version.minor
30
+ check_semver_criteria(newer_version.patch == older_version.patch + 1, "patch #{error_suffix}")
31
+ else
32
+ check_semver_criteria(newer_version.minor == older_version.minor + 1 && newer_version.patch == 0, "minor #{error_suffix}")
33
+ end
34
+ else
35
+ check_semver_criteria(newer_version.major == older_version.major + 1 && newer_version.minor == 0 && newer_version.patch == 0, "major #{error_suffix}")
36
+ end
37
+ end
38
+ newer_version = older_version
39
+ end
40
+ end
41
+
42
+ def check_semver_criteria(condition, message)
43
+ if !condition
44
+ Printer.fail(message)
45
+ abort()
46
+ end
47
+ end
48
+
49
+ def validate_changelog_contents(changelog_contents, prefix="")
50
+ version_header_regexes = [
51
+ ## h2 using --- separator. Example:
52
+ # 1.0.0
53
+ # -----
54
+ # First release!
55
+ '(^\d+\.\d+\.\d+).*\n----.*',
56
+
57
+ # h1/h2 header retrieved from https://github.com/tech-angels/vandamme/#format
58
+ '^#{0,3} ?([\w\d\.-]+\.[\w\d\.-]+[a-zA-Z0-9])(?: \/ (\w+ \d{1,2}(?:st|nd|rd|th)?,\s\d{4}|\d{4}-\d{2}-\d{2}|\w+))?\n?[=-]*'
59
+ ]
60
+
61
+ changelog_hash = nil
62
+ version_header_regexes.each do |version_header_regex|
63
+ parser = Vandamme::Parser.new(changelog: changelog_contents, version_header_exp: version_header_regex, format: 'markdown')
64
+ changelog_hash = parser.parse
65
+
66
+ break if !changelog_hash.empty?
67
+ end
68
+
69
+ if changelog_hash.empty?
70
+ Printer.fail("Unable to find any releases in the CHANGELOG.md. Please check that your formatting is correct.")
71
+ abort()
72
+ end
73
+
74
+ Printer.success("Found " + changelog_hash.count.to_s.bold + " release(s) in CHANGELOG.md.")
75
+
76
+ validate_semver(changelog_hash, prefix)
77
+
78
+ latest_release, latest_release_changelog = changelog_hash.first
79
+ CurrentRelease.new(latest_release, latest_release_changelog)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,39 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'releasinator/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "releasinator"
8
+ spec.version = Releasinator::VERSION
9
+ spec.authors = ["PayPal"]
10
+ spec.email = ["DL-PP-RUBY-SDK@paypal.com"]
11
+ spec.summary = %q{The releasinator assists in building and releasing SDKs across languages.}
12
+ spec.description = %q{The releasinator assists in building and releasing SDKs across languages.}
13
+ spec.homepage = "https://developer.paypal.com"
14
+ spec.license = "Apache-2.0"
15
+
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
+ else
21
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.11"
30
+ spec.add_development_dependency "rake", "~> 11.1"
31
+
32
+ spec.add_dependency "configatron", "~> 4.5"
33
+ spec.add_dependency "colorize", "~> 0.7"
34
+ spec.add_dependency "vandamme", "~> 0.0.11"
35
+ spec.add_dependency "semantic", "~> 1.4"
36
+ spec.add_dependency "json", "~> 1.8"
37
+ spec.add_dependency "octokit", "~> 4.0"
38
+
39
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: releasinator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - PayPal
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-04-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '11.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '11.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: configatron
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: colorize
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: vandamme
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.0.11
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.0.11
83
+ - !ruby/object:Gem::Dependency
84
+ name: semantic
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.4'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: json
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.8'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.8'
111
+ - !ruby/object:Gem::Dependency
112
+ name: octokit
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '4.0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '4.0'
125
+ description: The releasinator assists in building and releasing SDKs across languages.
126
+ email:
127
+ - DL-PP-RUBY-SDK@paypal.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - Gemfile
134
+ - Gemfile.lock
135
+ - README.md
136
+ - Rakefile
137
+ - lib/command_processor.rb
138
+ - lib/config_hash.rb
139
+ - lib/copy_file.rb
140
+ - lib/current_release.rb
141
+ - lib/default_config.rb
142
+ - lib/downstream_repo.rb
143
+ - lib/git_util.rb
144
+ - lib/github_repo.rb
145
+ - lib/printer.rb
146
+ - lib/publisher.rb
147
+ - lib/releasinator/version.rb
148
+ - lib/tasks/releasinator.rake
149
+ - lib/validator.rb
150
+ - lib/validator_changelog.rb
151
+ - releasinator.gemspec
152
+ homepage: https://developer.paypal.com
153
+ licenses:
154
+ - Apache-2.0
155
+ metadata:
156
+ allowed_push_host: https://rubygems.org
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubyforge_project:
173
+ rubygems_version: 2.5.1
174
+ signing_key:
175
+ specification_version: 4
176
+ summary: The releasinator assists in building and releasing SDKs across languages.
177
+ test_files: []