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,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/deprecate"
4
+
5
+ module GemKit
6
+ module Release
7
+ # A deprecation is a dated promise: it names the replacement *and* the
8
+ # version the old name stops existing in. Built on Gem::Deprecate, which
9
+ # gets the message format and the skip_during escape hatch right, plus one
10
+ # addition — a registry, so the set of outstanding promises is data the
11
+ # release tooling can enforce rather than prose someone has to remember.
12
+ #
13
+ # Deprecate a method:
14
+ #
15
+ # class Session
16
+ # extend GemKit::Release::Deprecate
17
+ #
18
+ # def old_reset = new_reset
19
+ # deprecate :old_reset, "Session#new_reset", "5.0"
20
+ # end
21
+ #
22
+ # Deprecate a whole constant that has moved or been renamed — leave the old
23
+ # name in place as a subclass of the new one, then declare it:
24
+ #
25
+ # class Completion < Brute::Completion::OpenRouter
26
+ # extend GemKit::Release::Deprecate
27
+ # superseded_by "Brute::Completion::OpenRouter", "5.0"
28
+ # end
29
+ #
30
+ # Both warn on use, naming the caller. Gem::Deprecate.skip_during silences
31
+ # them, so a test suite can exercise the old path in quiet.
32
+ module Deprecate
33
+ extend Gem::Deprecate
34
+
35
+ # One outstanding deprecation. `removed_in` is the deadline the release
36
+ # gate reads.
37
+ Entry = Struct.new(:name, :replacement, :removed_in, :declared_at, keyword_init: true) do
38
+ def to_s
39
+ "#{name} -> #{replacement == :none ? "(no replacement)" : replacement}"
40
+ end
41
+ end
42
+
43
+ class << self
44
+ # Every deprecation declared in the loaded library, in declaration order.
45
+ def registry
46
+ @registry ||= []
47
+ end
48
+
49
+ def register(name:, replacement:, removed_in:, declared_at: nil)
50
+ entry = Entry.new(
51
+ name: name.to_s,
52
+ replacement: replacement,
53
+ removed_in: Gem::Version.new(removed_in.to_s),
54
+ declared_at: declared_at || location(1),
55
+ )
56
+ registry << entry
57
+ entry
58
+ end
59
+
60
+ # The deprecations that come due at `version` — every deadline that has
61
+ # arrived or passed. Releasing `version` with any of these still in the
62
+ # tree breaks the promise the warning made.
63
+ def pending(version)
64
+ target = Gem::Version.new(version.to_s)
65
+ registry.select { |entry| entry.removed_in <= target }
66
+ end
67
+
68
+ # Deprecations still inside their grace period at `version`.
69
+ def upcoming(version)
70
+ target = Gem::Version.new(version.to_s)
71
+ registry.reject { |entry| entry.removed_in <= target }
72
+ end
73
+
74
+ # Single funnel for every warning: Gem::Deprecate.skip_during works
75
+ # across all of them, and specs have one place to listen.
76
+ def warn(message)
77
+ Kernel.warn(message) unless Gem::Deprecate.skip
78
+ end
79
+
80
+ # The Gem::Deprecate-shaped message. `origin` must be computed at the
81
+ # call site — one frame deeper and it names this file rather than the
82
+ # code that needs changing.
83
+ def message(target, replacement, removed_in, origin)
84
+ [
85
+ "NOTE: #{target} is deprecated",
86
+ replacement == :none ? " with no replacement" : "; use #{replacement} instead",
87
+ ". It will be removed in #{removed_in}",
88
+ "\n#{target} called from #{origin}.",
89
+ ].join
90
+ end
91
+
92
+ def location(depth)
93
+ caller_locations(depth + 1, 1)&.first&.then { |l| "#{l.path}:#{l.lineno}" }
94
+ end
95
+ end
96
+
97
+ # Deprecate one method. Mirrors Gem::Deprecate#rubygems_deprecate, but the
98
+ # deadline is explicit — a deprecation added late in a cycle usually wants
99
+ # the major after next, and guessing that is not the tool's business.
100
+ def deprecate(name, replacement, removed_in)
101
+ label = singleton_class? ? "#{attached_object}.#{name}" : "#{self}##{name}"
102
+ Deprecate.register(name: label, replacement: replacement, removed_in: removed_in,
103
+ declared_at: Deprecate.location(1))
104
+
105
+ class_eval do
106
+ old = "_deprecated_#{name}"
107
+ alias_method old, name
108
+ define_method name do |*args, &block|
109
+ target = is_a?(Module) ? "#{self}.#{name}" : "#{self.class}##{name}"
110
+ origin = Gem.location_of_caller.join(":")
111
+ Deprecate.warn(Deprecate.message(target, replacement, removed_in, origin))
112
+ send(old, *args, &block)
113
+ end
114
+ ruby2_keywords name if respond_to?(:ruby2_keywords, true)
115
+ end
116
+ end
117
+
118
+ # Deprecate the constant this is called in — the renamed-or-moved case.
119
+ # Named `superseded_by` rather than `deprecate_constant` because Module
120
+ # already has a method by that name and shadowing it would be rude.
121
+ def superseded_by(replacement, removed_in)
122
+ Deprecate.register(name: name || to_s, replacement: replacement, removed_in: removed_in,
123
+ declared_at: Deprecate.location(1))
124
+
125
+ return unless respond_to?(:new)
126
+
127
+ define_singleton_method(:new) do |*args, **options, &block|
128
+ origin = Gem.location_of_caller.join(":")
129
+ Deprecate.warn(Deprecate.message(name || to_s, replacement, removed_in, origin))
130
+ super(*args, **options, &block)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ __END__
138
+
139
+ describe "gem_kit/release/deprecate" do
140
+ Deprecate = GemKit::Release::Deprecate unless defined?(Deprecate)
141
+
142
+ captured = []
143
+ # Capture what Deprecate.warn emits and keep the shared registry clean —
144
+ # these specs declare throwaway deprecations.
145
+ isolated = lambda do |&block|
146
+ saved = Deprecate.registry.dup
147
+ original = Deprecate.method(:warn)
148
+ captured.clear
149
+ Deprecate.define_singleton_method(:warn) { |message| captured << message }
150
+ begin
151
+ block.call
152
+ ensure
153
+ Deprecate.define_singleton_method(:warn, original)
154
+ Deprecate.registry.replace(saved)
155
+ end
156
+ end
157
+
158
+ it "warns on a deprecated method, naming replacement, version and caller" do
159
+ isolated.call do
160
+ klass = Class.new do
161
+ extend Deprecate
162
+ def new_name = :result
163
+ def old_name = new_name
164
+ deprecate :old_name, "Thing#new_name", "9.0"
165
+ end
166
+
167
+ klass.new.old_name.should == :result # still works
168
+ captured.size.should == 1
169
+ captured.first.should.match(/is deprecated/)
170
+ captured.first.should.match(/use Thing#new_name instead/)
171
+ captured.first.should.match(/removed in 9\.0/)
172
+ captured.first.should.match(/called from /)
173
+ end
174
+ end
175
+
176
+ it "names the caller, not the deprecation machinery" do
177
+ isolated.call do
178
+ klass = Class.new do
179
+ extend Deprecate
180
+ def old_name = :result
181
+ deprecate :old_name, "Thing#new_name", "9.0"
182
+ end
183
+
184
+ # These specs live in this file's __END__, so "the caller" is a line in
185
+ # deprecate.rb either way — pin the exact line to tell them apart.
186
+ klass.new.old_name; call_line = __LINE__
187
+ captured.first.should.match(/called from .*deprecate\.rb:#{call_line}\./)
188
+ end
189
+ end
190
+
191
+ it "labels a class-method deprecation by the class, not its singleton" do
192
+ isolated.call do
193
+ Class.new do
194
+ def self.to_s = "Demo"
195
+ def self.old_thing = :ok
196
+ class << self
197
+ extend Deprecate
198
+ deprecate :old_thing, "Other.new_thing", "9.0"
199
+ end
200
+ end
201
+
202
+ Deprecate.registry.last.name.should == "Demo.old_thing"
203
+ end
204
+ end
205
+
206
+ it "warns on a superseded constant but keeps it working" do
207
+ isolated.call do
208
+ modern = Class.new { def initialize(x); @x = x; end; attr_reader :x }
209
+ legacy = Class.new(modern) do
210
+ extend Deprecate
211
+ def self.name = "Old::Name"
212
+ superseded_by "New::Name", "9.0"
213
+ end
214
+
215
+ legacy.new(42).x.should == 42 # still works
216
+ captured.size.should == 1
217
+ captured.first.should.match(/Old::Name is deprecated; use New::Name instead/)
218
+ end
219
+ end
220
+
221
+ it "supports :none for a deprecation with no replacement" do
222
+ isolated.call do
223
+ klass = Class.new do
224
+ extend Deprecate
225
+ def gone = :ok
226
+ deprecate :gone, :none, "9.0"
227
+ end
228
+
229
+ klass.new.gone
230
+ captured.first.should.match(/with no replacement/)
231
+ end
232
+ end
233
+
234
+ it "registers each declaration with its deadline and source" do
235
+ isolated.call do
236
+ Class.new do
237
+ extend Deprecate
238
+ def gone = nil
239
+ deprecate :gone, "Other#kept", "9.0"
240
+ end
241
+
242
+ entry = Deprecate.registry.last
243
+ entry.replacement.should == "Other#kept"
244
+ entry.removed_in.should == Gem::Version.new("9.0")
245
+ entry.declared_at.should.match(/deprecate\.rb:\d+/)
246
+ end
247
+ end
248
+
249
+ it "splits the registry into pending and upcoming at a version" do
250
+ isolated.call do
251
+ Deprecate.registry.clear
252
+ Deprecate.register(name: "A", replacement: "A2", removed_in: "5.0")
253
+ Deprecate.register(name: "B", replacement: "B2", removed_in: "6.0")
254
+
255
+ Deprecate.pending("5.0.0").map(&:name).should == ["A"]
256
+ Deprecate.upcoming("5.0.0").map(&:name).should == ["B"]
257
+ Deprecate.pending("4.9.0").should.be.empty
258
+ Deprecate.pending("6.1.0").map(&:name).should == ["A", "B"]
259
+ end
260
+ end
261
+
262
+ it "stays quiet inside Gem::Deprecate.skip_during" do
263
+ saved = Deprecate.registry.dup
264
+ begin
265
+ klass = Class.new do
266
+ extend Deprecate
267
+ def quiet = :ok
268
+ deprecate :quiet, "Other#loud", "9.0"
269
+ end
270
+
271
+ warned = []
272
+ original = Kernel.method(:warn)
273
+ Kernel.define_singleton_method(:warn) { |*args| warned << args.join }
274
+ begin
275
+ Gem::Deprecate.skip_during { klass.new.quiet.should == :ok }
276
+ ensure
277
+ Kernel.define_singleton_method(:warn, original)
278
+ end
279
+
280
+ warned.should.be.empty
281
+ ensure
282
+ Deprecate.registry.replace(saved)
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemKit
4
+ module Release
5
+ # The checks, in one place. Both the bump and the release ask the same two
6
+ # questions of a target version, and neither should be hand-rolling them:
7
+ #
8
+ # Is anything promised to disappear in this version still here?
9
+ # Does the changelog document this version?
10
+ #
11
+ # Every method returns a list of human-readable problems. Empty means pass,
12
+ # which makes the callers trivial and lets CI use the same object.
13
+ class Gate
14
+ attr_reader :project
15
+
16
+ def initialize(project)
17
+ @project = project
18
+ end
19
+
20
+ # Deprecations whose deadline has arrived at `version`. This is the check
21
+ # that has to run *before* a bump: bumping onto a deadline is what breaks
22
+ # the promise, so the bump is the last moment anyone can be stopped.
23
+ def deprecation_problems(version)
24
+ project.load!
25
+
26
+ Deprecate.pending(version).map do |entry|
27
+ "#{entry.removed_in.to_s.ljust(8)} #{entry}#{entry.declared_at ? "\n#{" " * 9}#{entry.declared_at}" : ""}"
28
+ end
29
+ end
30
+
31
+ # Deprecations still inside their grace period — worth printing on the
32
+ # way past, not worth blocking on.
33
+ def upcoming_deprecations(version)
34
+ project.load!
35
+ Deprecate.upcoming(version)
36
+ end
37
+
38
+ # Changelog format, plus "is there an entry for this version?" when one
39
+ # is given. Without a version this is the format check alone, which is
40
+ # the useful thing to run on every push.
41
+ def changelog_problems(version = nil)
42
+ changelog = Changelog.new(
43
+ File.exist?(project.changelog_path) ? File.read(project.changelog_path) : nil,
44
+ path: project.changelog_path,
45
+ )
46
+
47
+ version ? changelog.release_problems(version) : changelog.problems
48
+ end
49
+
50
+ # Everything standing between the project and releasing `version`.
51
+ def release_problems(version = project.version)
52
+ problems = []
53
+
54
+ changelog_problems(version).each { |problem| problems << problem }
55
+ deprecation_problems(version).each do |problem|
56
+ problems << "deprecation due in #{version}: #{problem}"
57
+ end
58
+
59
+ problems
60
+ end
61
+
62
+ # Everything standing between the project and *bumping to* `version`.
63
+ # Only the deprecation deadline applies — the changelog for a version
64
+ # cannot exist before the version does.
65
+ def bump_problems(version)
66
+ deprecation_problems(version).map { |problem| "deprecation due in #{version}: #{problem}" }
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ __END__
73
+
74
+ describe "gem_kit/release/gate" do
75
+ require "tmpdir"
76
+
77
+ # A Project stub: the Gate only asks it for a changelog path and a version,
78
+ # and to load the library (a no-op here — the specs drive the registry).
79
+ stub_project = lambda do |changelog_path, version: "1.0.0"|
80
+ Struct.new(:changelog_path, :version) do
81
+ def load! = true
82
+ end.new(changelog_path, Gem::Version.new(version))
83
+ end
84
+
85
+ clean_changelog = <<~MD
86
+ # Changelog
87
+
88
+ ## [Unreleased]
89
+
90
+ ## [1.0.0] - 2026-01-01
91
+
92
+ ### Added
93
+
94
+ - A thing.
95
+ MD
96
+
97
+ # Run a block with the deprecation registry isolated.
98
+ isolated = lambda do |&block|
99
+ saved = GemKit::Release::Deprecate.registry.dup
100
+ GemKit::Release::Deprecate.registry.clear
101
+ begin
102
+ block.call
103
+ ensure
104
+ GemKit::Release::Deprecate.registry.replace(saved)
105
+ end
106
+ end
107
+
108
+ it "passes when the changelog documents the version and nothing is due" do
109
+ Dir.mktmpdir do |dir|
110
+ path = File.join(dir, "CHANGELOG.md")
111
+ File.write(path, clean_changelog)
112
+
113
+ isolated.call do
114
+ GemKit::Release::Gate.new(stub_project.call(path)).release_problems("1.0.0").should == []
115
+ end
116
+ end
117
+ end
118
+
119
+ it "reports a missing changelog section for the version" do
120
+ Dir.mktmpdir do |dir|
121
+ path = File.join(dir, "CHANGELOG.md")
122
+ File.write(path, clean_changelog)
123
+
124
+ isolated.call do
125
+ problems = GemKit::Release::Gate.new(stub_project.call(path)).release_problems("2.0.0")
126
+ problems.first.should.match(/no section for 2\.0\.0/)
127
+ end
128
+ end
129
+ end
130
+
131
+ it "reports a deprecation whose deadline has arrived" do
132
+ Dir.mktmpdir do |dir|
133
+ path = File.join(dir, "CHANGELOG.md")
134
+ File.write(path, clean_changelog)
135
+
136
+ isolated.call do
137
+ GemKit::Release::Deprecate.register(name: "Old", replacement: "New", removed_in: "1.0")
138
+ gate = GemKit::Release::Gate.new(stub_project.call(path))
139
+
140
+ gate.release_problems("1.0.0").first.should.match(/deprecation due in 1\.0\.0: .*Old -> New/)
141
+ gate.bump_problems("1.0.0").size.should == 1
142
+ end
143
+ end
144
+ end
145
+
146
+ it "does not block a bump on a deadline that has not arrived" do
147
+ Dir.mktmpdir do |dir|
148
+ isolated.call do
149
+ GemKit::Release::Deprecate.register(name: "Old", replacement: "New", removed_in: "2.0")
150
+ gate = GemKit::Release::Gate.new(stub_project.call(File.join(dir, "CHANGELOG.md")))
151
+
152
+ gate.bump_problems("1.1.0").should == []
153
+ gate.upcoming_deprecations("1.1.0").map(&:name).should == ["Old"]
154
+ end
155
+ end
156
+ end
157
+
158
+ it "checks changelog format alone when given no version" do
159
+ Dir.mktmpdir do |dir|
160
+ path = File.join(dir, "CHANGELOG.md")
161
+ File.write(path, "## [1.0.0] - 2026-01-01\n\n### Added\n\n- x\n")
162
+
163
+ isolated.call do
164
+ GemKit::Release::Gate.new(stub_project.call(path)).changelog_problems
165
+ .first.should.match(/must start with the title/)
166
+ end
167
+ end
168
+ end
169
+
170
+ it "reports a changelog that does not exist" do
171
+ Dir.mktmpdir do |dir|
172
+ isolated.call do
173
+ GemKit::Release::Gate.new(stub_project.call(File.join(dir, "CHANGELOG.md")))
174
+ .changelog_problems.first.should.match(/does not exist/)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+
5
+ module GemKit
6
+ module Release
7
+ # What the tooling needs to know about the project it is running in.
8
+ #
9
+ # Almost all of it is already declared in the project's .gemspec — name,
10
+ # version, and (through `require_relative`) where the version constant
11
+ # lives. So the normal case is no configuration at all: point it at a
12
+ # directory and it works out the rest.
13
+ #
14
+ # project = GemKit::Release::Project.detect
15
+ # project.name # => "brute"
16
+ # project.version # => Gem::Version.new("4.1.0")
17
+ # project.version_file # => ".../lib/brute/version.rb"
18
+ #
19
+ # Anything that cannot be inferred is an override:
20
+ #
21
+ # GemKit::Release.configure do |config|
22
+ # config.changelog = "HISTORY.md"
23
+ # config.test_command = "bin/test"
24
+ # end
25
+ class Project
26
+ class NotFound < StandardError; end
27
+
28
+ # Overridable settings. Each is nil until set, and nil means "infer".
29
+ Config = Struct.new(:changelog, :version_file, :require_path, :test_command,
30
+ :changelog_writer, keyword_init: true)
31
+
32
+ def self.detect(dir = Dir.pwd, config: GemKit::Release.config)
33
+ gemspec = Dir[File.join(dir, "*.gemspec")].sort.first
34
+ raise NotFound, "no .gemspec in #{dir}" if gemspec.nil?
35
+
36
+ new(gemspec, config: config)
37
+ end
38
+
39
+ attr_reader :root, :gemspec_path, :config
40
+
41
+ def initialize(gemspec_path, config: GemKit::Release.config)
42
+ @gemspec_path = File.expand_path(gemspec_path)
43
+ @root = File.dirname(@gemspec_path)
44
+ @config = config
45
+ end
46
+
47
+ # The evaluated gemspec. Loaded from the project root, because a gemspec
48
+ # typically require_relative's its own version file.
49
+ def spec
50
+ @spec ||= Dir.chdir(root) { Gem::Specification.load(gemspec_path) } or
51
+ raise NotFound, "could not load #{gemspec_path}"
52
+ end
53
+
54
+ def name = spec.name
55
+
56
+ def version = Gem::Version.new(spec.version.to_s)
57
+
58
+ # The next major version — the default deadline for a deprecation.
59
+ def next_major_version
60
+ Gem::Version.new(version.segments.first.to_s).bump.to_s
61
+ end
62
+
63
+ def changelog_path
64
+ File.expand_path(config.changelog || "CHANGELOG.md", root)
65
+ end
66
+
67
+ # lib/gem_kit/release/version.rb for "gem_kit-release", lib/brute/version.rb
68
+ # for "brute" — the convention every `bundle gem` project follows.
69
+ def version_file
70
+ File.expand_path(config.version_file || File.join("lib", *name.split("-"), "version.rb"), root)
71
+ end
72
+
73
+ # An ERB template beside the version file, if the project generates it.
74
+ def version_template
75
+ candidate = "#{version_file}.erb"
76
+ candidate if File.exist?(candidate)
77
+ end
78
+
79
+ # What to require so that deprecation declarations register themselves.
80
+ # "gem_kit-release" -> "gem_kit/release".
81
+ def require_path
82
+ config.require_path || name.tr("-", "/")
83
+ end
84
+
85
+ # Load the library, so Deprecate's registry reflects this project.
86
+ def load!
87
+ $LOAD_PATH.unshift(File.join(root, "lib")) unless $LOAD_PATH.include?(File.join(root, "lib"))
88
+ require require_path
89
+ true
90
+ rescue LoadError => error
91
+ warn "gem_kit-release: could not require #{require_path.inspect} (#{error.message});" \
92
+ " deprecations will not be detected"
93
+ false
94
+ end
95
+
96
+ def test_command = config.test_command || "bin/test"
97
+
98
+ def changelog_writer = config.changelog_writer || "claude"
99
+ end
100
+ end
101
+ end
102
+
103
+ __END__
104
+
105
+ describe "gem_kit/release/project" do
106
+ require "tmpdir"
107
+
108
+ # A throwaway gem laid out the conventional way.
109
+ with_project = lambda do |name: "demo", version: "1.2.3", &block|
110
+ Dir.mktmpdir do |dir|
111
+ path = name.split("-")
112
+ FileUtils.mkdir_p(File.join(dir, "lib", *path))
113
+ File.write(File.join(dir, "lib", *path, "version.rb"), <<~RUBY)
114
+ module #{path.map { |p| p.split("_").map(&:capitalize).join }.join("::")}
115
+ VERSION = "#{version}"
116
+ end
117
+ RUBY
118
+ File.write(File.join(dir, "#{name}.gemspec"), <<~RUBY)
119
+ require_relative "lib/#{path.join("/")}/version"
120
+ Gem::Specification.new do |spec|
121
+ spec.name = "#{name}"
122
+ spec.version = "#{version}"
123
+ spec.authors = ["x"]
124
+ spec.summary = "x"
125
+ spec.files = []
126
+ end
127
+ RUBY
128
+ block.call(dir)
129
+ end
130
+ end
131
+
132
+ it "detects the gemspec and reads name and version from it" do
133
+ with_project.call do |dir|
134
+ project = GemKit::Release::Project.detect(dir)
135
+ project.name.should == "demo"
136
+ project.version.should == Gem::Version.new("1.2.3")
137
+ end
138
+ end
139
+
140
+ it "raises when there is no gemspec" do
141
+ Dir.mktmpdir do |dir|
142
+ lambda { GemKit::Release::Project.detect(dir) }.should.raise(GemKit::Release::Project::NotFound)
143
+ end
144
+ end
145
+
146
+ it "infers the version file from the gem name, hyphens as directories" do
147
+ with_project.call(name: "gem_kit-release") do |dir|
148
+ GemKit::Release::Project.detect(dir).version_file
149
+ .should == File.join(dir, "lib/gem_kit/release/version.rb")
150
+ end
151
+ end
152
+
153
+ it "infers the require path from the gem name" do
154
+ with_project.call(name: "gem_kit-release") do |dir|
155
+ GemKit::Release::Project.detect(dir).require_path.should == "gem_kit/release"
156
+ end
157
+ end
158
+
159
+ it "defaults the changelog to CHANGELOG.md in the project root" do
160
+ with_project.call do |dir|
161
+ GemKit::Release::Project.detect(dir).changelog_path.should == File.join(dir, "CHANGELOG.md")
162
+ end
163
+ end
164
+
165
+ it "bumps to the next major for the default deprecation deadline" do
166
+ with_project.call(version: "4.1.0") do |dir|
167
+ GemKit::Release::Project.detect(dir).next_major_version.should == "5"
168
+ end
169
+ end
170
+
171
+ it "finds an ERB version template when the project generates its version file" do
172
+ with_project.call do |dir|
173
+ GemKit::Release::Project.detect(dir).version_template.should.be.nil
174
+ File.write(File.join(dir, "lib/demo/version.rb.erb"), "x")
175
+ GemKit::Release::Project.detect(dir).version_template
176
+ .should == File.join(dir, "lib/demo/version.rb.erb")
177
+ end
178
+ end
179
+
180
+ it "takes overrides from the config" do
181
+ with_project.call do |dir|
182
+ config = GemKit::Release::Project::Config.new(changelog: "HISTORY.md", test_command: "rake")
183
+ project = GemKit::Release::Project.detect(dir, config: config)
184
+ project.changelog_path.should == File.join(dir, "HISTORY.md")
185
+ project.test_command.should == "rake"
186
+ end
187
+ end
188
+ end