gem_kit-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.
@@ -0,0 +1,173 @@
1
+ # Deprecations
2
+
3
+ A deprecation in <%= name %> is a **dated promise**: it names the replacement
4
+ *and* the version the old name stops existing in. That promise is
5
+ machine-readable — every declaration registers itself, and the release tooling
6
+ refuses to ship a version that breaks one.
7
+
8
+ The mechanism is [`GemKit::Release::Deprecate`](https://rubygems.org/gems/gem_kit-release),
9
+ built on [`Gem::Deprecate`](https://docs.ruby-lang.org/en/master/Gem/Deprecate.html).
10
+
11
+ ## The rules
12
+
13
+ 1. **Never delete a public name outright.** Leave it working, deprecated, until
14
+ its deadline.
15
+ 2. **Every deprecation names a removal version.** The current version is
16
+ <%= version %>, so the usual deadline is <%= next_major %>.
17
+ 3. **Removals happen in major versions only.** A minor or patch release never
18
+ takes a name away.
19
+ 4. **The deadline is enforced, not remembered.** `gem kit bump` and `gem kit release`
20
+ both refuse to move to a version that has a deprecation coming due.
21
+ 5. **Deprecating something is a changelog entry** — under `### Deprecated`,
22
+ naming the replacement and the removal version.
23
+
24
+ ## Deprecating a method
25
+
26
+ ```ruby
27
+ class Session
28
+ extend GemKit::Release::Deprecate
29
+
30
+ def new_reset
31
+ # ...
32
+ end
33
+
34
+ def old_reset = new_reset
35
+
36
+ deprecate :old_reset, "Session#new_reset", "<%= next_major %>"
37
+ end
38
+ ```
39
+
40
+ The old method keeps working and warns on every call, naming the caller:
41
+
42
+ ```
43
+ NOTE: Session#old_reset is deprecated; use Session#new_reset instead.
44
+ It will be removed in <%= next_major %>
45
+ Session#old_reset called from app.rb:12.
46
+ ```
47
+
48
+ Use `:none` as the replacement when there genuinely isn't one:
49
+
50
+ ```ruby
51
+ deprecate :old_reset, :none, "<%= next_major %>"
52
+ ```
53
+
54
+ For a class method, follow the `Gem::Deprecate` idiom — the registry records it
55
+ against the class, not its singleton:
56
+
57
+ ```ruby
58
+ class << self
59
+ extend GemKit::Release::Deprecate
60
+ deprecate :some_class_method, "Other.method", "<%= next_major %>"
61
+ end
62
+ ```
63
+
64
+ ## Deprecating a renamed or moved constant
65
+
66
+ Keep the old constant as a subclass of the new one and declare the rename in
67
+ its body:
68
+
69
+ ```ruby
70
+ module Old
71
+ class Thing < New::Thing
72
+ extend GemKit::Release::Deprecate
73
+ superseded_by "New::Thing", "<%= next_major %>"
74
+ end
75
+ end
76
+ ```
77
+
78
+ Old code keeps running unchanged; instantiating the old name warns and points at
79
+ the new one. The whole shim is those four lines — the implementation lives in
80
+ one place.
81
+
82
+ It is `superseded_by` rather than `deprecate_constant` because `Module` already
83
+ has a method by that name, and shadowing it would break callers who use it.
84
+
85
+ ## Finding what is outstanding
86
+
87
+ ```sh
88
+ gem kit deprecations
89
+ ```
90
+
91
+ ```
92
+ 1 outstanding deprecation(s) (current version <%= version %>):
93
+ <%= next_major %> Session#old_reset -> Session#new_reset
94
+ lib/session.rb:19
95
+ ```
96
+
97
+ Pass a version to ask "what comes due here?" — it exits non-zero if anything
98
+ does, which is what makes it usable as a gate in CI:
99
+
100
+ ```sh
101
+ gem kit deprecations <%= next_major %>.0.0
102
+ ```
103
+
104
+ Programmatically, the same data:
105
+
106
+ ```ruby
107
+ GemKit::Release::Deprecate.registry # every declaration
108
+ GemKit::Release::Deprecate.pending("<%= next_major %>.0.0") # deadlines that have arrived
109
+ GemKit::Release::Deprecate.upcoming("<%= next_major %>.0.0") # still in their grace period
110
+ ```
111
+
112
+ Each entry carries `name`, `replacement`, `removed_in` and `declared_at`.
113
+
114
+ ## Paying the debt
115
+
116
+ When a major version comes around, the bump is blocked until the deprecated
117
+ code is actually gone:
118
+
119
+ ```
120
+ $ gem kit bump major
121
+ ERROR: Refusing to bump <%= version %> -> <%= next_major %>.0.0:
122
+
123
+ <%= next_major %> Session#old_reset -> Session#new_reset
124
+ lib/session.rb:19
125
+
126
+ Remove them, then bump. Override with --force.
127
+ ```
128
+
129
+ So the order of work is:
130
+
131
+ 1. `gem kit deprecations <%= next_major %>.0.0` — read the list.
132
+ 2. Delete each deprecated name and its specs. For a constant shim, that means
133
+ deleting the whole file.
134
+ 3. Update anything in `examples/` and the docs still using the old name.
135
+ 4. Record the removals in `<%= changelog %>` under `### Removed`.
136
+ 5. `gem kit bump major` — now it goes through.
137
+
138
+ `--force` exists for the case where you have decided to extend a grace period,
139
+ and it prints what it is overriding. It is not the normal path: extending a
140
+ deadline properly means editing the declaration's version, which keeps the
141
+ registry honest.
142
+
143
+ ## Testing deprecated code
144
+
145
+ `Gem::Deprecate.skip_during` silences these warnings too, so a spec can
146
+ exercise the old path without noise:
147
+
148
+ ```ruby
149
+ Gem::Deprecate.skip_during do
150
+ legacy.old_reset
151
+ end
152
+ ```
153
+
154
+ To assert *that* something warns, stub the single funnel every warning goes
155
+ through:
156
+
157
+ ```ruby
158
+ captured = []
159
+ original = GemKit::Release::Deprecate.method(:warn)
160
+ GemKit::Release::Deprecate.define_singleton_method(:warn) { |message| captured << message }
161
+ begin
162
+ legacy.old_reset
163
+ ensure
164
+ GemKit::Release::Deprecate.define_singleton_method(:warn, original)
165
+ end
166
+ ```
167
+
168
+ ## See also
169
+
170
+ - [RELEASE.md](RELEASE.md) — where the deprecation gates sit in the release
171
+ process.
172
+ - [<%= changelog %>](<%= changelog %>) — the `### Deprecated` and `### Removed`
173
+ sections are the user-facing half of all this.
@@ -0,0 +1,159 @@
1
+ # Releasing <%= name %>
2
+
3
+ The whole process, in order:
4
+
5
+ ```sh
6
+ <%= test.ljust(30) %># 1. green suite
7
+ gem kit bump minor # 2. bump (prints: now run gem kit changelog --write)
8
+ gem kit changelog --write # 3. write the entry
9
+ gem kit changelog <VERSION> # 4. check it
10
+ git commit -am "Release ..." # 5. commit the bump + changelog
11
+ gem kit release # 6. build and push
12
+ gem kit tag --push # 7. tag it
13
+ ```
14
+
15
+ Steps 2 and 6 are gates: they refuse to proceed when something is missing.
16
+ Everything below is what they check and why.
17
+
18
+ ## Versioning
19
+
20
+ <%= name %> is [semver](https://semver.org/). The version lives in one place —
21
+ `<%= version_file %>` — and is only ever changed by `gem kit bump`.
22
+
23
+ | Segment | When | What it may contain |
24
+ | --- | --- | --- |
25
+ | **major** | A public name disappears or changes meaning | Removals of deprecated names, breaking signature changes |
26
+ | **minor** | New public surface, backwards compatible | New classes and methods; new deprecations |
27
+ | **patch** | Nothing new, nothing gone | Bug fixes, docs, internals |
28
+
29
+ Two rules follow from this, and both are enforced in code:
30
+
31
+ - **Removals only ever land in a major version.** A name promised to disappear
32
+ in <%= next_major %> disappears in <%= next_major %>.0.0, not in a patch.
33
+ - **Deprecating is a minor.** Adding a deprecation puts no obligation on the
34
+ user *yet*, so it does not need a major — but it starts the clock. See
35
+ [DEPRECATIONS.md](DEPRECATIONS.md).
36
+
37
+ ## 1. Green suite
38
+
39
+ ```sh
40
+ <%= test %>
41
+ ```
42
+
43
+ A red suite is not a release candidate; nothing downstream checks this for you.
44
+
45
+ ## 2. Bump
46
+
47
+ ```sh
48
+ gem kit bump <major|minor|patch>
49
+ ```
50
+
51
+ Rewrites `<%= version_file %>` — by rendering its `.erb` template if there is
52
+ one, otherwise by substituting the version literal in place — and prints the
53
+ transition:
54
+
55
+ ```
56
+ <%= version %> -> ...
57
+
58
+ now run: gem kit changelog --write
59
+ ```
60
+
61
+ **It refuses to bump onto a deprecation deadline.** If any registered
62
+ deprecation is due at the new version, it lists them with their source lines
63
+ and fails. Remove the deprecated code first — that is the point of the promise.
64
+ `--force` overrides and says what it overrode. Deprecations *not* yet due are
65
+ reported for information, not blocked on.
66
+
67
+ ## 3. Changelog
68
+
69
+ ```sh
70
+ gem kit changelog --write
71
+ ```
72
+
73
+ Hands the entry to the AI CLI named by `config.changelog_writer` (default:
74
+ `claude`), with a prompt describing the format and this project's conventions.
75
+ It reads the commits since the last tag and edits `<%= changelog %>` only.
76
+
77
+ Review what it writes. It is a first draft with the commits in front of it, not
78
+ an oracle — it cannot know which of two changes mattered to users.
79
+
80
+ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
81
+
82
+ ```md
83
+ ## [Unreleased]
84
+
85
+ ## [<%= version %>] - <%= today %>
86
+
87
+ ### Added
88
+
89
+ - Something users can now do.
90
+
91
+ ### Deprecated
92
+
93
+ - `Old::Name` — use `New::Name` instead. Removed in <%= next_major %>.
94
+ ```
95
+
96
+ Entries go under `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed` or
97
+ `Security` — those six and no others. `[Unreleased]` stays at the top, emptied
98
+ of anything that shipped.
99
+
100
+ ## 4. Lint the changelog
101
+
102
+ ```sh
103
+ gem kit changelog # format only
104
+ gem kit changelog <%= version %> # format, plus "is this version ready?"
105
+ ```
106
+
107
+ Checks the title, that every heading is `## [Unreleased]` or
108
+ `## [1.2.3] - YYYY-MM-DD`, that versions are valid, dated, unique and ordered
109
+ newest-first, that `###` sections are one of the six types and none are empty —
110
+ and, given a version, that it has a non-empty section at the top of the released
111
+ list. Every problem is reported as `<%= changelog %>:<line> <what>`.
112
+
113
+ ## 5. Commit
114
+
115
+ The version bump, the lockfile and the changelog belong in one commit, before
116
+ anything is pushed to RubyGems. A published gem whose changelog is still
117
+ unwritten in git is the failure this process exists to prevent.
118
+
119
+ ## 6. Release
120
+
121
+ ```sh
122
+ gem kit release # or: gem kit release --dry-run, which is the CI check
123
+ ```
124
+
125
+ Two gates, both before `gem build` runs:
126
+
127
+ 1. **Changelog** — this version needs its own non-empty, correctly formatted
128
+ section, sitting at the top of the released list.
129
+ 2. **Deprecations** — nothing promised to disappear in this version may still
130
+ be in the tree.
131
+
132
+ Then `gem build` and `gem push`. Requires RubyGems push credentials.
133
+
134
+ ## 7. Tag
135
+
136
+ ```sh
137
+ gem kit tag --push
138
+ ```
139
+
140
+ Creates `v<%= version %>`, refusing if it already exists. The tag is also what
141
+ the next `gem kit changelog --write` uses to find the commit range, so a missing
142
+ one makes the following release's changelog harder to write.
143
+
144
+ ## When something goes wrong
145
+
146
+ **Published a broken gem.** Don't delete it — `gem yank <%= name %> -v X.Y.Z` if
147
+ it is genuinely dangerous, otherwise ship a patch. Yanking a version other
148
+ people have already locked to breaks their builds.
149
+
150
+ **Bumped but the release failed.** The bump is just a file. Fix the cause and
151
+ re-run `gem kit release`; there is no need to un-bump.
152
+
153
+ **Changelog written for the wrong version.** Edit the heading and re-run
154
+ `gem kit changelog <version>`. Nothing downstream caches it.
155
+
156
+ ## See also
157
+
158
+ - [DEPRECATIONS.md](DEPRECATIONS.md) — the deprecation policy the gates enforce.
159
+ - [<%= changelog %>](<%= changelog %>).
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemKit
4
+ module Release
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module GemKit
6
+ module Release
7
+ # Owns the one file that states the version, and the semver arithmetic for
8
+ # moving it. Two layouts are supported, and which one is in play is decided
9
+ # by whether an ERB template sits beside the file:
10
+ #
11
+ # lib/x/version.rb.erb present -> render it with `version` in scope
12
+ # otherwise -> substitute the version string in place
13
+ #
14
+ # The second is the common case and deliberately surgical: it rewrites the
15
+ # quoted version literal and touches nothing else in the file.
16
+ class VersionFile
17
+ class Error < StandardError; end
18
+
19
+ SEGMENTS = %w[major minor patch].freeze
20
+
21
+ # A quoted semver literal, e.g. VERSION = "4.1.0"
22
+ LITERAL = /(["'])(\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.-]+)?)\1/
23
+
24
+ attr_reader :path, :template
25
+
26
+ def initialize(path, template: nil)
27
+ @path = path
28
+ @template = template
29
+ end
30
+
31
+ # "4.1.0", :minor -> "4.2.0"
32
+ def self.bump(version, segment)
33
+ unless SEGMENTS.include?(segment.to_s)
34
+ raise Error, "unknown segment #{segment.inspect} (expected #{SEGMENTS.join(", ")})"
35
+ end
36
+
37
+ major, minor, patch = version.to_s.split(".").map(&:to_i)
38
+ case segment.to_s
39
+ when "major" then "#{major + 1}.0.0"
40
+ when "minor" then "#{major}.#{minor + 1}.0"
41
+ when "patch" then "#{major}.#{minor}.#{patch + 1}"
42
+ end
43
+ end
44
+
45
+ # The version currently written in the file.
46
+ def read
47
+ contents = File.read(path)
48
+ match = contents.match(LITERAL)
49
+ raise Error, "no version literal in #{path}" if match.nil?
50
+
51
+ match[2]
52
+ end
53
+
54
+ # Write `version` into the file. Returns the version written.
55
+ def write(version)
56
+ if template
57
+ File.write(path, ERB.new(File.read(template)).result(binding))
58
+ else
59
+ contents = File.read(path)
60
+ raise Error, "no version literal in #{path}" unless contents.match?(LITERAL)
61
+
62
+ File.write(path, contents.sub(LITERAL) { "#{$1}#{version}#{$1}" })
63
+ end
64
+
65
+ version
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ __END__
72
+
73
+ describe "gem_kit/release/version_file" do
74
+ require "tmpdir"
75
+
76
+ VF = GemKit::Release::VersionFile unless defined?(VF)
77
+
78
+ with_file = lambda do |contents, &block|
79
+ Dir.mktmpdir do |dir|
80
+ path = File.join(dir, "version.rb")
81
+ File.write(path, contents)
82
+ block.call(path, dir)
83
+ end
84
+ end
85
+
86
+ it "bumps each segment, zeroing the ones below it" do
87
+ VF.bump("4.1.3", :major).should == "5.0.0"
88
+ VF.bump("4.1.3", :minor).should == "4.2.0"
89
+ VF.bump("4.1.3", :patch).should == "4.1.4"
90
+ end
91
+
92
+ it "rejects an unknown segment" do
93
+ lambda { VF.bump("1.0.0", :epoch) }.should.raise(GemKit::Release::VersionFile::Error)
94
+ end
95
+
96
+ it "reads the version literal out of the file" do
97
+ with_file.call(%(module Demo\n VERSION = "1.2.3"\nend\n)) do |path|
98
+ VF.new(path).read.should == "1.2.3"
99
+ end
100
+ end
101
+
102
+ it "rewrites only the version literal, leaving the rest of the file alone" do
103
+ original = %(# frozen_string_literal: true\n\nmodule Demo\n VERSION = "1.2.3" # keep\nend\n)
104
+ with_file.call(original) do |path|
105
+ VF.new(path).write("2.0.0").should == "2.0.0"
106
+ File.read(path).should == original.sub("1.2.3", "2.0.0")
107
+ end
108
+ end
109
+
110
+ it "handles single-quoted literals" do
111
+ with_file.call(%(VERSION = '0.9.0'\n)) do |path|
112
+ VF.new(path).write("0.10.0")
113
+ File.read(path).should == %(VERSION = '0.10.0'\n)
114
+ end
115
+ end
116
+
117
+ it "raises when there is no version literal to rewrite" do
118
+ with_file.call("module Demo\nend\n") do |path|
119
+ lambda { VF.new(path).read }.should.raise(GemKit::Release::VersionFile::Error)
120
+ lambda { VF.new(path).write("1.0.0") }.should.raise(GemKit::Release::VersionFile::Error)
121
+ end
122
+ end
123
+
124
+ it "renders an ERB template when the project generates its version file" do
125
+ with_file.call(%(VERSION = "0.0.0"\n)) do |path, dir|
126
+ template = File.join(dir, "version.rb.erb")
127
+ File.write(template, %(module Demo\n VERSION = "<%= version %>"\nend\n))
128
+
129
+ VF.new(path, template: template).write("3.1.4")
130
+ File.read(path).should == %(module Demo\n VERSION = "3.1.4"\nend\n)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "release/version"
4
+ require_relative "release/deprecate"
5
+ require_relative "release/changelog"
6
+ require_relative "release/project"
7
+ require_relative "release/version_file"
8
+ require_relative "release/gate"
9
+
10
+ module GemKit
11
+ # Versioning, changelog and deprecation gates for Ruby gems.
12
+ #
13
+ # The premise: a deprecation is a dated promise — it names its replacement
14
+ # and the version the old name stops existing in — and a release is only
15
+ # honest if it keeps every promise that has come due and documents what
16
+ # changed. Both are checkable, so neither should depend on anyone
17
+ # remembering.
18
+ #
19
+ # In a Rakefile, which is the whole integration:
20
+ #
21
+ # require "gem_kit/release/tasks"
22
+ #
23
+ # Everything else is inferred from the project's .gemspec. Override only
24
+ # what cannot be:
25
+ #
26
+ # GemKit::Release.configure do |config|
27
+ # config.changelog = "HISTORY.md"
28
+ # config.test_command = "bin/test"
29
+ # config.changelog_writer = "claude"
30
+ # end
31
+ module Release
32
+ def self.config
33
+ @config ||= Project::Config.new
34
+ end
35
+
36
+ def self.configure
37
+ yield config
38
+ config
39
+ end
40
+
41
+ # The project the tooling is running in.
42
+ def self.project(dir = Dir.pwd)
43
+ Project.detect(dir)
44
+ end
45
+
46
+ def self.gate(dir = Dir.pwd)
47
+ Gate.new(project(dir))
48
+ end
49
+
50
+ # Reset configuration — for tests.
51
+ def self.reset!
52
+ @config = nil
53
+ end
54
+ end
55
+ end
56
+
57
+ __END__
58
+
59
+ describe "gem_kit/release" do
60
+ it "exposes a configurable, resettable config" do
61
+ begin
62
+ GemKit::Release.configure { |c| c.changelog = "HISTORY.md" }
63
+ GemKit::Release.config.changelog.should == "HISTORY.md"
64
+ ensure
65
+ GemKit::Release.reset!
66
+ end
67
+
68
+ GemKit::Release.config.changelog.should.be.nil
69
+ end
70
+
71
+ it "detects itself as a project" do
72
+ project = GemKit::Release.project(File.expand_path("../..", __dir__))
73
+ project.name.should == "gem_kit-release"
74
+ project.version.should == Gem::Version.new(GemKit::Release::VERSION)
75
+ project.require_path.should == "gem_kit/release"
76
+ end
77
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/command"
4
+
5
+ module Gem
6
+ module Commands
7
+ # `gem kit <subcommand>` — the only command this gem registers.
8
+ #
9
+ # One registration rather than six keeps the toolchain under a name that is
10
+ # obviously ours, leaves `gem help commands` readable, and means nothing
11
+ # here can collide with a command RubyGems adds later.
12
+ #
13
+ # This class is a bridge and nothing else: it hands the argv to the dry-cli
14
+ # registry, which owns the parsing and the help pages, and turns the result
15
+ # into a RubyGems exit. handle_options is deliberately inert — parsing here
16
+ # would swallow flags meant for a subcommand.
17
+ class KitCommand < Gem::Command
18
+ def initialize
19
+ super("kit", "Versioning, changelog and deprecation gates for this gem")
20
+ end
21
+
22
+ def arguments
23
+ <<~TXT
24
+ setup copy DEPRECATIONS.md and RELEASE.md into this project
25
+ bump move the version, refusing to bump onto a deprecation deadline
26
+ changelog lint CHANGELOG.md, or have an AI CLI write this version's entry
27
+ deprecations list the deprecations this gem has not yet honoured
28
+ release gate, build and push this gem
29
+ tag tag the current version in git
30
+ TXT
31
+ end
32
+
33
+ def usage = "#{program_name} SUBCOMMAND [options]"
34
+
35
+ def defaults_str = "(lists the subcommands)"
36
+
37
+ def description
38
+ <<~TXT
39
+ A deprecation is a dated promise: it names its replacement and the version
40
+ the old name stops existing in. These commands keep those promises — `bump`
41
+ refuses to move onto a deadline, and `release` refuses to ship a version with
42
+ an unkept promise or no changelog entry of its own.
43
+
44
+ Everything is read from the .gemspec in the working directory, so there is
45
+ nothing to configure in the common case.
46
+
47
+ Run `gem kit SUBCOMMAND --help` for one subcommand's arguments, options and
48
+ examples.
49
+ TXT
50
+ end
51
+
52
+ # Inert on purpose: the dry-cli registry parses the argv, including the
53
+ # subcommand's own flags.
54
+ def handle_options(args)
55
+ @argv = args.dup
56
+ end
57
+
58
+ def execute
59
+ # require_relative, not require: this resolves whether or not the gem
60
+ # is installed, which keeps the command testable from the repo.
61
+ require_relative "../../gem_kit/release/cli"
62
+
63
+ terminate_interaction(GemKit::Release::CLI.run(@argv || []))
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ __END__
70
+
71
+ describe "rubygems/commands/kit_command" do
72
+ require_relative "../../../spec/support/gem_kit_release_spec"
73
+ extend GemKitReleaseSpec
74
+
75
+ it "dispatches to a subcommand, passing its arguments through" do
76
+ with_gem do |dir|
77
+ status, out, _err = invoke(["bump", "minor"], dir)
78
+
79
+ status.should == 0
80
+ out.should.match(/1\.2\.3 -> 1\.3\.0/)
81
+ File.read(File.join(dir, "lib/demo/version.rb")).should.match(/"1\.3\.0"/)
82
+ end
83
+ end
84
+
85
+ it "passes flags through to the subcommand's own parser" do
86
+ with_gem do |dir|
87
+ status, out, _err = invoke(["bump", "major", "--force"], dir,
88
+ deprecations: [["Old", "New", "2.0"]])
89
+
90
+ status.should == 0
91
+ out.should.match(/--force given/)
92
+ end
93
+ end
94
+
95
+ it "propagates a subcommand's failure as a non-zero status" do
96
+ with_gem do |dir|
97
+ status, _out, err = invoke(["changelog", "9.9.9"], dir)
98
+ status.should == 1
99
+ err.should.match(/no section for 9\.9\.9/)
100
+ end
101
+ end
102
+
103
+ it "describes itself for `gem help kit`" do
104
+ command = Gem::Commands::KitCommand.new
105
+
106
+ command.command.should == "kit"
107
+ command.usage.should.match(/gem kit SUBCOMMAND/)
108
+ command.arguments.should.match(/deprecations/)
109
+ command.description.should.match(/dated promise/)
110
+ end
111
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RubyGems loads this file from every installed gem's lib/ on every `gem`
4
+ # invocation, which is how `gem kit` becomes a real command. Keep it to the
5
+ # require and the registration — anything heavier is a tax on `gem list`.
6
+
7
+ require "rubygems/command_manager"
8
+
9
+ require_relative "rubygems/commands/kit_command"
10
+
11
+ Gem::CommandManager.instance.register_command :kit