gem_kit-release 0.3.1 → 0.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbdaca4c93bb73c92daa2e585aa8f6dbcef18a7717a6f9c3549f943272b13947
4
- data.tar.gz: 8d41aaa5cd762b8c6660d6cffbb669d1d17ce5a604ea578d1673681eb6c39809
3
+ metadata.gz: '028c5393e0908e49e47886256eb800f21382125f148bcd378bc6d2214fac27c2'
4
+ data.tar.gz: 85cd0ed8b03d81d4efcf0c63579b0046e273eb52eb36f9af36ab09fa8c972de8
5
5
  SHA512:
6
- metadata.gz: e431cf45975cc92e084d02988ed88a68053bf538f386bf734a167a9e0a6914de609a8d22555ba4e238c41ff6ae932bbfa44e5933fd665f19b0c4468336d60e8b
7
- data.tar.gz: 852b384995fbbd1ee34b28200cdcd472eddb6c3460e99101b70df56da4435b45be91fc6805959f21eb942bfc7287f883000e70e1566f684e518102727a29f1d1
6
+ metadata.gz: d5d1107714f5da3a524c06a40eac52b782c71d92a0ca90972c43c5e46c9bfd53bc6bd3cc6490bd3af43ed394eec825787ab30031b05bec192dcaa967b679bdd1
7
+ data.tar.gz: a890866fbb74226735ed5852d2b8fab0accf513cbe529b5ff505fbde0e0f7657f5d30e500088cbd602378fbf266c6f2277dbac8f7cb80c1cf4ce669fc39dba88
@@ -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)
@@ -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).
@@ -129,7 +129,10 @@ module GemKit
129
129
  # lib/gem_kit/release/version.rb for "gem_kit-release", lib/brute/version.rb
130
130
  # for "brute" — the convention every `bundle gem` project follows.
131
131
  def version_file
132
- File.expand_path(config.version_file || File.join("lib", *name.split("-"), "version.rb"), root)
132
+ File.expand_path(
133
+ config.version_file || File.join("lib", *library_segments, "version.rb"),
134
+ root,
135
+ )
133
136
  end
134
137
 
135
138
  # An ERB template beside the version file, if the project generates it.
@@ -141,7 +144,35 @@ module GemKit
141
144
  # What to require so that deprecation declarations register themselves.
142
145
  # "gem_kit-release" -> "gem_kit/release".
143
146
  def require_path
144
- config.require_path || name.tr("-", "/")
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
+ #
160
+ # A third belongs to bindings. `oo7-rb` and `sqlite3-ruby` are a library
161
+ # name wearing a suffix that says which language binds it, and the library
162
+ # underneath drops it: lib/oo7.rb holding `module Oo7`, with
163
+ # lib/oo7/version.rb beside it. Often the suffix is only there because
164
+ # RubyGems would not give up the bare name. The nested form names
165
+ # lib/oo7/rb.rb and the flat one lib/oo7_rb.rb, neither of which was ever
166
+ # there, so without this a binding gem cannot be bumped at all.
167
+ #
168
+ # All three are conventional and the name alone cannot tell them apart, so
169
+ # ask the tree: whichever exists is what this project is.
170
+ #
171
+ # When none exists the nested form wins, because that is what
172
+ # `bundle gem` would have produced and a project with no library yet is
173
+ # about to become one.
174
+ def library_segments
175
+ layouts.find { |segments| exists_under_lib?(segments) } || nested_layout
145
176
  end
146
177
 
147
178
  # Load the library, so Deprecate's registry reflects this project.
@@ -158,6 +189,40 @@ module GemKit
158
189
  def test_command = config.test_command || "bin/test"
159
190
 
160
191
  def changelog_writer = config.changelog_writer || "claude"
