puma-release 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +114 -0
- data/exe/puma-release +7 -0
- data/lib/puma_release/agent_client.rb +163 -0
- data/lib/puma_release/build_support.rb +46 -0
- data/lib/puma_release/changelog_generator.rb +171 -0
- data/lib/puma_release/changelog_validator.rb +85 -0
- data/lib/puma_release/ci_checker.rb +108 -0
- data/lib/puma_release/cli.rb +52 -0
- data/lib/puma_release/commands/build.rb +68 -0
- data/lib/puma_release/commands/github.rb +76 -0
- data/lib/puma_release/commands/prepare.rb +178 -0
- data/lib/puma_release/commands/run.rb +51 -0
- data/lib/puma_release/context.rb +167 -0
- data/lib/puma_release/contributor_resolver.rb +52 -0
- data/lib/puma_release/error.rb +5 -0
- data/lib/puma_release/events.rb +18 -0
- data/lib/puma_release/git_repo.rb +169 -0
- data/lib/puma_release/github_client.rb +163 -0
- data/lib/puma_release/link_reference_builder.rb +49 -0
- data/lib/puma_release/options.rb +47 -0
- data/lib/puma_release/release_range.rb +69 -0
- data/lib/puma_release/repo_files.rb +85 -0
- data/lib/puma_release/shell.rb +107 -0
- data/lib/puma_release/stage_detector.rb +66 -0
- data/lib/puma_release/ui.rb +36 -0
- data/lib/puma_release/upgrade_guide_writer.rb +106 -0
- data/lib/puma_release/version.rb +5 -0
- data/lib/puma_release/version_recommender.rb +151 -0
- data/lib/puma_release.rb +28 -0
- data/test/test_helper.rb +72 -0
- data/test/unit/agent_client_test.rb +116 -0
- data/test/unit/build_command_test.rb +23 -0
- data/test/unit/build_support_test.rb +6 -0
- data/test/unit/changelog_validator_test.rb +42 -0
- data/test/unit/context_test.rb +209 -0
- data/test/unit/contributor_resolver_test.rb +47 -0
- data/test/unit/git_repo_test.rb +169 -0
- data/test/unit/github_client_test.rb +90 -0
- data/test/unit/github_command_test.rb +153 -0
- data/test/unit/options_test.rb +17 -0
- data/test/unit/prepare_test.rb +136 -0
- data/test/unit/repo_files_test.rb +119 -0
- data/test/unit/run_test.rb +32 -0
- data/test/unit/shell_test.rb +29 -0
- data/test/unit/stage_detector_test.rb +72 -0
- metadata +143 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module PumaRelease
|
|
6
|
+
class CiChecker
|
|
7
|
+
FAILURE_CONCLUSIONS = %w[failure cancelled timed_out action_required startup_failure stale].freeze
|
|
8
|
+
PENDING_STATUSES = %w[queued in_progress pending waiting requested].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :context
|
|
11
|
+
|
|
12
|
+
def initialize(context)
|
|
13
|
+
@context = context
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ensure_green!(sha)
|
|
17
|
+
debug("ensure_green! called with sha=#{sha}")
|
|
18
|
+
status = combined_status(sha)
|
|
19
|
+
debug("combined_status returned: #{status.inspect}")
|
|
20
|
+
return context.ui.info("CI is green.") if status == :success
|
|
21
|
+
return handle_unknown if status == :unknown
|
|
22
|
+
|
|
23
|
+
raise Error, "CI for #{sha[0, 12]} is #{status}. Stop and investigate before releasing."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def combined_status(sha)
|
|
29
|
+
status_url = "repos/#{context.metadata_repo}/commits/#{sha}/status"
|
|
30
|
+
runs_url = "repos/#{context.metadata_repo}/commits/#{sha}/check-runs"
|
|
31
|
+
|
|
32
|
+
debug("fetching commit status from: #{status_url}")
|
|
33
|
+
statuses = gh_json("gh", "api", status_url) || {}
|
|
34
|
+
debug("commit status response keys: #{statuses.keys.inspect}")
|
|
35
|
+
debug("commit status state: #{statuses["state"].inspect}")
|
|
36
|
+
debug("statuses array length: #{Array(statuses["statuses"]).length}")
|
|
37
|
+
Array(statuses["statuses"]).each_with_index do |item, i|
|
|
38
|
+
debug(" status[#{i}]: context=#{item["context"].inspect} state=#{item["state"].inspect}")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
debug("fetching check-runs from: #{runs_url}")
|
|
42
|
+
runs = gh_json("gh", "api", runs_url) || {}
|
|
43
|
+
debug("check-runs response keys: #{runs.keys.inspect}")
|
|
44
|
+
debug("check_runs array length: #{Array(runs["check_runs"]).length}")
|
|
45
|
+
Array(runs["check_runs"]).each_with_index do |run, i|
|
|
46
|
+
debug(" check_run[#{i}]: name=#{run["name"].inspect} status=#{run["status"].inspect} conclusion=#{run["conclusion"].inspect}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
contexts = Array(statuses["statuses"]).map { |item| item.fetch("state") }
|
|
50
|
+
conclusions = Array(runs["check_runs"]).flat_map { |run| [run["status"], run["conclusion"]].compact }
|
|
51
|
+
values = contexts + conclusions
|
|
52
|
+
|
|
53
|
+
debug("contexts from statuses: #{contexts.inspect}")
|
|
54
|
+
debug("conclusions from check-runs: #{conclusions.inspect}")
|
|
55
|
+
debug("combined values: #{values.inspect}")
|
|
56
|
+
|
|
57
|
+
if values.empty?
|
|
58
|
+
debug("returning :unknown — no values found")
|
|
59
|
+
return :unknown
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
failure_values = values.select { |v| FAILURE_CONCLUSIONS.include?(v) || v == "error" }
|
|
63
|
+
if failure_values.any?
|
|
64
|
+
debug("returning :failure — found failure values: #{failure_values.inspect}")
|
|
65
|
+
return :failure
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
pending_values = values.select { |v| PENDING_STATUSES.include?(v) }
|
|
69
|
+
if pending_values.any?
|
|
70
|
+
debug("returning :pending — found pending values: #{pending_values.inspect}")
|
|
71
|
+
return :pending
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
unrecognized = values - ["success", "neutral", "skipped", "completed"]
|
|
75
|
+
if unrecognized.empty?
|
|
76
|
+
debug("returning :success — all values recognized as success: #{values.inspect}")
|
|
77
|
+
return :success
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
debug("returning :unknown — unrecognized values: #{unrecognized.inspect}")
|
|
81
|
+
:unknown
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_unknown
|
|
85
|
+
return context.ui.warn("Could not determine CI status; continuing because --allow-unknown-ci was set.") if context.allow_unknown_ci?
|
|
86
|
+
raise Error, "Could not determine CI status. Re-run with --allow-unknown-ci if you want to proceed anyway."
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def gh_json(*command)
|
|
90
|
+
debug("gh_json running: #{command.join(" ")}")
|
|
91
|
+
result = context.shell.run(*command, allow_failure: true)
|
|
92
|
+
debug("gh_json exit status: #{result.exitstatus}, success: #{result.success?}")
|
|
93
|
+
unless result.success?
|
|
94
|
+
debug("gh_json stderr: #{result.stderr.strip.inspect}")
|
|
95
|
+
debug("gh_json stdout: #{result.stdout.strip.inspect}")
|
|
96
|
+
return nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
body = result.stdout.strip
|
|
100
|
+
debug("gh_json response body (first 500 chars): #{body[0, 500].inspect}")
|
|
101
|
+
body.empty? ? {} : JSON.parse(body)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def debug(message)
|
|
105
|
+
context.ui.debug(message) if context.debug?
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PumaRelease
|
|
4
|
+
class CLI
|
|
5
|
+
COMMANDS = {
|
|
6
|
+
"prepare" => Commands::Prepare,
|
|
7
|
+
"build" => Commands::Build,
|
|
8
|
+
"github" => Commands::Github,
|
|
9
|
+
"run" => Commands::Run
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :argv, :env
|
|
13
|
+
|
|
14
|
+
def initialize(argv, env: ENV)
|
|
15
|
+
@argv = argv
|
|
16
|
+
@env = env
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
options = Options.parse(argv)
|
|
21
|
+
context = Context.new(options, env:)
|
|
22
|
+
subscribe(context)
|
|
23
|
+
command = COMMANDS.fetch(options.fetch(:command)) { raise Error, usage }
|
|
24
|
+
command.new(context).call
|
|
25
|
+
rescue Error => e
|
|
26
|
+
UI.new.error(e.message)
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def subscribe(context)
|
|
33
|
+
context.events.subscribe(:checkpoint) do |_name, payload|
|
|
34
|
+
next unless payload[:kind] == :wait_for_merge
|
|
35
|
+
|
|
36
|
+
context.ui.info("Checkpoint: waiting for PR merge (#{payload[:pr_url]})")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def usage
|
|
41
|
+
[
|
|
42
|
+
"Usage: puma-release [options] [command]",
|
|
43
|
+
"",
|
|
44
|
+
"Commands:",
|
|
45
|
+
" prepare open the release PR and draft release",
|
|
46
|
+
" build tag the release and build both gem artifacts",
|
|
47
|
+
" github publish the GitHub release and upload assets",
|
|
48
|
+
" run detect the next step and run it (default)"
|
|
49
|
+
].join("\n")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PumaRelease
|
|
4
|
+
module Commands
|
|
5
|
+
class Build
|
|
6
|
+
attr_reader :context, :git_repo, :repo_files, :github
|
|
7
|
+
|
|
8
|
+
def initialize(context)
|
|
9
|
+
@context = context
|
|
10
|
+
@git_repo = GitRepo.new(context)
|
|
11
|
+
@repo_files = RepoFiles.new(context)
|
|
12
|
+
@github = GitHubClient.new(context)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
context.check_dependencies!("git", "gh", "bundle")
|
|
17
|
+
context.announce_live_mode!
|
|
18
|
+
context.ensure_release_writes_allowed!
|
|
19
|
+
git_repo.ensure_clean_base!
|
|
20
|
+
version = repo_files.current_version
|
|
21
|
+
tag = git_repo.release_tag(version)
|
|
22
|
+
context.ui.info("Ensuring tag #{tag} points at HEAD and is pushed...")
|
|
23
|
+
git_repo.ensure_release_tag_pushed!(tag)
|
|
24
|
+
sync_release_target_to_tag(tag)
|
|
25
|
+
sync_release_title(tag, version)
|
|
26
|
+
context.ui.info("Building MRI gem...")
|
|
27
|
+
context.shell.run("bundle", "exec", "rake", "build")
|
|
28
|
+
context.ui.info("Built: pkg/puma-#{version}.gem")
|
|
29
|
+
jruby_built = BuildSupport.new(context).build_jruby_gem(version)
|
|
30
|
+
manual_jruby_instructions unless jruby_built
|
|
31
|
+
context.events.publish(:checkpoint, kind: :wait_for_rubygems, version:, tag:)
|
|
32
|
+
context.ui.info("STOP: push both gems to RubyGems, then rerun puma-release.")
|
|
33
|
+
:wait_for_rubygems
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def sync_release_target_to_tag(tag)
|
|
39
|
+
release = github.release(tag)
|
|
40
|
+
return unless release
|
|
41
|
+
|
|
42
|
+
tag_sha = git_repo.local_tag_sha(tag)
|
|
43
|
+
raise Error, "Local tag #{tag} is missing." if tag_sha.empty?
|
|
44
|
+
return if release.fetch("targetCommitish", "") == tag_sha
|
|
45
|
+
|
|
46
|
+
context.ui.info("Updating release target for #{tag} to #{tag_sha[0, 12]}...")
|
|
47
|
+
github.edit_release_target(tag, tag_sha)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def sync_release_title(tag, version)
|
|
51
|
+
release = github.release(tag)
|
|
52
|
+
return unless release
|
|
53
|
+
|
|
54
|
+
title = repo_files.release_name(version)
|
|
55
|
+
return if release.fetch("name", "") == title
|
|
56
|
+
|
|
57
|
+
context.ui.info("Updating release title for #{tag} to #{title.inspect}...")
|
|
58
|
+
github.edit_release_title(tag, title)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def manual_jruby_instructions
|
|
62
|
+
context.ui.warn("JRuby gem was not built automatically.")
|
|
63
|
+
puts "To build it manually, switch to JRuby and run:"
|
|
64
|
+
puts " bundle exec rake java gem"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PumaRelease
|
|
4
|
+
module Commands
|
|
5
|
+
class Github
|
|
6
|
+
attr_reader :context, :git_repo, :repo_files, :github
|
|
7
|
+
|
|
8
|
+
def initialize(context)
|
|
9
|
+
@context = context
|
|
10
|
+
@git_repo = GitRepo.new(context)
|
|
11
|
+
@repo_files = RepoFiles.new(context)
|
|
12
|
+
@github = GitHubClient.new(context)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
context.check_dependencies!("git", "gh")
|
|
17
|
+
context.announce_live_mode!
|
|
18
|
+
context.ensure_release_writes_allowed!
|
|
19
|
+
git_repo.ensure_clean_base!
|
|
20
|
+
version = repo_files.current_version
|
|
21
|
+
tag = git_repo.release_tag(version)
|
|
22
|
+
proposal_tag = git_repo.proposal_tag(version)
|
|
23
|
+
artifact_paths = artifacts(version)
|
|
24
|
+
body = repo_files.extract_history_section(version) || raise(Error, "Could not find section for #{version} in #{context.history_file}")
|
|
25
|
+
title = repo_files.release_name(version)
|
|
26
|
+
git_repo.ensure_release_tag_pushed!(tag)
|
|
27
|
+
tag_sha = git_repo.local_tag_sha(tag)
|
|
28
|
+
raise Error, "Local tag #{tag} is missing." if tag_sha.empty?
|
|
29
|
+
|
|
30
|
+
release = ensure_release_for_final_tag(tag, proposal_tag, body, title, tag_sha)
|
|
31
|
+
release = github.edit_release_target(tag, tag_sha) if release.fetch("targetCommitish", "") != tag_sha
|
|
32
|
+
release = github.edit_release_title(tag, title) if release.fetch("name", "") != title
|
|
33
|
+
release = github.edit_release_notes(tag, body) if release.fetch("body", "") != body
|
|
34
|
+
github.upload_release_assets(tag, *artifact_paths)
|
|
35
|
+
release = github.publish_release(tag) if release.fetch("isDraft", false)
|
|
36
|
+
cleanup_proposal_release!(proposal_tag)
|
|
37
|
+
context.events.publish(:release_published, tag:, url: release.fetch("url"))
|
|
38
|
+
context.ui.info("GitHub release published: #{release.fetch("url")}")
|
|
39
|
+
:complete
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def ensure_release_for_final_tag(tag, proposal_tag, body, title, tag_sha)
|
|
45
|
+
release = github.release(tag)
|
|
46
|
+
proposal_release = github.release(proposal_tag)
|
|
47
|
+
raise Error, "Found both #{tag} and #{proposal_tag} releases. Clean up the proposal release before continuing." if release && proposal_release
|
|
48
|
+
return release if release
|
|
49
|
+
return promote_proposal_release!(proposal_tag, tag, tag_sha) if proposal_release
|
|
50
|
+
|
|
51
|
+
github.create_release(tag, body, title:, draft: true)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def promote_proposal_release!(proposal_tag, tag, tag_sha)
|
|
55
|
+
context.ui.info("Promoting draft release from #{proposal_tag} to #{tag}...")
|
|
56
|
+
github.retag_release(proposal_tag, tag, target: tag_sha)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cleanup_proposal_release!(proposal_tag)
|
|
60
|
+
github.delete_release(proposal_tag, allow_failure: true) if github.release(proposal_tag)
|
|
61
|
+
github.delete_tag_ref(proposal_tag, allow_failure: true) unless git_repo.remote_tag_sha(proposal_tag).empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def artifacts(version)
|
|
65
|
+
paths = [
|
|
66
|
+
context.repo_dir.join("pkg", "puma-#{version}.gem"),
|
|
67
|
+
context.repo_dir.join("pkg", "puma-#{version}-java.gem")
|
|
68
|
+
]
|
|
69
|
+
missing = paths.reject(&:file?)
|
|
70
|
+
raise Error, "Missing release artifact(s): #{missing.join(" ")}" unless missing.empty?
|
|
71
|
+
|
|
72
|
+
paths.map(&:to_s)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PumaRelease
|
|
4
|
+
module Commands
|
|
5
|
+
class Prepare
|
|
6
|
+
attr_reader :context, :git_repo, :repo_files, :github, :contributors
|
|
7
|
+
|
|
8
|
+
def initialize(context)
|
|
9
|
+
@context = context
|
|
10
|
+
@git_repo = GitRepo.new(context)
|
|
11
|
+
@repo_files = RepoFiles.new(context)
|
|
12
|
+
@github = GitHubClient.new(context)
|
|
13
|
+
@contributors = ContributorResolver.new(context, git_repo:, github:)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
context.check_dependencies!("git", "gh", context.agent_binary)
|
|
18
|
+
context.announce_live_mode!
|
|
19
|
+
context.ensure_release_writes_allowed!
|
|
20
|
+
git_repo.ensure_clean_base!
|
|
21
|
+
ensure_green_ci!
|
|
22
|
+
|
|
23
|
+
last_tag = git_repo.last_tag
|
|
24
|
+
context.ui.info("Last release tag: #{last_tag}")
|
|
25
|
+
release_range = ReleaseRange.new(context, last_tag)
|
|
26
|
+
recommendation = VersionRecommender.new(context, release_range).call
|
|
27
|
+
bump_type = recommendation.fetch("bump_type")
|
|
28
|
+
current_version = repo_files.current_version
|
|
29
|
+
new_version = git_repo.bump_version(current_version, bump_type)
|
|
30
|
+
context.ui.info("Version bump: #{current_version} -> #{new_version}")
|
|
31
|
+
show_version_recommendation(recommendation)
|
|
32
|
+
|
|
33
|
+
earner = show_codename_earner(last_tag, bump_type)
|
|
34
|
+
changelog = prepare_changelog(release_range, new_version, last_tag)
|
|
35
|
+
context.ui.info("Generating link references...")
|
|
36
|
+
refs = LinkReferenceBuilder.new(context).build(changelog)
|
|
37
|
+
repo_files.prepend_history_section!(new_version, changelog, refs)
|
|
38
|
+
repo_files.update_version!(new_version, bump_type, codename: context.codename)
|
|
39
|
+
upgrade_guide_path = write_upgrade_guide(release_range, new_version, recommendation, bump_type)
|
|
40
|
+
security_file = update_security_policy(new_version, bump_type)
|
|
41
|
+
|
|
42
|
+
branch = "release-v#{new_version}"
|
|
43
|
+
git_repo.checkout_release_branch!(branch)
|
|
44
|
+
git_repo.commit_release!(new_version, extra_files: [*Array(upgrade_guide_path), *Array(security_file)])
|
|
45
|
+
git_repo.push_branch!(branch)
|
|
46
|
+
|
|
47
|
+
compare_url = "https://github.com/#{context.metadata_repo}/compare/#{last_tag}...#{git_repo.head_sha}"
|
|
48
|
+
pr_url = github.create_release_pr("Release v#{new_version}", branch, body: compare_url)
|
|
49
|
+
github.comment_on_pr(pr_url, pr_comment(recommendation, earner))
|
|
50
|
+
release = ensure_draft_release(new_version, branch)
|
|
51
|
+
release_url = release.fetch("url")
|
|
52
|
+
github.update_pr_body(pr_url, "#{compare_url}\n\n#{release_url}")
|
|
53
|
+
context.events.publish(:checkpoint, kind: :wait_for_merge, pr_url:, release_url:, branch:)
|
|
54
|
+
|
|
55
|
+
context.ui.info("Release PR created: #{pr_url}")
|
|
56
|
+
context.ui.info("Draft GitHub release ready: #{release.fetch("url")}")
|
|
57
|
+
context.ui.warn(waiting_on_codename_message(earner)) if earner
|
|
58
|
+
context.ui.info("STOP: review and merge the PR, then rerun puma-release.")
|
|
59
|
+
:wait_for_merge
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def ensure_green_ci!
|
|
65
|
+
return context.ui.warn("Skipping CI check because --skip-ci-check was set.") if context.skip_ci_check?
|
|
66
|
+
|
|
67
|
+
context.ui.info("Checking CI status for HEAD...")
|
|
68
|
+
ci_checker.ensure_green!(git_repo.head_sha)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ci_checker = CiChecker.new(context)
|
|
72
|
+
|
|
73
|
+
def show_codename_earner(last_tag, bump_type)
|
|
74
|
+
return nil if bump_type == "patch"
|
|
75
|
+
return nil if context.codename
|
|
76
|
+
|
|
77
|
+
context.ui.info("Top contributors since #{last_tag}:")
|
|
78
|
+
git_repo.top_contributors_since(last_tag).first(5).each { |line| puts line }
|
|
79
|
+
earner = contributors.codename_earner(last_tag)
|
|
80
|
+
return nil unless earner
|
|
81
|
+
|
|
82
|
+
label = earner.fetch(:name)
|
|
83
|
+
label += " (@#{earner[:login]})" if earner[:login]
|
|
84
|
+
context.ui.info("Codename earner: #{label}")
|
|
85
|
+
earner
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def show_version_recommendation(recommendation)
|
|
89
|
+
context.ui.info("Version bump recommendation:")
|
|
90
|
+
puts recommendation.fetch("reasoning_markdown")
|
|
91
|
+
|
|
92
|
+
breaking_changes = recommendation.fetch("breaking_changes", [])
|
|
93
|
+
return if breaking_changes.empty?
|
|
94
|
+
|
|
95
|
+
context.ui.warn("Potential breaking changes:")
|
|
96
|
+
breaking_changes.each { |item| puts "- #{item}" }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def update_security_policy(new_version, bump_type)
|
|
100
|
+
return nil unless bump_type == "major"
|
|
101
|
+
|
|
102
|
+
repo_files.update_security!(new_version)
|
|
103
|
+
context.security_file
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def write_upgrade_guide(release_range, new_version, recommendation, bump_type)
|
|
107
|
+
return nil unless bump_type == "major"
|
|
108
|
+
|
|
109
|
+
UpgradeGuideWriter.new(
|
|
110
|
+
context,
|
|
111
|
+
release_range,
|
|
112
|
+
new_version:,
|
|
113
|
+
breaking_changes: recommendation.fetch("breaking_changes", []),
|
|
114
|
+
codename: context.codename
|
|
115
|
+
).call
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def prepare_changelog(release_range, new_version, last_tag)
|
|
119
|
+
tag = git_repo.proposal_tag(new_version)
|
|
120
|
+
git_repo.create_signed_tag!(tag, message: "Temporary changelog tag for #{tag}")
|
|
121
|
+
ChangelogGenerator.new(context, release_range, new_tag: tag, last_tag:).call
|
|
122
|
+
ensure
|
|
123
|
+
git_repo.delete_local_tag!(tag, allow_failure: true) if tag
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def ensure_draft_release(version, branch)
|
|
127
|
+
tag = git_repo.proposal_tag(version)
|
|
128
|
+
body = release_body(version)
|
|
129
|
+
title = repo_files.release_name(version)
|
|
130
|
+
release = github.release(tag)
|
|
131
|
+
release ||= github.create_release(tag, body, title:, draft: true, target: branch)
|
|
132
|
+
release = github.edit_release_target(tag, branch) if release.fetch("targetCommitish", "") != branch
|
|
133
|
+
release = github.edit_release_title(tag, title) if release.fetch("name", "") != title
|
|
134
|
+
(release.fetch("body", "") == body) ? release : github.edit_release_notes(tag, body)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def pr_comment(recommendation, earner)
|
|
138
|
+
lines = [
|
|
139
|
+
context.comment_attribution(recommendation.fetch("model_name", context.comment_author_model_name)),
|
|
140
|
+
"",
|
|
141
|
+
"## Version bump recommendation",
|
|
142
|
+
"",
|
|
143
|
+
"Recommended bump: **#{recommendation.fetch("bump_type")}**",
|
|
144
|
+
"",
|
|
145
|
+
recommendation.fetch("reasoning_markdown")
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
breaking_changes = recommendation.fetch("breaking_changes", [])
|
|
149
|
+
if breaking_changes.any?
|
|
150
|
+
lines += ["", "## Potential breaking changes", ""]
|
|
151
|
+
lines += breaking_changes.map { |item| "- #{item}" }
|
|
152
|
+
else
|
|
153
|
+
lines += ["", "## Potential breaking changes", "", "_None identified._"]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
return lines.join("\n") unless earner
|
|
157
|
+
|
|
158
|
+
[*lines, "", "## Codename", "", codename_message(earner)].join("\n")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def codename_message(earner)
|
|
162
|
+
return "@#{earner.fetch(:login)} earned the codename for this release. Please propose a codename!" if earner[:login]
|
|
163
|
+
|
|
164
|
+
"#{earner.fetch(:name)} earned the codename for this release. Please propose a codename!"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def waiting_on_codename_message(earner)
|
|
168
|
+
return "Waiting on @#{earner.fetch(:login)} for a codename before merging." if earner[:login]
|
|
169
|
+
|
|
170
|
+
"Waiting on #{earner.fetch(:name)} for a codename before merging."
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def release_body(version)
|
|
174
|
+
repo_files.extract_history_section(version) || raise(Error, "Could not find section for #{version} in #{context.history_file}")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PumaRelease
|
|
4
|
+
module Commands
|
|
5
|
+
class Run
|
|
6
|
+
attr_reader :context
|
|
7
|
+
|
|
8
|
+
def initialize(context)
|
|
9
|
+
@context = context
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
step = stage_detector.next_step
|
|
14
|
+
return wait_for_merge if step == :wait_for_merge
|
|
15
|
+
return complete if step == :complete
|
|
16
|
+
return run_step(step) if confirm_step(step)
|
|
17
|
+
|
|
18
|
+
:aborted
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def stage_detector = StageDetector.new(context)
|
|
24
|
+
|
|
25
|
+
def confirm_step(step)
|
|
26
|
+
return true if context.yes?
|
|
27
|
+
|
|
28
|
+
context.ui.confirm("Detected next step: #{step}. Continue?")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run_step(step)
|
|
32
|
+
case step
|
|
33
|
+
when :prepare then Prepare.new(context).call
|
|
34
|
+
when :build then Build.new(context).call
|
|
35
|
+
when :github then Github.new(context).call
|
|
36
|
+
else raise Error, "Unknown step: #{step}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def complete
|
|
41
|
+
context.ui.info("The current release is already complete. No action needed.")
|
|
42
|
+
:complete
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def wait_for_merge
|
|
46
|
+
context.ui.info("A release PR is already in flight. Merge it, update local #{context.base_branch}, and rerun puma-release.")
|
|
47
|
+
:wait_for_merge
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|