gem_kit-release 0.1.0 → 0.2.1

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.
@@ -1,66 +1,184 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/cli"
3
+ require "thor"
4
4
 
5
- require_relative "cli/command"
6
- require_relative "cli/bump"
7
- require_relative "cli/changelog"
8
- require_relative "cli/deprecations"
9
- require_relative "cli/release"
10
- require_relative "cli/setup"
11
- require_relative "cli/tag"
5
+ # `require`, not `require_relative`: gem_kit is a separate gem, and inside an
6
+ # installed gem_kit-release there is no ../gem_kit to reach. It works in the
7
+ # monorepo, where both gems share one lib/, which is exactly why this is worth
8
+ # a comment.
9
+ require "gem_kit"
10
+ require_relative "version"
11
+ require_relative "changelog"
12
+ require_relative "project"
13
+ require_relative "version_file"
14
+ require_relative "gate"
15
+ require_relative "commands/command"
16
+ require_relative "commands/bump"
17
+ require_relative "commands/changelog"
18
+ require_relative "commands/deprecations"
19
+ require_relative "commands/release"
20
+ require_relative "commands/tag"
21
+ require_relative "generators/setup"
12
22
 
13
23
  module GemKit
14
24
  module Release
15
- # The command registry. dry-cli gives us the parts a hand-rolled parser
16
- # always ends up needing badly: per-command help pages built from the
17
- # `argument`/`option`/`example` declarations, value validation, and a
18
- # listing of the subcommands.
25
+ # The command line behind `gem kit`, on Thor.
26
+ #
27
+ # Thor gives us the parts a hand-rolled parser always ends up needing
28
+ # badly: per-command help built from the `desc`/`method_option`
29
+ # declarations, a subcommand listing, and — the reason for choosing it over
30
+ # a plain option parser — Thor::Group and Thor::Actions, the same generator
31
+ # machinery Rails' `rails generate` is built on. `gem kit setup` is a
32
+ # generator, so it gets create/identical/conflict reporting and the
33
+ # overwrite prompt without writing any of it.
19
34
  #
20
35
  # gem kit the listing
21
- # gem kit bump --help that command's arguments, options, examples
36
+ # gem kit help bump that command's options
22
37
  #
23
- module CLI
24
- extend Dry::CLI::Registry
25
-
26
- register "setup", Setup, aliases: ["init"]
27
- register "bump", Bump
28
- register "changelog", Changelog, aliases: ["log"]
29
- register "deprecations", Deprecations, aliases: ["deps"]
30
- register "release", Release
31
- register "tag", Tag
32
-
33
- # How the commands are reached, which is not something dry-cli can work
34
- # out: it takes the program name from $PROGRAM_NAME, and ours is `gem`.
35
- PROGRAM_NAME = "gem kit"
36
-
37
- # Run one invocation. Returns an exit status rather than exiting, so the
38
- # `gem kit` bridge can hand it to terminate_interaction and the specs can
39
- # assert on it.
40
- def self.run(arguments, out: $stdout, err: $stderr)
41
- with_program_name do
42
- Dry::CLI.new(self).call(arguments: arguments, out: out, err: err)
43
- end
44
- 0
45
- rescue Failure => failure
46
- err.puts(failure.message)
47
- 1
48
- rescue SystemExit => exit_exception
49
- # dry-cli exits on an unknown command or a bad argument value.
50
- exit_exception.status
38
+ # Each command here is a thin front for a plain object under CLI::; the
39
+ # work itself is in Gate, VersionFile, Changelog and Deprecate.
40
+ class CLI < Thor
41
+ class_option :gem, type: :string,
42
+ desc: "Which gem, in a repository holding more than one gemspec"
43
+
44
+ # Thor asks; without it every invocation warns.
45
+ def self.exit_on_failure? = true
46
+
47
+ # `gem kit` with no arguments lists the commands rather than erroring.
48
+ def self.banner(command, _namespace = nil, _subcommand = false)
49
+ "gem kit #{command.usage}"
51
50
  end
52
51
 
