gem_kit-release 0.3.0 → 0.3.2
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 +4 -4
- data/lib/gem_kit/release/cli.rb +53 -0
- data/lib/gem_kit/release/commands/bump.rb +69 -0
- data/lib/gem_kit/release/gate.rb +3 -0
- data/lib/gem_kit/release/project.rb +141 -5
- data/lib/gem_kit/release/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9d6b16c56ad134e56b75cb00642a0a5f01ba96c6d7b620854736b871444b5aa
|
|
4
|
+
data.tar.gz: c3b718404bb08947a3df23e822a3edb5dc9ae7470da3605272b9d3e6f87b1dda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff045009cdcf213639c10607dfc0bffbc3e03f49e00ae2c34d2e2e50f26b13ffdcd2910d2a00f0b1311ea73ddfcf7318fc75196ff3b5a0960be9be63d3484bf9
|
|
7
|
+
data.tar.gz: 4a16c794925df5faaa54e1a0fe3b2429ba3ceddc578e3da8b45fadbbdead1b947eb29c308d50ae27db1a5e0ef9fb7a7e281dbde3762ef8d923df649bf92636a3
|
data/lib/gem_kit/release/cli.rb
CHANGED
|
@@ -38,6 +38,23 @@ module GemKit
|
|
|
38
38
|
# Each command here is a thin front for a plain object under CLI::; the
|
|
39
39
|
# work itself is in Gate, VersionFile, Changelog and Deprecate.
|
|
40
40
|
class CLI < Thor
|
|
41
|
+
# The order the commands go in — printed above the command listing so
|
|
42
|
+
# nobody has to guess when to commit or when to write the changelog. The
|
|
43
|
+
# question the process is easy to get wrong on is exactly the one this
|
|
44
|
+
# answers: the changelog is written *after* the bump, then the two are
|
|
45
|
+
# committed together, and only then does release run.
|
|
46
|
+
WORKFLOW = <<~TXT
|
|
47
|
+
Release, in order:
|
|
48
|
+
|
|
49
|
+
1. bin/test # green suite
|
|
50
|
+
2. gem kit bump [SEGMENT] # move the version, relock the Gemfile
|
|
51
|
+
3. gem kit changelog --write # AI CLI drafts this version's entry
|
|
52
|
+
4. gem kit changelog [VERSION] # lint the entry, confirm ready to release
|
|
53
|
+
5. git commit -am "Release ..." # bump + lockfile + entry, one commit
|
|
54
|
+
6. gem kit release # gate, build, push, tag (also pushes the tag)
|
|
55
|
+
7. git push # publish the release commit
|
|
56
|
+
TXT
|
|
57
|
+
|
|
41
58
|
class_option :gem, type: :string,
|
|
42
59
|
desc: "Which gem, in a repository holding more than one gemspec"
|
|
43
60
|
|
|
@@ -49,6 +66,15 @@ module GemKit
|
|
|
49
66
|
"gem kit #{command.usage}"
|
|
50
67
|
end
|
|
51
68
|
|
|
69
|
+
# Prepend the workflow to the top-level `gem kit` help. `gem kit help
|
|
70
|
+
# <command>` sets subcommand=true and lands here too; that page already
|
|
71
|
+
# has its own long_desc, so leave it alone.
|
|
72
|
+
def self.help(shell, subcommand = false)
|
|
73
|
+
shell.say(WORKFLOW) unless subcommand
|
|
74
|
+
shell.say unless subcommand
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
|
|
52
78
|
# Short forms for the three typed most often.
|
|
53
79
|
map "log" => :changelog,
|
|
54
80
|
"deps" => :deprecations,
|
|
@@ -210,6 +236,33 @@ describe "gem_kit/release/cli" do
|
|
|
210
236
|
end
|
|
211
237
|
end
|
|
212
238
|
|
|
239
|
+
# The order between `bump` and `release` — when to write the changelog and
|
|
240
|
+
# when to commit — is exactly the question the numbered list answers, so
|
|
241
|
+
# the top-level help has to print it.
|
|
242
|
+
it "prints the numbered workflow above the command listing" do
|
|
243
|
+
with_gem do |dir|
|
|
244
|
+
_status, out, _err = invoke([], dir)
|
|
245
|
+
|
|
246
|
+
out.should.match(/Release, in order:/)
|
|
247
|
+
out.should.match(/1\. bin\/test/)
|
|
248
|
+
out.should.match(/2\. gem kit bump/)
|
|
249
|
+
out.should.match(/5\. git commit/)
|
|
250
|
+
out.should.match(/6\. gem kit release/)
|
|
251
|
+
out.should.match(/7\. git push/)
|
|
252
|
+
# The workflow lands above "Commands:", not swallowed by it.
|
|
253
|
+
out.index("Release, in order:").should < out.index("Commands:")
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# `gem kit help <command>` already has its own long_desc; the workflow
|
|
258
|
+
# belongs on the top-level page only.
|
|
259
|
+
it "does not repeat the workflow on a per-command help page" do
|
|
260
|
+
with_gem do |dir|
|
|
261
|
+
_status, out, _err = invoke(["help", "bump"], dir)
|
|
262
|
+
out.should.not.match(/Release, in order:/)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
213
266
|
it "names the commands `gem kit <command>`, not the running program" do
|
|
214
267
|
with_gem do |dir|
|
|
215
268
|
_status, out, _err = invoke([], dir)
|
|
@@ -35,6 +35,8 @@ module GemKit
|
|
|
35
35
|
version_file.write(target)
|
|
36
36
|
say("#{current} -> #{target}")
|
|
37
37
|
|
|
38
|
+
relock(target)
|
|
39
|
+
|
|
38
40
|
upcoming = gate.upcoming_deprecations(target)
|
|
39
41
|
unless upcoming.empty?
|
|
40
42
|
say("#{upcoming.size} deprecation(s) still outstanding (none due in #{target}).")
|
|
@@ -45,6 +47,35 @@ module GemKit
|
|
|
45
47
|
say
|
|
46
48
|
say("#{RED}now run: gem kit changelog --write#{RESET}")
|
|
47
49
|
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# A Gemfile that says `gemspec` or `path: "."` records this gem's own
|
|
54
|
+
# version in Gemfile.lock, so a bump that only rewrites the version
|
|
55
|
+
# file leaves the two disagreeing — and bundler refuses outright
|
|
56
|
+
# under frozen mode, which is what bundlerEnv sets. Relock, so the
|
|
57
|
+
# bump is one coherent change rather than a trap for the next
|
|
58
|
+
# command anyone runs.
|
|
59
|
+
#
|
|
60
|
+
# `bundle lock` rather than `bundle install`: nothing needs
|
|
61
|
+
# installing, only the lockfile needs to agree.
|
|
62
|
+
def relock(target)
|
|
63
|
+
return unless project.self_locked?
|
|
64
|
+
|
|
65
|
+
say("Relocking #{File.basename(project.lockfile_path)}…")
|
|
66
|
+
|
|
67
|
+
ok = Dir.chdir(project.root) do
|
|
68
|
+
# BUNDLE_FROZEN is exactly what stops this, and inheriting it
|
|
69
|
+
# from an ambient devshell would defeat the point.
|
|
70
|
+
system({"BUNDLE_FROZEN" => "false", "BUNDLE_GEMFILE" => nil},
|
|
71
|
+
"bundle", "lock", out: File::NULL, err: File::NULL)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return if ok
|
|
75
|
+
|
|
76
|
+
say("#{RED}could not relock — run `bundle lock` before committing " \
|
|
77
|
+
"#{target}#{RESET}")
|
|
78
|
+
end
|
|
48
79
|
end
|
|
49
80
|
end
|
|
50
81
|
end
|
|
@@ -95,6 +126,44 @@ describe "gem_kit/release/commands/bump" do
|
|
|
95
126
|
end
|
|
96
127
|
end
|
|
97
128
|
|
|
129
|
+
# The version lives in two files when a gem is in its own bundle, and a bump
|
|
130
|
+
# that moves one of them leaves bundler refusing to do anything.
|
|
131
|
+
it "relocks a lockfile that records this gem's own version" do
|
|
132
|
+
with_gem do |dir|
|
|
133
|
+
File.write(File.join(dir, "Gemfile"), %(source "https://rubygems.org"\ngemspec\n))
|
|
134
|
+
File.write(File.join(dir, "Gemfile.lock"), <<~LOCK)
|
|
135
|
+
PATH
|
|
136
|
+
remote: .
|
|
137
|
+
specs:
|
|
138
|
+
demo (1.2.3)
|
|
139
|
+
|
|
140
|
+
GEM
|
|
141
|
+
remote: https://rubygems.org/
|
|
142
|
+
|
|
143
|
+
PLATFORMS
|
|
144
|
+
ruby
|
|
145
|
+
|
|
146
|
+
DEPENDENCIES
|
|
147
|
+
demo!
|
|
148
|
+
|
|
149
|
+
BUNDLED WITH
|
|
150
|
+
2.7.2
|
|
151
|
+
LOCK
|
|
152
|
+
|
|
153
|
+
_status, out, _err = invoke(["bump", "minor"], dir)
|
|
154
|
+
|
|
155
|
+
out.should.match(/Relocking Gemfile\.lock/)
|
|
156
|
+
File.read(File.join(dir, "Gemfile.lock")).should.match(/demo \(1\.3\.0\)/)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "says nothing about a lockfile that does not record this gem" do
|
|
161
|
+
with_gem do |dir|
|
|
162
|
+
_status, out, _err = invoke(["bump", "minor"], dir)
|
|
163
|
+
out.should.not.match(/Relocking/)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
98
167
|
it "mentions deprecations outstanding but not yet due" do
|
|
99
168
|
with_gem do |dir|
|
|
100
169
|
_status, out, _err = invoke(["bump", "minor"], dir, deprecations: [["Old", "New", "9.0"]])
|
data/lib/gem_kit/release/gate.rb
CHANGED
|
@@ -98,6 +98,9 @@ __END__
|
|
|
98
98
|
|
|
99
99
|
describe "gem_kit/release/gate" do
|
|
100
100
|
require "tmpdir"
|
|
101
|
+
# These specs ask the gate whether a temp directory is a repository, so the
|
|
102
|
+
# walk up out of Dir.tmpdir has to be stopped or the answer is the machine's.
|
|
103
|
+
require_relative "../../../spec/support/git_isolation"
|
|
101
104
|
|
|
102
105
|
# A Project stub: the Gate only asks it for a changelog path and a version,
|
|
103
106
|
# and to load the library (a no-op here — the specs drive the registry).
|
|
@@ -105,6 +105,21 @@ module GemKit
|
|
|
105
105
|
multi_gem? ? "CHANGELOG-#{name}.md" : "CHANGELOG.md"
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
def lockfile_path
|
|
109
|
+
File.join(root, "Gemfile.lock")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A Gemfile that says `gemspec` or `path: "."` puts this gem in its own
|
|
113
|
+
# bundle, and the lockfile then records this gem's version. Bumping
|
|
114
|
+
# without relocking leaves the two disagreeing — which bundler reports as
|
|
115
|
+
# "the gemspecs for path gems changed", and refuses outright under
|
|
116
|
+
# frozen mode, as bundlerEnv sets.
|
|
117
|
+
def self_locked?
|
|
118
|
+
return false unless File.exist?(lockfile_path)
|
|
119
|
+
|
|
120
|
+
File.read(lockfile_path).match?(/^\s{4}#{Regexp.escape(name)} \(\d/)
|
|
121
|
+
end
|
|
122
|
+
|
|
108
123
|
# `v1.2.3` says which version but not which gem, which is fine until a
|
|
109
124
|
# repository holds two of them at the same version.
|
|
110
125
|
def tag_prefix
|
|
@@ -114,7 +129,10 @@ module GemKit
|
|
|
114
129
|
# lib/gem_kit/release/version.rb for "gem_kit-release", lib/brute/version.rb
|
|
115
130
|
# for "brute" — the convention every `bundle gem` project follows.
|
|
116
131
|
def version_file
|
|
117
|
-
File.expand_path(
|
|
132
|
+
File.expand_path(
|
|
133
|
+
config.version_file || File.join("lib", *library_segments, "version.rb"),
|
|
134
|
+
root,
|
|
135
|
+
)
|
|
118
136
|
end
|
|
119
137
|
|
|
120
138
|
# An ERB template beside the version file, if the project generates it.
|
|
@@ -126,7 +144,36 @@ module GemKit
|
|
|
126
144
|
# What to require so that deprecation declarations register themselves.
|
|
127
145
|
# "gem_kit-release" -> "gem_kit/release".
|
|
128
146
|
def require_path
|
|
129
|
-
config.require_path ||
|
|
147
|
+
config.require_path || library_segments.join("/")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Where the library sits under lib/, as path segments.
|
|
151
|
+
#
|
|
152
|
+
# `bundle gem gem_kit-release` NESTS, treating the hyphen as a directory
|
|
153
|
+
# separator: lib/gem_kit/release.rb, module GemKit::Release. That is the
|
|
154
|
+
# common case and the one assumed here first.
|
|
155
|
+
#
|
|
156
|
+
# But a hyphenated name whose library is a SINGLE module does not nest.
|
|
157
|
+
# `ag-ui` is lib/ag_ui.rb holding `module AgUi` — hyphen as underscore —
|
|
158
|
+
# and the nested form names lib/ag/ui.rb, a file that was never there.
|
|
159
|
+
# Both layouts are conventional and the name alone cannot tell them
|
|
160
|
+
# apart, so ask the tree: whichever exists is what this project is.
|
|
161
|
+
#
|
|
162
|
+
# When neither exists the nested form wins, because that is what
|
|
163
|
+
# `bundle gem` would have produced and a project with no library yet is
|
|
164
|
+
# about to become one.
|
|
165
|
+
def library_segments
|
|
166
|
+
nested = name.split("-")
|
|
167
|
+
if exists_under_lib?(nested)
|
|
168
|
+
nested
|
|
169
|
+
else
|
|
170
|
+
flat = [name.tr("-", "_")]
|
|
171
|
+
if exists_under_lib?(flat)
|
|
172
|
+
flat
|
|
173
|
+
else
|
|
174
|
+
nested
|
|
175
|
+
end
|
|
176
|
+
end
|
|
130
177
|
end
|
|
131
178
|
|
|
132
179
|
# Load the library, so Deprecate's registry reflects this project.
|
|
@@ -143,6 +190,15 @@ module GemKit
|
|
|
143
190
|
def test_command = config.test_command || "bin/test"
|
|
144
191
|
|
|
145
192
|
def changelog_writer = config.changelog_writer || "claude"
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
# A layout is present if the entry file is there, or the directory it
|
|
197
|
+
# would hold — a gem is free to ship lib/foo/ without lib/foo.rb.
|
|
198
|
+
def exists_under_lib?(segments)
|
|
199
|
+
base = File.join(root, "lib", *segments)
|
|
200
|
+
File.exist?("#{base}.rb") || File.directory?(base)
|
|
201
|
+
end
|
|
146
202
|
end
|
|
147
203
|
end
|
|
148
204
|
end
|
|
@@ -152,10 +208,16 @@ __END__
|
|
|
152
208
|
describe "gem_kit/release/project" do
|
|
153
209
|
require "tmpdir"
|
|
154
210
|
|
|
155
|
-
# A throwaway gem
|
|
156
|
-
|
|
211
|
+
# A throwaway gem. `layout:` picks between the two conventions a hyphenated
|
|
212
|
+
# name can follow: :nested is `bundle gem`'s lib/gem_kit/release, :flat is a
|
|
213
|
+
# single-module gem's lib/ag_ui.
|
|
214
|
+
with_project = lambda do |name: "demo", version: "1.2.3", layout: :nested, &block|
|
|
157
215
|
Dir.mktmpdir do |dir|
|
|
158
|
-
|
|
216
|
+
if layout == :flat
|
|
217
|
+
path = [name.tr("-", "_")]
|
|
218
|
+
else
|
|
219
|
+
path = name.split("-")
|
|
220
|
+
end
|
|
159
221
|
FileUtils.mkdir_p(File.join(dir, "lib", *path))
|
|
160
222
|
File.write(File.join(dir, "lib", *path, "version.rb"), <<~RUBY)
|
|
161
223
|
module #{path.map { |p| p.split("_").map(&:capitalize).join }.join("::")}
|
|
@@ -203,6 +265,48 @@ describe "gem_kit/release/project" do
|
|
|
203
265
|
end
|
|
204
266
|
end
|
|
205
267
|
|
|
268
|
+
# A hyphenated name whose library is one module: lib/ag_ui.rb, not
|
|
269
|
+
# lib/ag/ui.rb. Deriving the path from the name alone named a file that was
|
|
270
|
+
# never there, so `gem kit bump` could not find the version and deprecations
|
|
271
|
+
# went undetected.
|
|
272
|
+
it "reads an underscored single-module layout off the tree" do
|
|
273
|
+
with_project.call(name: "ag-ui", layout: :flat) do |dir|
|
|
274
|
+
project = GemKit::Release::Project.detect(dir)
|
|
275
|
+
project.require_path.should == "ag_ui"
|
|
276
|
+
project.version_file.should == File.join(dir, "lib/ag_ui/version.rb")
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Nesting stays the default: it is what `bundle gem` produces, so it is what
|
|
281
|
+
# a project with no library yet is about to become.
|
|
282
|
+
it "falls back to the nested layout when neither is on disk" do
|
|
283
|
+
Dir.mktmpdir do |dir|
|
|
284
|
+
File.write(File.join(dir, "ag-ui.gemspec"), <<~RUBY)
|
|
285
|
+
Gem::Specification.new do |spec|
|
|
286
|
+
spec.name = "ag-ui"
|
|
287
|
+
spec.version = "0.1.0"
|
|
288
|
+
spec.authors = ["x"]
|
|
289
|
+
spec.summary = "x"
|
|
290
|
+
spec.files = []
|
|
291
|
+
end
|
|
292
|
+
RUBY
|
|
293
|
+
GemKit::Release::Project.detect(dir).require_path.should == "ag/ui"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# An explicit setting still wins over anything read off the tree.
|
|
298
|
+
it "prefers a configured require path and version file" do
|
|
299
|
+
with_project.call(name: "ag-ui", layout: :flat) do |dir|
|
|
300
|
+
config = GemKit::Release::Project::Config.new(
|
|
301
|
+
require_path: "somewhere/else",
|
|
302
|
+
version_file: "lib/custom.rb",
|
|
303
|
+
)
|
|
304
|
+
project = GemKit::Release::Project.detect(dir, config: config)
|
|
305
|
+
project.require_path.should == "somewhere/else"
|
|
306
|
+
project.version_file.should == File.join(dir, "lib/custom.rb")
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
206
310
|
it "defaults the changelog to CHANGELOG.md in the project root" do
|
|
207
311
|
with_project.call do |dir|
|
|
208
312
|
project = GemKit::Release::Project.detect(dir)
|
|
@@ -252,6 +356,38 @@ describe "gem_kit/release/project" do
|
|
|
252
356
|
end
|
|
253
357
|
end
|
|
254
358
|
|
|
359
|
+
# A gem whose own Gemfile pulls it in has its version written in the lockfile
|
|
360
|
+
# as well as the version file, and both have to move together.
|
|
361
|
+
it "knows whether the lockfile records this gem's own version" do
|
|
362
|
+
with_project.call do |dir|
|
|
363
|
+
GemKit::Release::Project.detect(dir).self_locked?.should.be.false
|
|
364
|
+
|
|
365
|
+
File.write(File.join(dir, "Gemfile.lock"), <<~LOCK)
|
|
366
|
+
PATH
|
|
367
|
+
remote: .
|
|
368
|
+
specs:
|
|
369
|
+
demo (1.2.3)
|
|
370
|
+
|
|
371
|
+
GEM
|
|
372
|
+
remote: https://rubygems.org/
|
|
373
|
+
LOCK
|
|
374
|
+
GemKit::Release::Project.detect(dir).self_locked?.should.be.true
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
it "does not mistake another gem's line in the lockfile for its own" do
|
|
379
|
+
with_project.call do |dir|
|
|
380
|
+
File.write(File.join(dir, "Gemfile.lock"), <<~LOCK)
|
|
381
|
+
GEM
|
|
382
|
+
remote: https://rubygems.org/
|
|
383
|
+
specs:
|
|
384
|
+
other-demo (1.0.0)
|
|
385
|
+
demo-helper (2.0.0)
|
|
386
|
+
LOCK
|
|
387
|
+
GemKit::Release::Project.detect(dir).self_locked?.should.be.false
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
255
391
|
it "finds an ERB version template when the project generates its version file" do
|
|
256
392
|
with_project.call do |dir|
|
|
257
393
|
GemKit::Release::Project.detect(dir).version_template.should.be.nil
|