192
+
193
+ private
194
+
195
+ # The suffixes a binding gem wears, which its library drops.
196
+ BINDING_SUFFIXES = %w[rb ruby].freeze
197
+
198
+ # What a hyphenated name could mean, most conventional first. A name with
199
+ # no hyphen in it has exactly one meaning and the list says so.
200
+ def layouts
201
+ [nested_layout, flat_layout, *binding_layout].uniq
202
+ end
203
+
204
+ def nested_layout = name.split("-")
205
+
206
+ def flat_layout = [name.tr("-", "_")]
207
+
208
+ # [["oo7"]] for "oo7-rb"; nothing for a name wearing no binding suffix,
209
+ # and nothing for one that is only the suffix.
210
+ def binding_layout
211
+ nested_layout.then do |segments|
212
+ if segments.length > 1 && BINDING_SUFFIXES.include?(segments.last)
213
+ [segments[0..-2]]
214
+ else
215
+ []
216
+ end
217
+ end
218
+ end
219
+
220
+ # A layout is present if the entry file is there, or the directory it
221
+ # would hold — a gem is free to ship lib/foo/ without lib/foo.rb.
222
+ def exists_under_lib?(segments)
223
+ base = File.join(root, "lib", *segments)
224
+ File.exist?("#{base}.rb") || File.directory?(base)
225
+ end
161
226
  end
162
227
  end
163
228
  end
@@ -167,16 +232,29 @@ __END__
167
232
  describe "gem_kit/release/project" do
168
233
  require "tmpdir"
169
234
 
170
- # A throwaway gem laid out the conventional way.
171
- with_project = lambda do |name: "demo", version: "1.2.3", &block|
235
+ # A throwaway gem. `layout:` picks between the three conventions a hyphenated
236
+ # name can follow: :nested is `bundle gem`'s lib/gem_kit/release, :flat is a
237
+ # single-module gem's lib/ag_ui, and :binding is a bindings gem's lib/oo7 for
238
+ # a gem called oo7-rb.
239
+ with_project = lambda do |name: "demo", version: "1.2.3", layout: :nested, &block|
172
240
  Dir.mktmpdir do |dir|
173
- path = name.split("-")
241
+ case layout
242
+ when :flat then path = [name.tr("-", "_")]
243
+ when :binding then path = name.split("-")[0..-2]
244
+ else path = name.split("-")
245
+ end
174
246
  FileUtils.mkdir_p(File.join(dir, "lib", *path))
175
- File.write(File.join(dir, "lib", *path, "version.rb"), <<~RUBY)
176
- module #{path.map { |p| p.split("_").map(&:capitalize).join }.join("::")}
177
- VERSION = "#{version}"
178
- end
179
- RUBY
247
+
248
+ # Nested blocks rather than `module A::B`, which is only valid once A
249
+ # exists -- and a throwaway gem's outermost constant never does. The one
250
+ # hyphenated name that got away with it was gem_kit-release, because this
251
+ # process has GemKit defined already.
252
+ constants = path.map { |segment| segment.split("_").map(&:capitalize).join }
253
+ lines = constants.each_with_index.map { |constant, depth| "#{" " * depth}module #{constant}" }
254
+ lines << "#{" " * constants.length}VERSION = #{version.inspect}"
255
+ lines.concat(Array.new(constants.length) { |depth| "#{" " * (constants.length - 1 - depth)}end" })
256
+
257
+ File.write(File.join(dir, "lib", *path, "version.rb"), "#{lines.join("\n")}\n")
180
258
  File.write(File.join(dir, "#{name}.gemspec"), <<~RUBY)
181
259
  require_relative "lib/#{path.join("/")}/version"
182
260
  Gem::Specification.new do |spec|
@@ -218,6 +296,86 @@ describe "gem_kit/release/project" do
218
296
  end
219
297
  end
220
298
 