53
- # dry-cli renders usage as `File.basename($PROGRAM_NAME) <command>`, so
54
- # every help page would otherwise say `gem bump` — or, under the test
55
- # runner, `-e`. Basename of "gem kit" is "gem kit", which is exactly the
56
- # string we want.
57
- def self.with_program_name
58
- original = $PROGRAM_NAME
59
- $PROGRAM_NAME = PROGRAM_NAME
60
- yield
61
- ensure
62
- $PROGRAM_NAME = original
52
+ # Short forms for the three typed most often.
53
+ map "log" => :changelog,
54
+ "deps" => :deprecations,
55
+ "init" => :setup
56
+
57
+ register(Generators::Setup, "setup", "setup",
58
+ "Copy DEPRECATIONS.md and RELEASE.md into this project")
59
+
60
+ desc "bump [SEGMENT]", "Move the gem version, refusing to bump onto a deprecation deadline"
61
+ long_desc <<~TXT
62
+ Rewrites the gem's version file — by rendering its .erb template if there is
63
+ one, otherwise by substituting the version literal in place.
64
+
65
+ Refuses to bump onto a deprecation deadline. A deprecation names the version
66
+ the old name stops existing in; bumping to that version while the old name is
67
+ still in the tree breaks that promise. Remove the deprecated code first, or
68
+ pass --force, which says what it is overriding.
69
+ TXT
70
+ method_option :force, type: :boolean, default: false, aliases: "-f",
71
+ desc: "Bump even when a deprecation comes due"
72
+ def bump(segment = "patch")
73
+ Commands::Bump.new(options).call(segment)
74
+ end
75
+
76
+ desc "changelog [VERSION]", "Lint CHANGELOG.md, or have an AI CLI write this version's entry"
77
+ long_desc <<~TXT
78
+ Checks CHANGELOG.md against Keep a Changelog: the title, that every heading is
79
+ `## [Unreleased]` or `## [1.2.3] - YYYY-MM-DD`, that versions are valid, dated,
80
+ unique and ordered newest-first, and that ### sections are one of Added,
81
+ Changed, Deprecated, Removed, Fixed, Security — none of them empty.
82
+
83
+ Given a version, it also asks whether that version has a non-empty section of
84
+ its own sitting at the top of the released list.
85
+
86
+ With --write, hands the job to the CLI named by config.changelog_writer
87
+ (default: claude). Review what it writes.
88
+ TXT
89
+ method_option :write, type: :boolean, default: false, aliases: "-w",
90
+ desc: "Ask the configured AI CLI to write this version's entry"
91
+ def changelog(version = nil)
92
+ Commands::Changelog.new(options).call(version)
93
+ end
94
+
95
+ desc "deprecations [VERSION]", "List the deprecations this gem has not yet honoured"
96
+ long_desc <<~TXT
97
+ A deprecation names its replacement and the version the old name stops
98
+ existing in. This lists the promises still outstanding, with the source
99
+ location of each declaration.
100
+
101
+ Given a version, it lists only what comes due there and fails if anything
102
+ does, which is what makes it usable as a gate in CI.
103
+ TXT
104
+ def deprecations(version = nil)
105
+ Commands::Deprecations.new(options).call(version)
63
106
  end
