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,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
|
|
5
|
+
class GitRepoTest < Minitest::Test
|
|
6
|
+
def test_push_branch_uses_release_repo_remote_when_present
|
|
7
|
+
shell = FakeShell.new(
|
|
8
|
+
{
|
|
9
|
+
["git", "remote"] => "origin\nmine\n",
|
|
10
|
+
["git", "remote", "get-url", "origin"] => "https://github.com/puma/puma.git\n",
|
|
11
|
+
["git", "remote", "get-url", "mine"] => "git@github.com:nateberkopec/puma.git\n"
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma", metadata_repo: "puma/puma")
|
|
16
|
+
|
|
17
|
+
PumaRelease::GitRepo.new(context).push_branch!("release-v7.3.0")
|
|
18
|
+
|
|
19
|
+
assert_includes shell.commands, ["git", "push", "-u", "mine", "release-v7.3.0"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_push_branch_falls_back_to_release_repo_url_when_remote_is_missing
|
|
23
|
+
shell = FakeShell.new(
|
|
24
|
+
{
|
|
25
|
+
["git", "remote"] => "origin\n",
|
|
26
|
+
["git", "remote", "get-url", "origin"] => "git@github.com:puma/puma.git\n"
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma", metadata_repo: "puma/puma")
|
|
31
|
+
|
|
32
|
+
PumaRelease::GitRepo.new(context).push_branch!("release-v7.3.0")
|
|
33
|
+
|
|
34
|
+
assert_includes shell.commands, ["git", "push", "git@github.com:nateberkopec/puma.git", "release-v7.3.0"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_push_branch_confirms_the_full_git_command_in_live_mode
|
|
38
|
+
shell = FakeShell.new(
|
|
39
|
+
{
|
|
40
|
+
["git", "remote"] => "origin\nmine\n",
|
|
41
|
+
["git", "remote", "get-url", "origin"] => "https://github.com/puma/puma.git\n",
|
|
42
|
+
["git", "remote", "get-url", "mine"] => "git@github.com:nateberkopec/puma.git\n"
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
confirmations = []
|
|
46
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma", metadata_repo: "puma/puma")
|
|
47
|
+
context.define_singleton_method(:confirm_live_git_command!) { |*command| confirmations << command }
|
|
48
|
+
|
|
49
|
+
PumaRelease::GitRepo.new(context).push_branch!("release-v7.3.0")
|
|
50
|
+
|
|
51
|
+
assert_equal [["git", "push", "-u", "mine", "release-v7.3.0"]], confirmations
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_ensure_clean_base_prefers_metadata_repo_remote_when_present
|
|
55
|
+
shell = FakeShell.new(
|
|
56
|
+
{
|
|
57
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"] => "main\n",
|
|
58
|
+
["git", "status", "--porcelain"] => "",
|
|
59
|
+
["git", "remote"] => "origin\nupstream\n",
|
|
60
|
+
["git", "remote", "get-url", "origin"] => "git@github.com:nateberkopec/puma.git\n",
|
|
61
|
+
["git", "remote", "get-url", "upstream"] => "https://github.com/puma/puma.git\n",
|
|
62
|
+
["git", "rev-parse", "upstream/main"] => "abc123\n",
|
|
63
|
+
["git", "rev-parse", "HEAD"] => "abc123\n"
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma", metadata_repo: "puma/puma", base_branch: "main")
|
|
68
|
+
|
|
69
|
+
PumaRelease::GitRepo.new(context).ensure_clean_base!
|
|
70
|
+
|
|
71
|
+
assert_includes shell.commands, ["git", "fetch", "upstream", "--quiet"]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_commit_release_creates_a_signed_commit
|
|
75
|
+
temp_repo do |repo|
|
|
76
|
+
version_file = repo.join("lib/puma/const.rb")
|
|
77
|
+
history_file = repo.join("History.md")
|
|
78
|
+
version_file.write("")
|
|
79
|
+
history_file.write("")
|
|
80
|
+
shell = FakeShell.new
|
|
81
|
+
context = OpenStruct.new(shell:, version_file:, history_file:)
|
|
82
|
+
|
|
83
|
+
PumaRelease::GitRepo.new(context).commit_release!("7.3.0")
|
|
84
|
+
|
|
85
|
+
assert_includes shell.commands, ["git", "commit", "-S", "-m", "Release v7.3.0"]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_ensure_release_tag_pushed_creates_a_signed_tag
|
|
90
|
+
shell = FakeShell.new(
|
|
91
|
+
{
|
|
92
|
+
["git", "rev-parse", "HEAD"] => "abc123\n",
|
|
93
|
+
["git", "ls-remote", "--tags", "mine", "refs/tags/v7.3.0", "refs/tags/v7.3.0^{}"] => "",
|
|
94
|
+
["git", "remote"] => "origin\nmine\n",
|
|
95
|
+
["git", "remote", "get-url", "origin"] => "https://github.com/puma/puma.git\n",
|
|
96
|
+
["git", "remote", "get-url", "mine"] => "git@github.com:nateberkopec/puma.git\n"
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma", metadata_repo: "puma/puma")
|
|
101
|
+
|
|
102
|
+
PumaRelease::GitRepo.new(context).ensure_release_tag_pushed!("v7.3.0")
|
|
103
|
+
|
|
104
|
+
assert_includes shell.commands, ["git", "tag", "-s", "v7.3.0", "-m", "Release v7.3.0"]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_remote_tag_sha_returns_the_peeled_commit_for_signed_tags
|
|
108
|
+
shell = FakeShell.new(
|
|
109
|
+
{
|
|
110
|
+
["git", "ls-remote", "--tags", "mine", "refs/tags/v7.3.0", "refs/tags/v7.3.0^{}"] => [
|
|
111
|
+
"deadbeef\trefs/tags/v7.3.0",
|
|
112
|
+
"abc123\trefs/tags/v7.3.0^{}"
|
|
113
|
+
].join("\n"),
|
|
114
|
+
["git", "remote"] => "origin\nmine\n",
|
|
115
|
+
["git", "remote", "get-url", "origin"] => "https://github.com/puma/puma.git\n",
|
|
116
|
+
["git", "remote", "get-url", "mine"] => "git@github.com:nateberkopec/puma.git\n"
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma", metadata_repo: "puma/puma")
|
|
121
|
+
|
|
122
|
+
assert_equal "abc123", PumaRelease::GitRepo.new(context).remote_tag_sha("v7.3.0")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_ensure_release_tag_pushed_rejects_an_unsigned_local_tag
|
|
126
|
+
shell = FakeShell.new(
|
|
127
|
+
{
|
|
128
|
+
["git", "rev-parse", "HEAD"] => "abc123\n",
|
|
129
|
+
["git", "rev-parse", "-q", "--verify", "refs/tags/v7.3.0^{commit}"] => "abc123\n",
|
|
130
|
+
["git", "ls-remote", "--tags", "mine", "refs/tags/v7.3.0", "refs/tags/v7.3.0^{}"] => "",
|
|
131
|
+
["git", "cat-file", "-p", "refs/tags/v7.3.0"] => "tree deadbeef\nauthor Example <example@test> 0 +0000\n\nnot signed\n",
|
|
132
|
+
["git", "remote"] => "origin\nmine\n",
|
|
133
|
+
["git", "remote", "get-url", "origin"] => "https://github.com/puma/puma.git\n",
|
|
134
|
+
["git", "remote", "get-url", "mine"] => "git@github.com:nateberkopec/puma.git\n"
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma", metadata_repo: "puma/puma")
|
|
139
|
+
|
|
140
|
+
error = assert_raises(PumaRelease::Error) { PumaRelease::GitRepo.new(context).ensure_release_tag_pushed!("v7.3.0") }
|
|
141
|
+
|
|
142
|
+
assert_includes error.message, "is not GPG-signed"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_ensure_release_tag_pushed_rejects_a_remote_tag_that_differs_from_the_local_signed_tag
|
|
146
|
+
shell = FakeShell.new(
|
|
147
|
+
{
|
|
148
|
+
["git", "rev-parse", "HEAD"] => "abc123\n",
|
|
149
|
+
["git", "rev-parse", "-q", "--verify", "refs/tags/v7.3.0^{commit}"] => "abc123\n",
|
|
150
|
+
["git", "rev-parse", "-q", "--verify", "refs/tags/v7.3.0"] => "localtag\n",
|
|
151
|
+
["git", "ls-remote", "--tags", "mine", "refs/tags/v7.3.0", "refs/tags/v7.3.0^{}"] => [
|
|
152
|
+
"remotetag\trefs/tags/v7.3.0",
|
|
153
|
+
"abc123\trefs/tags/v7.3.0^{}"
|
|
154
|
+
].join("\n"),
|
|
155
|
+
["git", "ls-remote", "--tags", "mine", "refs/tags/v7.3.0"] => "remotetag\trefs/tags/v7.3.0\n",
|
|
156
|
+
["git", "cat-file", "-p", "refs/tags/v7.3.0"] => "object abc123\ntype commit\ntag v7.3.0\n\n-----BEGIN PGP SIGNATURE-----\n",
|
|
157
|
+
["git", "remote"] => "origin\nmine\n",
|
|
158
|
+
["git", "remote", "get-url", "origin"] => "https://github.com/puma/puma.git\n",
|
|
159
|
+
["git", "remote", "get-url", "mine"] => "git@github.com:nateberkopec/puma.git\n"
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma", metadata_repo: "puma/puma")
|
|
164
|
+
|
|
165
|
+
error = assert_raises(PumaRelease::Error) { PumaRelease::GitRepo.new(context).ensure_release_tag_pushed!("v7.3.0") }
|
|
166
|
+
|
|
167
|
+
assert_includes error.message, "does not match the local signed tag"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
|
|
5
|
+
class GitHubClientTest < Minitest::Test
|
|
6
|
+
def test_edit_release_target_passes_target_to_gh
|
|
7
|
+
shell = FakeShell.new(
|
|
8
|
+
{
|
|
9
|
+
["gh", "release", "edit", "v7.3.0", "--repo", "nateberkopec/puma", "--target", "abc123"] => FakeShell::Result.new(stdout: "", stderr: "", success?: true, exitstatus: 0),
|
|
10
|
+
["gh", "release", "view", "v7.3.0", "--repo", "nateberkopec/puma", "--json", "tagName,name,isDraft,body,url,assets,targetCommitish"] => FakeShell::Result.new(stdout: '{"tagName":"v7.3.0","targetCommitish":"abc123"}', stderr: "", success?: true, exitstatus: 0)
|
|
11
|
+
}
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
confirmations = []
|
|
15
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma")
|
|
16
|
+
context.define_singleton_method(:confirm_live_gh_command!) { |*command| confirmations << command }
|
|
17
|
+
release = PumaRelease::GitHubClient.new(context).edit_release_target("v7.3.0", "abc123")
|
|
18
|
+
|
|
19
|
+
assert_equal [["gh", "release", "edit", "v7.3.0", "--repo", "nateberkopec/puma", "--target", "abc123"]], confirmations
|
|
20
|
+
assert_equal "abc123", release.fetch("targetCommitish")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_edit_release_title_passes_title_to_gh
|
|
24
|
+
shell = FakeShell.new(
|
|
25
|
+
{
|
|
26
|
+
["gh", "release", "edit", "v7.3.0", "--repo", "nateberkopec/puma", "--title", "v7.3.0 - INSERT CODENAME HERE"] => FakeShell::Result.new(stdout: "", stderr: "", success?: true, exitstatus: 0),
|
|
27
|
+
["gh", "release", "view", "v7.3.0", "--repo", "nateberkopec/puma", "--json", "tagName,name,isDraft,body,url,assets,targetCommitish"] => FakeShell::Result.new(stdout: '{"tagName":"v7.3.0","name":"v7.3.0 - INSERT CODENAME HERE"}', stderr: "", success?: true, exitstatus: 0)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
confirmations = []
|
|
32
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma")
|
|
33
|
+
context.define_singleton_method(:confirm_live_gh_command!) { |*command| confirmations << command }
|
|
34
|
+
release = PumaRelease::GitHubClient.new(context).edit_release_title("v7.3.0", "v7.3.0 - INSERT CODENAME HERE")
|
|
35
|
+
|
|
36
|
+
assert_equal [["gh", "release", "edit", "v7.3.0", "--repo", "nateberkopec/puma", "--title", "v7.3.0 - INSERT CODENAME HERE"]], confirmations
|
|
37
|
+
assert_equal "v7.3.0 - INSERT CODENAME HERE", release.fetch("name")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_create_release_pr_confirms_before_writing
|
|
41
|
+
shell = FakeShell.new(
|
|
42
|
+
{
|
|
43
|
+
["gh", "pr", "create", "--repo", "puma/puma", "--base", "main", "--head", "release-v7.3.0", "--title", "Release v7.3.0", "--body", "compare"] => "https://github.com/puma/puma/pull/1\n"
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
confirmations = []
|
|
48
|
+
context = OpenStruct.new(shell:, release_repo: "puma/puma", base_branch: "main")
|
|
49
|
+
context.define_singleton_method(:confirm_live_gh_command!) { |*command| confirmations << command }
|
|
50
|
+
|
|
51
|
+
pr_url = PumaRelease::GitHubClient.new(context).create_release_pr("Release v7.3.0", "release-v7.3.0", body: "compare")
|
|
52
|
+
|
|
53
|
+
assert_equal [["gh", "pr", "create", "--repo", "puma/puma", "--base", "main", "--head", "release-v7.3.0", "--title", "Release v7.3.0", "--body", "compare"]], confirmations
|
|
54
|
+
assert_equal "https://github.com/puma/puma/pull/1", pr_url
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_retag_release_updates_the_release_tag_name_via_api
|
|
58
|
+
shell = FakeShell.new(
|
|
59
|
+
{
|
|
60
|
+
["gh", "api", "repos/nateberkopec/puma/releases/tags/v7.3.0-proposal"] => FakeShell::Result.new(stdout: '{"id":123}', stderr: "", success?: true, exitstatus: 0),
|
|
61
|
+
["gh", "api", "-X", "PATCH", "repos/nateberkopec/puma/releases/123", "-f", "tag_name=v7.3.0", "-f", "target_commitish=abc123"] => FakeShell::Result.new(stdout: "", stderr: "", success?: true, exitstatus: 0),
|
|
62
|
+
["gh", "release", "view", "v7.3.0", "--repo", "nateberkopec/puma", "--json", "tagName,name,isDraft,body,url,assets,targetCommitish"] => FakeShell::Result.new(stdout: '{"tagName":"v7.3.0","targetCommitish":"abc123"}', stderr: "", success?: true, exitstatus: 0)
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
confirmations = []
|
|
67
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma")
|
|
68
|
+
context.define_singleton_method(:confirm_live_gh_command!) { |*command| confirmations << command }
|
|
69
|
+
|
|
70
|
+
release = PumaRelease::GitHubClient.new(context).retag_release("v7.3.0-proposal", "v7.3.0", target: "abc123")
|
|
71
|
+
|
|
72
|
+
assert_equal [["gh", "api", "-X", "PATCH", "repos/nateberkopec/puma/releases/123", "-f", "tag_name=v7.3.0", "-f", "target_commitish=abc123"]], confirmations
|
|
73
|
+
assert_equal "v7.3.0", release.fetch("tagName")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_delete_tag_ref_deletes_a_remote_tag_ref_via_api
|
|
77
|
+
shell = FakeShell.new(
|
|
78
|
+
{
|
|
79
|
+
["gh", "api", "-X", "DELETE", "repos/nateberkopec/puma/git/refs/tags/v7.3.0-proposal"] => FakeShell::Result.new(stdout: "", stderr: "", success?: true, exitstatus: 0)
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
confirmations = []
|
|
84
|
+
context = OpenStruct.new(shell:, release_repo: "nateberkopec/puma")
|
|
85
|
+
context.define_singleton_method(:confirm_live_gh_command!) { |*command| confirmations << command }
|
|
86
|
+
|
|
87
|
+
assert PumaRelease::GitHubClient.new(context).delete_tag_ref("v7.3.0-proposal")
|
|
88
|
+
assert_equal [["gh", "api", "-X", "DELETE", "repos/nateberkopec/puma/git/refs/tags/v7.3.0-proposal"]], confirmations
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
|
|
5
|
+
class GithubCommandTest < Minitest::Test
|
|
6
|
+
def test_checks_git_and_gh_and_requires_a_clean_main_checkout
|
|
7
|
+
calls = []
|
|
8
|
+
context = Object.new
|
|
9
|
+
context.define_singleton_method(:check_dependencies!) { |*commands| calls << commands }
|
|
10
|
+
context.define_singleton_method(:announce_live_mode!) {}
|
|
11
|
+
context.define_singleton_method(:ensure_release_writes_allowed!) {}
|
|
12
|
+
|
|
13
|
+
git_repo = Object.new
|
|
14
|
+
git_repo.define_singleton_method(:ensure_clean_base!) { calls << :clean_main }
|
|
15
|
+
|
|
16
|
+
repo_files = Object.new
|
|
17
|
+
repo_files.define_singleton_method(:current_version) { raise PumaRelease::Error, "stop" }
|
|
18
|
+
|
|
19
|
+
command = PumaRelease::Commands::Github.allocate
|
|
20
|
+
command.instance_variable_set(:@context, context)
|
|
21
|
+
command.instance_variable_set(:@git_repo, git_repo)
|
|
22
|
+
command.instance_variable_set(:@repo_files, repo_files)
|
|
23
|
+
command.instance_variable_set(:@github, Object.new)
|
|
24
|
+
|
|
25
|
+
error = assert_raises(PumaRelease::Error) { command.call }
|
|
26
|
+
|
|
27
|
+
assert_equal "stop", error.message
|
|
28
|
+
assert_includes calls, ["git", "gh"]
|
|
29
|
+
assert_includes calls, :clean_main
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_ensures_the_signed_tag_is_pushed_before_creating_a_release
|
|
33
|
+
Dir.mktmpdir do |dir|
|
|
34
|
+
repo_dir = Pathname(dir)
|
|
35
|
+
context = OpenStruct.new(
|
|
36
|
+
repo_dir:,
|
|
37
|
+
history_file: repo_dir.join("History.md"),
|
|
38
|
+
events: Object.new,
|
|
39
|
+
ui: Object.new
|
|
40
|
+
)
|
|
41
|
+
context.history_file.write("## 7.2.0 / 2026-01-20\n\n* Bugfixes\n * One fix ([#1])\n")
|
|
42
|
+
context.define_singleton_method(:check_dependencies!) { |_git, _gh| }
|
|
43
|
+
context.define_singleton_method(:announce_live_mode!) {}
|
|
44
|
+
context.define_singleton_method(:ensure_release_writes_allowed!) {}
|
|
45
|
+
context.events.define_singleton_method(:publish) { |_name, _payload| }
|
|
46
|
+
context.ui.define_singleton_method(:info) { |_message| }
|
|
47
|
+
|
|
48
|
+
git_repo = Object.new
|
|
49
|
+
git_repo.define_singleton_method(:ensure_clean_base!) {}
|
|
50
|
+
git_repo.define_singleton_method(:release_tag) { |_version| "v7.2.0" }
|
|
51
|
+
git_repo.define_singleton_method(:proposal_tag) { |_version| "v7.2.0-proposal" }
|
|
52
|
+
git_repo.define_singleton_method(:ensure_release_tag_pushed!) { |_tag| raise PumaRelease::Error, "stop" }
|
|
53
|
+
|
|
54
|
+
repo_files = Object.new
|
|
55
|
+
repo_files.define_singleton_method(:current_version) { "7.2.0" }
|
|
56
|
+
repo_files.define_singleton_method(:extract_history_section) { |_version| "* Bugfixes\n * One fix ([#1])" }
|
|
57
|
+
repo_files.define_singleton_method(:release_name) { |_version| "v7.2.0" }
|
|
58
|
+
|
|
59
|
+
github = Object.new
|
|
60
|
+
github.define_singleton_method(:create_release) { |_tag, _body, title:, draft:| flunk "create_release should not be called before the tag is ensured" }
|
|
61
|
+
github.define_singleton_method(:release) { |_tag| nil }
|
|
62
|
+
|
|
63
|
+
command = PumaRelease::Commands::Github.allocate
|
|
64
|
+
command.instance_variable_set(:@context, context)
|
|
65
|
+
command.instance_variable_set(:@git_repo, git_repo)
|
|
66
|
+
command.instance_variable_set(:@repo_files, repo_files)
|
|
67
|
+
command.instance_variable_set(:@github, github)
|
|
68
|
+
|
|
69
|
+
repo_dir.join("pkg").mkpath
|
|
70
|
+
repo_dir.join("pkg/puma-7.2.0.gem").write("")
|
|
71
|
+
repo_dir.join("pkg/puma-7.2.0-java.gem").write("")
|
|
72
|
+
|
|
73
|
+
error = assert_raises(PumaRelease::Error) { command.call }
|
|
74
|
+
|
|
75
|
+
assert_equal "stop", error.message
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_promotes_the_proposal_release_to_the_final_tag
|
|
80
|
+
Dir.mktmpdir do |dir|
|
|
81
|
+
repo_dir = Pathname(dir)
|
|
82
|
+
context = OpenStruct.new(
|
|
83
|
+
repo_dir:,
|
|
84
|
+
history_file: repo_dir.join("History.md"),
|
|
85
|
+
events: Object.new,
|
|
86
|
+
ui: Object.new
|
|
87
|
+
)
|
|
88
|
+
context.history_file.write("## 7.2.0 / 2026-01-20\n\n* Bugfixes\n * One fix ([#1])\n")
|
|
89
|
+
published = []
|
|
90
|
+
infos = []
|
|
91
|
+
context.define_singleton_method(:check_dependencies!) { |_git, _gh| }
|
|
92
|
+
context.define_singleton_method(:announce_live_mode!) {}
|
|
93
|
+
context.define_singleton_method(:ensure_release_writes_allowed!) {}
|
|
94
|
+
context.events.define_singleton_method(:publish) { |name, payload| published << [name, payload] }
|
|
95
|
+
context.ui.define_singleton_method(:info) { |message| infos << message }
|
|
96
|
+
|
|
97
|
+
git_repo = Object.new
|
|
98
|
+
git_repo.define_singleton_method(:ensure_clean_base!) {}
|
|
99
|
+
git_repo.define_singleton_method(:release_tag) { |_version| "v7.2.0" }
|
|
100
|
+
git_repo.define_singleton_method(:proposal_tag) { |_version| "v7.2.0-proposal" }
|
|
101
|
+
git_repo.define_singleton_method(:ensure_release_tag_pushed!) { |_tag| }
|
|
102
|
+
git_repo.define_singleton_method(:local_tag_sha) { |_tag| "abc123" }
|
|
103
|
+
git_repo.define_singleton_method(:remote_tag_sha) { |_tag| "proposal123" }
|
|
104
|
+
|
|
105
|
+
repo_files = Object.new
|
|
106
|
+
repo_files.define_singleton_method(:current_version) { "7.2.0" }
|
|
107
|
+
repo_files.define_singleton_method(:extract_history_section) { |_version| "* Bugfixes\n * One fix ([#1])" }
|
|
108
|
+
repo_files.define_singleton_method(:release_name) { |_version| "v7.2.0" }
|
|
109
|
+
|
|
110
|
+
calls = []
|
|
111
|
+
github = Object.new
|
|
112
|
+
github.define_singleton_method(:release) do |tag|
|
|
113
|
+
case tag
|
|
114
|
+
when "v7.2.0" then nil
|
|
115
|
+
when "v7.2.0-proposal" then {"isDraft" => true, "targetCommitish" => "release-v7.2.0", "name" => "v7.2.0", "body" => "* Bugfixes\n * One fix ([#1])", "url" => "https://example.test/release"}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
github.define_singleton_method(:retag_release) do |old_tag, new_tag, target:|
|
|
119
|
+
calls << [:retag_release, old_tag, new_tag, target]
|
|
120
|
+
{"isDraft" => true, "targetCommitish" => target, "name" => "v7.2.0", "body" => "* Bugfixes\n * One fix ([#1])", "url" => "https://example.test/release"}
|
|
121
|
+
end
|
|
122
|
+
github.define_singleton_method(:edit_release_target) { |_tag, _target| flunk "edit_release_target should not be called when retag_release already set the final target" }
|
|
123
|
+
github.define_singleton_method(:edit_release_title) { |_tag, _title| flunk "edit_release_title should not be called when the release title already matches" }
|
|
124
|
+
github.define_singleton_method(:edit_release_notes) { |_tag, _body| flunk "edit_release_notes should not be called when the release notes already match" }
|
|
125
|
+
github.define_singleton_method(:upload_release_assets) { |tag, *paths| calls << [:upload_release_assets, tag, paths.map { |path| File.basename(path) }] }
|
|
126
|
+
github.define_singleton_method(:publish_release) do |tag|
|
|
127
|
+
calls << [:publish_release, tag]
|
|
128
|
+
{"isDraft" => false, "url" => "https://example.test/release"}
|
|
129
|
+
end
|
|
130
|
+
github.define_singleton_method(:delete_release) { |_tag, allow_failure:| calls << [:delete_release, allow_failure] }
|
|
131
|
+
github.define_singleton_method(:delete_tag_ref) { |tag, allow_failure:| calls << [:delete_tag_ref, tag, allow_failure] }
|
|
132
|
+
github.define_singleton_method(:create_release) { |_tag, _body, title:, draft:| flunk "create_release should not be called when a proposal release already exists" }
|
|
133
|
+
|
|
134
|
+
command = PumaRelease::Commands::Github.allocate
|
|
135
|
+
command.instance_variable_set(:@context, context)
|
|
136
|
+
command.instance_variable_set(:@git_repo, git_repo)
|
|
137
|
+
command.instance_variable_set(:@repo_files, repo_files)
|
|
138
|
+
command.instance_variable_set(:@github, github)
|
|
139
|
+
|
|
140
|
+
repo_dir.join("pkg").mkpath
|
|
141
|
+
repo_dir.join("pkg/puma-7.2.0.gem").write("")
|
|
142
|
+
repo_dir.join("pkg/puma-7.2.0-java.gem").write("")
|
|
143
|
+
|
|
144
|
+
assert_equal :complete, command.call
|
|
145
|
+
assert_includes infos, "Promoting draft release from v7.2.0-proposal to v7.2.0..."
|
|
146
|
+
assert_includes calls, [:retag_release, "v7.2.0-proposal", "v7.2.0", "abc123"]
|
|
147
|
+
assert_includes calls, [:upload_release_assets, "v7.2.0", ["puma-7.2.0.gem", "puma-7.2.0-java.gem"]]
|
|
148
|
+
assert_includes calls, [:publish_release, "v7.2.0"]
|
|
149
|
+
assert_includes calls, [:delete_tag_ref, "v7.2.0-proposal", true]
|
|
150
|
+
assert_equal [[:release_published, {tag: "v7.2.0", url: "https://example.test/release"}]], published
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
|
|
5
|
+
class OptionsTest < Minitest::Test
|
|
6
|
+
def test_parse_sets_live_flag
|
|
7
|
+
options = PumaRelease::Options.parse(["--live"])
|
|
8
|
+
|
|
9
|
+
assert_equal true, options.fetch(:live)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_parse_sets_skip_ci_check_flag
|
|
13
|
+
options = PumaRelease::Options.parse(["--skip-ci-check"])
|
|
14
|
+
|
|
15
|
+
assert_equal true, options.fetch(:skip_ci_check)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
|
|
5
|
+
class PrepareTest < Minitest::Test
|
|
6
|
+
class FakeUI
|
|
7
|
+
attr_reader :infos, :warnings
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@infos = []
|
|
11
|
+
@warnings = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def info(message)
|
|
15
|
+
infos << message
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def warn(message)
|
|
19
|
+
warnings << message
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_ensure_green_ci_skips_when_flag_is_set
|
|
24
|
+
ui = FakeUI.new
|
|
25
|
+
context = OpenStruct.new(skip_ci_check?: true, ui:)
|
|
26
|
+
git_repo = OpenStruct.new(head_sha: "abc123")
|
|
27
|
+
prepare = PumaRelease::Commands::Prepare.allocate
|
|
28
|
+
prepare.instance_variable_set(:@context, context)
|
|
29
|
+
prepare.instance_variable_set(:@git_repo, git_repo)
|
|
30
|
+
|
|
31
|
+
checker = Object.new
|
|
32
|
+
def checker.ensure_green!(_sha)
|
|
33
|
+
flunk "CiChecker should not be called when CI is skipped"
|
|
34
|
+
end
|
|
35
|
+
prepare.define_singleton_method(:ci_checker) { checker }
|
|
36
|
+
|
|
37
|
+
prepare.send(:ensure_green_ci!)
|
|
38
|
+
|
|
39
|
+
assert_equal ["Skipping CI check because --skip-ci-check was set."], ui.warnings
|
|
40
|
+
assert_empty ui.infos
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_ensure_green_ci_checks_head_when_flag_is_not_set
|
|
44
|
+
ui = FakeUI.new
|
|
45
|
+
context = OpenStruct.new(skip_ci_check?: false, ui:)
|
|
46
|
+
git_repo = OpenStruct.new(head_sha: "abc123")
|
|
47
|
+
prepare = PumaRelease::Commands::Prepare.allocate
|
|
48
|
+
prepare.instance_variable_set(:@context, context)
|
|
49
|
+
prepare.instance_variable_set(:@git_repo, git_repo)
|
|
50
|
+
|
|
51
|
+
checker = Object.new
|
|
52
|
+
calls = []
|
|
53
|
+
checker.define_singleton_method(:ensure_green!) { |sha| calls << sha }
|
|
54
|
+
prepare.define_singleton_method(:ci_checker) { checker }
|
|
55
|
+
|
|
56
|
+
prepare.send(:ensure_green_ci!)
|
|
57
|
+
|
|
58
|
+
assert_equal ["Checking CI status for HEAD..."], ui.infos
|
|
59
|
+
assert_equal ["abc123"], calls
|
|
60
|
+
assert_empty ui.warnings
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_pr_comment_starts_with_llm_attribution
|
|
64
|
+
context = Object.new
|
|
65
|
+
context.define_singleton_method(:comment_attribution) do |model_name|
|
|
66
|
+
"This comment was written by #{model_name} working on behalf of [puma-release](https://github.com/nateberkopec/puma-release)."
|
|
67
|
+
end
|
|
68
|
+
context.define_singleton_method(:comment_author_model_name) { "fallback-model" }
|
|
69
|
+
prepare = PumaRelease::Commands::Prepare.allocate
|
|
70
|
+
prepare.instance_variable_set(:@context, context)
|
|
71
|
+
|
|
72
|
+
comment = prepare.send(
|
|
73
|
+
:pr_comment,
|
|
74
|
+
{
|
|
75
|
+
"model_name" => "openai-codex/gpt-5.4",
|
|
76
|
+
"bump_type" => "minor",
|
|
77
|
+
"reasoning_markdown" => "Because of [this commit](https://github.com/puma/puma/commit/abc)."
|
|
78
|
+
},
|
|
79
|
+
nil
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert_match(%r{\AThis comment was written by openai-codex/gpt-5\.4 working on behalf of \[puma-release\]\(https://github.com/nateberkopec/puma-release\)\.}, comment)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_ensure_draft_release_uses_the_proposal_tag
|
|
86
|
+
git_repo = Object.new
|
|
87
|
+
git_repo.define_singleton_method(:proposal_tag) { |_version| "v7.3.0-proposal" }
|
|
88
|
+
|
|
89
|
+
repo_files = Object.new
|
|
90
|
+
repo_files.define_singleton_method(:release_name) { |_version| "v7.3.0 - Example" }
|
|
91
|
+
repo_files.define_singleton_method(:extract_history_section) { |_version| "* Features\n * Example ([#1])" }
|
|
92
|
+
|
|
93
|
+
calls = []
|
|
94
|
+
github = Object.new
|
|
95
|
+
github.define_singleton_method(:release) { |_tag| nil }
|
|
96
|
+
github.define_singleton_method(:create_release) do |tag, body, title:, draft:, target:|
|
|
97
|
+
calls << [:create_release, tag, body, title, draft, target]
|
|
98
|
+
{"name" => title, "body" => body, "targetCommitish" => target}
|
|
99
|
+
end
|
|
100
|
+
github.define_singleton_method(:edit_release_target) { |_tag, _target| flunk "edit_release_target should not be called when the draft release target already matches" }
|
|
101
|
+
github.define_singleton_method(:edit_release_title) { |_tag, _title| flunk "edit_release_title should not be called when the title already matches" }
|
|
102
|
+
github.define_singleton_method(:edit_release_notes) { |_tag, _body| flunk "edit_release_notes should not be called when the notes already match" }
|
|
103
|
+
|
|
104
|
+
prepare = PumaRelease::Commands::Prepare.allocate
|
|
105
|
+
prepare.instance_variable_set(:@context, OpenStruct.new(history_file: Pathname("History.md")))
|
|
106
|
+
prepare.instance_variable_set(:@git_repo, git_repo)
|
|
107
|
+
prepare.instance_variable_set(:@repo_files, repo_files)
|
|
108
|
+
prepare.instance_variable_set(:@github, github)
|
|
109
|
+
|
|
110
|
+
prepare.send(:ensure_draft_release, "7.3.0", "release-v7.3.0")
|
|
111
|
+
|
|
112
|
+
assert_equal [[:create_release, "v7.3.0-proposal", "* Features\n * Example ([#1])", "v7.3.0 - Example", true, "release-v7.3.0"]], calls
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_show_version_recommendation_prints_reasoning_and_breaking_changes
|
|
116
|
+
ui = FakeUI.new
|
|
117
|
+
context = OpenStruct.new(ui:)
|
|
118
|
+
prepare = PumaRelease::Commands::Prepare.allocate
|
|
119
|
+
prepare.instance_variable_set(:@context, context)
|
|
120
|
+
|
|
121
|
+
output = capture_io do
|
|
122
|
+
prepare.send(
|
|
123
|
+
:show_version_recommendation,
|
|
124
|
+
{
|
|
125
|
+
"reasoning_markdown" => "Major because of [this commit](https://github.com/puma/puma/commit/abc123).",
|
|
126
|
+
"breaking_changes" => ["Dropped support for an older Rack integration"]
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
end.first
|
|
130
|
+
|
|
131
|
+
assert_equal ["Version bump recommendation:"], ui.infos
|
|
132
|
+
assert_equal ["Potential breaking changes:"], ui.warnings
|
|
133
|
+
assert_includes output, "Major because of [this commit](https://github.com/puma/puma/commit/abc123)."
|
|
134
|
+
assert_includes output, "- Dropped support for an older Rack integration"
|
|
135
|
+
end
|
|
136
|
+
end
|