299
+ # A hyphenated name whose library is one module: lib/ag_ui.rb, not
300
+ # lib/ag/ui.rb. Deriving the path from the name alone named a file that was
301
+ # never there, so `gem kit bump` could not find the version and deprecations
302
+ # went undetected.
303
+ it "reads an underscored single-module layout off the tree" do
304
+ with_project.call(name: "ag-ui", layout: :flat) do |dir|
305
+ project = GemKit::Release::Project.detect(dir)
306
+ project.require_path.should == "ag_ui"
307
+ project.version_file.should == File.join(dir, "lib/ag_ui/version.rb")
308
+ end
309
+ end
310
+
311
+ # A bindings gem drops the suffix: oo7-rb is lib/oo7.rb and lib/oo7/version.rb,
312
+ # which neither of the other two layouts names. Found the hard way -- the gem
313
+ # was called oo7-rb only because RubyGems holds `oo7` for something else, and
314
+ # `gem kit bump` then wrote to lib/oo7/rb/version.rb, a path nothing had.
315
+ it "reads a bindings layout, whose library drops the suffix, off the tree" do
316
+ with_project.call(name: "oo7-rb", layout: :binding) do |dir|
317
+ project = GemKit::Release::Project.detect(dir)
318
+ project.require_path.should == "oo7"
319
+ project.version_file.should == File.join(dir, "lib/oo7/version.rb")
320
+ end
321
+ end
322
+
323
+ it "reads a -ruby suffix the same way" do
324
+ with_project.call(name: "sqlite3-ruby", layout: :binding) do |dir|
325
+ project = GemKit::Release::Project.detect(dir)
326
+ project.version_file.should == File.join(dir, "lib/sqlite3/version.rb")
327
+ end
328
+ end
329
+
330
+ # The suffix is only dropped when the tree says so. A gem that genuinely nests
331
+ # under a segment called "rb" keeps it.
332
+ it "keeps a nested layout that really does end in rb" do
333
+ with_project.call(name: "thing-rb", layout: :nested) do |dir|
334
+ project = GemKit::Release::Project.detect(dir)
335
+ project.require_path.should == "thing/rb"
336
+ project.version_file.should == File.join(dir, "lib/thing/rb/version.rb")
337
+ end
338
+ end
339
+
340
+ # "release" is not a binding suffix, so a name ending in something else is
341
+ # untouched by any of this.
342
+ it "leaves a hyphenated name that wears no binding suffix alone" do
343
+ with_project.call(name: "gem_kit-release", layout: :nested) do |dir|
344
+ project = GemKit::Release::Project.detect(dir)
345
+ project.require_path.should == "gem_kit/release"
346
+ end
347
+ end
348
+
349
+ # Nesting stays the default: it is what `bundle gem` produces, so it is what
350
+ # a project with no library yet is about to become.
351
+ it "falls back to the nested layout when neither is on disk" do
352
+ Dir.mktmpdir do |dir|
353
+ File.write(File.join(dir, "ag-ui.gemspec"), <<~RUBY)
354
+ Gem::Specification.new do |spec|
355
+ spec.name = "ag-ui"
356
+ spec.version = "0.1.0"
357
+ spec.authors = ["x"]
358
+ spec.summary = "x"
359
+ spec.files = []
360
+ end
361
+ RUBY
362
+ GemKit::Release::Project.detect(dir).require_path.should == "ag/ui"
363
+ end
364
+ end
365
+
366
+ # An explicit setting still wins over anything read off the tree.
367
+ it "prefers a configured require path and version file" do
368
+ with_project.call(name: "ag-ui", layout: :flat) do |dir|
369
+ config = GemKit::Release::Project::Config.new(
370
+ require_path: "somewhere/else",
371
+ version_file: "lib/custom.rb",
372
+ )
373
+ project = GemKit::Release::Project.detect(dir, config: config)
374
+ project.require_path.should == "somewhere/else"
375
+ project.version_file.should == File.join(dir, "lib/custom.rb")
376
+ end
377
+ end
378
+
221
379
  it "defaults the changelog to CHANGELOG.md in the project root" do
222
380
  with_project.call do |dir|
223
381
  project = GemKit::Release::Project.detect(dir)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GemKit
4
4
  module Release
5
- VERSION = "0.3.1"
5
+ VERSION = "0.3.3"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gem_kit-release
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Kidd