107
+
108
+ desc "release", "Gate, build and push this gem"
109
+ long_desc <<~TXT
110
+ Two gates, both before `gem build` runs:
111
+
112
+ the changelog must have a non-empty, correctly formatted section for this
113
+ version, sitting at the top of the released list; and nothing promised to
114
+ disappear in this version may still be in the tree.
115
+
116
+ Then builds the gemspec and pushes it. Requires RubyGems push credentials.
117
+ --dry-run runs the gates and stops, which is what belongs in CI.
118
+ TXT
119
+ method_option :dry_run, type: :boolean, default: false, aliases: "-n",
120
+ desc: "Run the gates and stop"
121
+ method_option :tag, type: :boolean, default: false, aliases: "-t",
122
+ desc: "Tag the release after pushing"
123
+ def release
124
+ Commands::Release.new(options).call
125
+ end
126
+
127
+ desc "tag", "Tag the current version in git"
128
+ long_desc <<~TXT
129
+ Creates the annotated tag <prefix><version> — v4.1.0 by default — for the
130
+ version in the gem's version file, refusing if that tag already exists.
131
+
132
+ The tag is what the next changelog is written against, so a missing one makes
133
+ the following release harder to describe.
134
+ TXT
135
+ method_option :push, type: :boolean, default: false, aliases: "-p",
136
+ desc: "Push the tag to origin"
137
+ method_option :prefix, type: :string,
138
+ desc: "Tag name prefix (default: v, or <gem>-v in a multi-gem repository)"
139
+ def tag
140
+ Commands::Tag.new(options).call
141
+ end
142
+ end
143
+
144
+ # The extension seam. A plugin gem adds commands to `gem kit` by reopening
145
+ # the CLI through this, which is a documented entry point rather than a
146
+ # class_eval someone reverse-engineered:
147
+ #
148
+ # # lib/gem_kit/plugin.rb, in a gem of your own
149
+ # require "gem_kit/release/cli"
150
+ #
151
+ # GemKit::Release.plugin do
152
+ # desc "lint", "Check this gem for the things gems get wrong"
153
+ # def lint = GemKit::Plugin::Lint.new(options).call
154
+ # end
155
+ #
156
+ # The block is evaluated on the Thor class, so everything Thor offers is
157
+ # in scope: `desc`, `long_desc`, `method_option`, `map`, and `register` for
158
+ # a Thor::Group generator. Commands added this way are indistinguishable
159
+ # from the built-in ones — they appear in `gem kit`, take `--gem`, and get
160
+ # a help page.
161
+ #
162
+ # Loading is the plugin's own business: ship a lib/rubygems_plugin.rb that
163
+ # requires your file, and RubyGems will load it on every `gem` invocation,
164
+ # the same way this gem gets loaded.
165
+ def self.plugin(&block)
166
+ CLI.class_eval(&block)
167
+ CLI
168
+ end
169
+
170
+ # Run one invocation. Returns an exit status rather than exiting, so the
171
+ # `gem kit` bridge can hand it to terminate_interaction and the specs can
172
+ # assert on it.
173
+ def self.run(arguments, out: $stdout, err: $stderr)
174
+ CLI.start(arguments)
175
+ 0
176
+ rescue Failure => failure
177
+ err.puts(failure.message)
178
+ 1
179
+ rescue SystemExit => exit_exception
180
+ # Thor exits on an unknown command or a missing required argument.
181
+ exit_exception.status
64
182
  end
65
183
  end
66
184
  end
@@ -71,51 +189,89 @@ describe "gem_kit/release/cli" do
71
189
  require_relative "../../../spec/support/gem_kit_release_spec"
72
190
  extend GemKitReleaseSpec
73
191
 
74
- # With no subcommand dry-cli treats it as a usage error: the listing goes to
75
- # stderr and the status is non-zero, the way `git` alone behaves.
76
- it "lists every subcommand with its description" do
192
+ it "lists every command with its description" do
77
193
  with_gem do |dir|
78
- status, _out, err = invoke([], dir)
194
+ _status, out, _err = invoke([], dir)
79
195
 
80
- status.should.not == 0
81
- %w[setup bump changelog deprecations release tag].each do |name|
82
- err.should.match(/#{name}/)
83
- end
84
- err.should.match(/Move the gem version/)
196
+ %w[setup bump changelog deprecations release tag].each { |name| out.should.match(/#{name}/) }
197
+ out.should.match(/Move the gem version/)
85
198
  end
86
199
  end
87
200
 
88
- it "names the commands `gem kit <subcommand>`, not the running program" do
201
+ it "names the commands `gem kit <command>`, not the running program" do
89
202
  with_gem do |dir|
90
- _status, _out, err = invoke([], dir)
91
-
92
- err.should.match(/gem kit bump \[SEGMENT\]/)
93
- err.should.not.match(/^\s+-e /)
94
- end
95
- end
203
+ _status, out, _err = invoke([], dir)
96
204
 
97
- it "restores $PROGRAM_NAME afterwards" do
98
- with_gem do |dir|
99
- before = $PROGRAM_NAME
100
- invoke([], dir)
101
- $PROGRAM_NAME.should == before
205
+ out.should.match(/gem kit bump/)
206
+ out.should.not.match(/^\s+rspec /)
102
207
  end
103
208
  end
104
209
 
105
- it "prints a subcommand's arguments, options and examples for --help" do
210
+ it "prints a command's options and long description for help" do
106
211
  with_gem do |dir|
107
- _status, out, _err = invoke(["bump", "--help"], dir)
212
+ _status, out, _err = invoke(["help", "bump"], dir)
108
213
 
109
214
  out.should.match(/gem kit bump/)
110
215
  out.should.match(/SEGMENT/)
111
216
  out.should.match(/--force/)
112
- out.should.match(/Examples:/)
217
+ out.should.match(/deprecation deadline/)
113
218
  end
114
219
  end
115
220
 
116
- it "fails on an unknown subcommand" do
221
+ it "fails on an unknown command" do
117
222
  with_gem do |dir|
118
223
  invoke(["nonsense"], dir).first.should.not == 0
119
224
  end
120
225
  end
226
+
227
+ # What a plugin gem does. The command has to be indistinguishable from a
228
+ # built-in one, or the seam is not worth documenting.
229
+ it "takes a command from a plugin" do
230
+ GemKit::Release.plugin do
231
+ desc "hello NAME", "Say hello from a plugin"
232
+ def hello(name) = $stdout.puts("hello #{name} (#{options[:gem] || "no --gem"})")
233
+ end
234
+
235
+ begin
236
+ with_gem do |dir|
237
+ status, out, _err = invoke(["hello", "world"], dir)
238
+ status.should == 0
239
+ out.should.match(/hello world/)
240
+
241
+ # The class options reach it too.
242
+ _status, out, _err = invoke(["hello", "world", "--gem", "demo"], dir)
243
+ out.should.match(/hello world \(demo\)/)
244
+
245
+ # And it is listed and documented like the rest.
246
+ _status, listing, _err = invoke([], dir)
247
+ listing.should.match(/gem kit hello NAME/)
248
+ listing.should.match(/Say hello from a plugin/)
249
+ end
250
+ ensure
251
+ GemKit::Release::CLI.remove_command("hello")
252
+ end
253
+ end
254
+
255
+ it "hands the plugin the whole Thor DSL, generators included" do
256
+ generator = Class.new(Thor::Group) do
257
+ include Thor::Actions
258
+ def self.banner = "gem kit scaffold"
259
+ def report = $stdout.puts("scaffolded")
260
+ end
261
+ Object.const_set(:PluginScaffoldGenerator, generator) unless defined?(PluginScaffoldGenerator)
262
+
263
+ GemKit::Release.plugin do
264
+ register(PluginScaffoldGenerator, "scaffold", "scaffold", "Scaffold something")
265
+ end
266
+
267
+ begin
268
+ with_gem do |dir|
269
+ status, out, _err = invoke(["scaffold"], dir)
270
+ status.should == 0
271
+ out.should.match(/scaffolded/)
272
+ end
273
+ ensure
274
+ GemKit::Release::CLI.remove_command("scaffold")
275
+ end
276
+ end
121
277
  end
@@ -4,29 +4,16 @@ require_relative "command"
4
4
 
5
5
  module GemKit
6
6
  module Release
7
- module CLI
7
+ module Commands
8
8
  # The deadline check lives here rather than at release time because the
9
9
  # bump is the last moment anyone can be stopped: once the version file
10
10
  # says 5.0.0, every promise to disappear "in 5.0" is already broken.
11
11
  class Bump < Command
12
- desc "Move the gem version, refusing to bump onto a deprecation deadline"
13
12
 
14
- argument :segment,
15
- type: :string, required: false, default: "patch",
16
- values: %w[major minor patch],
17
- desc: "Which segment of the version to move"
18
13
 
19
- option :force,
20
- type: :boolean, default: false,
21
- desc: "Bump even when a deprecation comes due"
22
14
 
23
- example [
24
- "minor # 4.1.0 -> 4.2.0",
25
- "major # refused while a 5.0 deprecation is still in the tree",
26
- "major --force # bump anyway, saying what it overrode",
27
- ]
28
15
 
29
- def call(segment: "patch", **options)
16
+ def call(segment = "patch")
30
17
  current = project.version.to_s
31
18
 
32
19
  begin
@@ -65,7 +52,7 @@ end
65
52
 
66
53
  __END__
67
54
 
68
- describe "gem_kit/release/cli/bump" do
55
+ describe "gem_kit/release/commands/bump" do
69
56
  require_relative "../../../../spec/support/gem_kit_release_spec"
70
57
  extend GemKitReleaseSpec
71
58
 
@@ -4,27 +4,15 @@ require_relative "command"
4
4
 
5
5
  module GemKit
6
6
  module Release
7
- module CLI
7
+ module Commands
8
8
  # Linting is the default because it is the safe, read-only half. Writing
9
9
  # edits a file and costs money, so it asks for the flag.
10
10
  class Changelog < Command
11
- desc "Lint CHANGELOG.md, or have an AI CLI write this version's entry"
12
11
 
13
- argument :version,
14
- type: :string, required: false,
15
- desc: "Check that this version is ready to release (default: format only)"
16
12
 
17
- option :write,
18
- type: :boolean, default: false,
19
- desc: "Ask the configured AI CLI to write this version's entry"
20
13
 
21
- example [
22
- " # lint the format",
23
- "4.2.0 # ...and check 4.2.0 is ready to release",
24
- "--write # hand the entry to the configured AI CLI",
25
- ]
26
14
 
27
- def call(version: nil, **options)
15
+ def call(version = nil)
28
16
  return write_entry if options[:write]
29
17
 
30
18
  problems = version ? gate.release_problems(version) : gate.changelog_problems
@@ -84,7 +72,7 @@ end
84
72
 
85
73
  __END__
86
74
 
87
- describe "gem_kit/release/cli/changelog" do
75
+ describe "gem_kit/release/commands/changelog" do
88
76
  require_relative "../../../../spec/support/gem_kit_release_spec"
89
77
  extend GemKitReleaseSpec
90
78
 
@@ -1,30 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/cli"
4
-
5
3
  require_relative "../../release"
6
4
 
7
5
  module GemKit
8
6
  module Release
9
- module CLI
10
- # Raised instead of exiting, so the caller decides what a failure means:
11
- # the `gem kit` bridge turns it into terminate_interaction, and the specs
12
- # turn it into a status.
13
- class Failure < StandardError; end
7
+ # Raised instead of exiting, so the caller decides what a failure means:
8
+ # the Thor CLI turns it into a non-zero status, and the specs assert on it.
9
+ class Failure < StandardError; end
14
10
 
15
- # Shared base for the subcommands. Each is thin — declare arguments and
16
- # options so dry-cli can render the help page, ask a library object a
17
- # question, report. The work stays in Gate, VersionFile, Changelog and
18
- # Deprecate.
19
- class Command < Dry::CLI::Command
11
+ module Commands
12
+ # Shared base for the commands behind `gem kit`. Each is thin ask a
13
+ # library object a question, report and knows nothing about Thor. The
14
+ # work stays in Gate, VersionFile, Changelog and Deprecate; the parsing,
15
+ # the help pages and the generator machinery stay in Thor.
16
+ class Command
20
17
  RED = "\e[0;31m"
21
18
  RESET = "\e[0m"
22
19
 
20
+ attr_reader :options
21
+
22
+ def initialize(options = {})
23
+ @options = options
24
+ end
25
+
23
26
  # The gem in the working directory. Everything is inferred from its
24
27
  # .gemspec, so there is nothing to configure in the common case.
28
+ # `--gem` picks one out of a repository holding several.
25
29
  def project
26
- @project ||= Project.detect
27
- rescue Project::NotFound => error
30
+ @project ||= Project.detect(Dir.pwd, name: options[:gem])
31
+ rescue Project::NotFound, Project::Ambiguous => error
28
32
  fail_with(error.message)
29
33
  end
30
34
 
@@ -4,23 +4,15 @@ require_relative "command"
4
4
 
5
5
  module GemKit
6
6
  module Release
7
- module CLI
7
+ module Commands
8
8
  # Without a version: everything outstanding. With one: only what comes
9
9
  # due there, failing if anything does — which is what makes it usable as
10
10
  # a CI gate.
11
11
  class Deprecations < Command
12
- desc "List the deprecations this gem has not yet honoured"
13
12
 
14
- argument :version,
15
- type: :string, required: false,
16
- desc: "List only what comes due at this version, and fail if any does"
17
13
 
18
- example [
19
- " # everything outstanding, by removal version",
20
- "5.0.0 # only what comes due at 5.0.0; fails if any does",
21
- ]
22
14
 
23
- def call(version: nil, **)
15
+ def call(version = nil)
24
16
  project.load!
25
17
  registry = Deprecate.registry
26
18
 
@@ -34,7 +26,7 @@ module GemKit
34
26
  return render(registry)
35
27
  end
36
28
 
37
- due = Deprecate.pending(version)
29
+ due = GemKit::Deprecate.pending(version)
38
30
  if due.empty?
39
31
  say("No deprecations come due at #{version}. (#{registry.size} outstanding overall.)")
40
32
  return
@@ -49,7 +41,7 @@ end
49
41
 
50
42
  __END__
51
43
 
52
- describe "gem_kit/release/cli/deprecations" do
44
+ describe "gem_kit/release/commands/deprecations" do
53
45
  require_relative "../../../../spec/support/gem_kit_release_spec"
54
46
  extend GemKitReleaseSpec
55
47
 
@@ -4,21 +4,14 @@ require_relative "command"
4
4
 
5
5
  module GemKit
6
6
  module Release
7
- module CLI
7
+ module Commands
8
8
  # Gate, build, push. The gates run before anything is built, because a
9
9
  # half-done release is worse than a refused one.
10
10
  class Release < Command
11
- desc "Gate, build and push this gem"
12
11
 
13
- option :dry_run, type: :boolean, default: false, desc: "Run the gates and stop"
14
- option :tag, type: :boolean, default: false, desc: "Tag the release after pushing"
15
12
 
16
- example [
17
- "--dry-run # run the gates; what belongs in CI",
18
- "--tag # release, then tag it",
19
- ]
20
13
 
21
- def call(**options)
14
+ def call
22
15
  version = project.version.to_s
23
16
  problems = gate.release_problems(version)
24
17
 
@@ -42,7 +35,7 @@ module GemKit
42
35
 
43
36
  say("Released #{project.name} #{version}")
44
37
 
45
- Tag.new.call if options[:tag]
38
+ Tag.new(options).call if options[:tag]
46
39
  end
47
40
  end
48
41
  end
@@ -51,7 +44,7 @@ end
51
44
 
52
45
  __END__
53
46
 
54
- describe "gem_kit/release/cli/release" do
47
+ describe "gem_kit/release/commands/release" do
55
48
  require_relative "../../../../spec/support/gem_kit_release_spec"
56
49
  extend GemKitReleaseSpec
57
50
 
@@ -4,24 +4,16 @@ require_relative "command"
4
4
 
5
5
  module GemKit
6
6
  module Release
7
- module CLI
7
+ module Commands
8
8
  # The tag matters beyond bookkeeping: it is what the next changelog is
9
9
  # written against, so a missing one makes the following release harder to
10
10
  # describe.
11
11
  class Tag < Command
12
- desc "Tag the current version in git"
13
12
 
14
- option :push, type: :boolean, default: false, desc: "Push the tag to origin"
15
- option :prefix, type: :string, default: "v", desc: "Tag name prefix"
16
13
 
17
- example [
18
- " # tag v4.1.0",
19
- "--push # ...and push it",
20
- "--prefix release- # tag release-4.1.0",
21
- ]
22
14
 
23
- def call(**options)
24
- tag = "#{options.fetch(:prefix, "v")}#{project.version}"
15
+ def call
16
+ tag = "#{options[:prefix] || project.tag_prefix}#{project.version}"
25
17
 
26
18
  Dir.chdir(project.root) do
27
19
  fail_with("not a git repository") unless system("git rev-parse --git-dir >/dev/null 2>&1")
@@ -45,7 +37,7 @@ end
45
37
 
46
38
  __END__
47
39
 
48
- describe "gem_kit/release/cli/tag" do
40
+ describe "gem_kit/release/commands/tag" do
49
41
  require_relative "../../../../spec/support/gem_kit_release_spec"
50
42
  extend GemKitReleaseSpec
51
43
 
@@ -67,6 +59,28 @@ describe "gem_kit/release/cli/tag" do
67
59
  end
68
60
  end
69
61
 
62
+ # Two gems at the same version in one repository would otherwise fight over
63
+ # a single `v0.1.0`.
64
+ it "namespaces the tag by gem when the repository holds several" do
65
+ with_gem do |dir|
66
+ as_repo.call(dir)
67
+ File.write(File.join(dir, "other.gemspec"), <<~RUBY)
68
+ Gem::Specification.new do |spec|
69
+ spec.name = "other"
70
+ spec.version = "1.2.3"
71
+ spec.authors = ["x"]
72
+ spec.summary = "x"
73
+ spec.files = []
74
+ end
75
+ RUBY
76
+
77
+ invoke(["tag", "--gem", "demo"], dir).first.should == 0
78
+ invoke(["tag", "--gem", "other"], dir).first.should == 0
79
+
80
+ `git -C #{dir} tag -l`.split.sort.should == ["demo-v1.2.3", "other-v1.2.3"]
81
+ end
82
+ end
83
+
70
84
  it "honours a custom prefix" do
71
85
  with_gem do |dir|
72
86
  as_repo.call(dir)