kettle-dev 2.4.5 → 2.5.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +146 -1
- data/README.md +115 -1
- data/exe/kettle-changelog +4 -2
- data/exe/kettle-release +40 -2
- data/exe/kettle-reset +35 -0
- data/lib/kettle/dev/changelog_cli.rb +73 -34
- data/lib/kettle/dev/ci_monitor.rb +0 -2
- data/lib/kettle/dev/dvcs_cli.rb +3 -2
- data/lib/kettle/dev/executable_version.rb +15 -1
- data/lib/kettle/dev/git_adapter.rb +126 -18
- data/lib/kettle/dev/interactive_release_command.rb +125 -0
- data/lib/kettle/dev/lockfile_reset.rb +442 -0
- data/lib/kettle/dev/rakelib/spec_test.rake +8 -2
- data/lib/kettle/dev/rakelib/yard.rake +11 -0
- data/lib/kettle/dev/release_cli.rb +203 -149
- data/lib/kettle/dev/release_secrets.rb +153 -0
- data/lib/kettle/dev/reset_cli.rb +81 -0
- data/lib/kettle/dev/{gem_coop_versions.rb → ruby_gems_versions.rb} +6 -6
- data/lib/kettle/dev/version.rb +4 -1
- data/lib/kettle/dev.rb +11 -1
- data.tar.gz.sig +0 -0
- metadata +37 -11
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "json"
|
|
7
|
+
require "net/http"
|
|
8
|
+
require "uri"
|
|
9
|
+
require "bundler"
|
|
10
|
+
|
|
11
|
+
module Kettle
|
|
12
|
+
module Dev
|
|
13
|
+
class LockfileReset
|
|
14
|
+
DEFAULT_DISABLED_ENV = {
|
|
15
|
+
"KETTLE_DEV_DEV" => "false",
|
|
16
|
+
"K_JEM_TEMPLATING" => "false",
|
|
17
|
+
"STRUCTUREDMERGE_DEV" => "false",
|
|
18
|
+
"TREE_SITTER_LANGUAGE_PACK_DEV" => "false",
|
|
19
|
+
"RUBOCOP_LTS_LOCAL" => "false",
|
|
20
|
+
"GALTZO_FLOSS_DEV" => "false",
|
|
21
|
+
"UR_BRAIN_DEV" => "false"
|
|
22
|
+
}.freeze
|
|
23
|
+
RELEASE_LOCKFILES_TARGET = "release-lockfiles"
|
|
24
|
+
SUPPORTED_TARGETS = ["Gemfile.lock", "Appraisal.root.gemfile.lock", RELEASE_LOCKFILES_TARGET].freeze
|
|
25
|
+
UNBUNDLED_ENV_KEYS = %w[
|
|
26
|
+
BUNDLE_BIN_PATH
|
|
27
|
+
BUNDLE_FROZEN
|
|
28
|
+
BUNDLER_VERSION
|
|
29
|
+
RUBYOPT
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
def initialize(root:, command_runner:)
|
|
33
|
+
@root = root
|
|
34
|
+
@command_runner = command_runner
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset(target)
|
|
38
|
+
paths = lockfile_paths_for(target)
|
|
39
|
+
force_full_update = release_lockfiles_target?(target)
|
|
40
|
+
uninstall_unreleased_local_gems(paths) if force_full_update
|
|
41
|
+
paths.each do |path|
|
|
42
|
+
reset_lockfile!(path, full_update: force_full_update) if force_full_update || normalization_needed?(path)
|
|
43
|
+
end
|
|
44
|
+
if force_full_update && uninstall_unreleased_local_gems(paths)
|
|
45
|
+
paths.each do |path|
|
|
46
|
+
reset_lockfile!(path, full_update: true)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
diagnostics = paths.flat_map { |path| diagnostics(path) }
|
|
50
|
+
raise Error, validation_message(target, diagnostics) unless diagnostics.empty?
|
|
51
|
+
|
|
52
|
+
paths
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reset_lockfile!(path, full_update: false)
|
|
56
|
+
gemfile = gemfile_for_lockfile(path)
|
|
57
|
+
unless gemfile && File.file?(gemfile)
|
|
58
|
+
warn("Cannot reset #{display_path(path)} because its Gemfile was not found.")
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
command = reset_command(path: path, gemfile: gemfile, full_update: full_update)
|
|
63
|
+
if full_update || has_local_path_remote?(path)
|
|
64
|
+
rebuild_lockfile(path) { command_runner.call(command) }
|
|
65
|
+
else
|
|
66
|
+
command_runner.call(command)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reset_command(path:, gemfile:, full_update: false)
|
|
71
|
+
update_gems = (full_update || has_local_path_remote?(path)) ? [] : reset_update_gems(path)
|
|
72
|
+
env = normalization_env.merge(
|
|
73
|
+
"BUNDLE_GEMFILE" => gemfile,
|
|
74
|
+
"BUNDLE_LOCKFILE" => path
|
|
75
|
+
)
|
|
76
|
+
command = +"env"
|
|
77
|
+
UNBUNDLED_ENV_KEYS.each do |key|
|
|
78
|
+
command << " -u #{key}"
|
|
79
|
+
end
|
|
80
|
+
env.each do |key, value|
|
|
81
|
+
command << " #{key}=#{Shellwords.escape(value)}"
|
|
82
|
+
end
|
|
83
|
+
command << " bundle lock"
|
|
84
|
+
command << " --update"
|
|
85
|
+
command << " #{update_gems.map { |gem_name| Shellwords.escape(gem_name) }.join(" ")}" unless update_gems.empty?
|
|
86
|
+
command << " --add-checksums"
|
|
87
|
+
command
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reset_update_gems(path)
|
|
91
|
+
(
|
|
92
|
+
path_source_gems(path).to_a |
|
|
93
|
+
path_dependency_gems(path).to_a |
|
|
94
|
+
empty_registry_checksums(path).map(&:first)
|
|
95
|
+
).uniq.sort
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def normalization_needed?(path)
|
|
99
|
+
has_local_path_remote?(path) || !empty_registry_checksums(path).empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def diagnostics(path)
|
|
103
|
+
diagnostics = []
|
|
104
|
+
local_path_remote_lines(path).each do |line_number|
|
|
105
|
+
diagnostics << "#{display_path(path)} has local path remote at line #{line_number}"
|
|
106
|
+
end
|
|
107
|
+
empty_registry_checksums(path).each do |name, version, line_number|
|
|
108
|
+
diagnostics << "#{display_path(path)} CHECKSUMS has no sha256 for #{name} #{version} at line #{line_number}"
|
|
109
|
+
end
|
|
110
|
+
diagnostics
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def lockfile_paths
|
|
114
|
+
candidates = [
|
|
115
|
+
File.join(root, "Gemfile.lock"),
|
|
116
|
+
File.join(root, "Appraisal.root.gemfile.lock")
|
|
117
|
+
]
|
|
118
|
+
candidates.select { |path| File.file?(path) }.sort
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def lockfile_path_for(target)
|
|
122
|
+
paths = lockfile_paths_for(target)
|
|
123
|
+
raise Error, "reset target #{target.inspect} resolves to multiple lockfiles; use lockfile_paths_for" if paths.length != 1
|
|
124
|
+
|
|
125
|
+
paths.first
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def lockfile_paths_for(target)
|
|
129
|
+
normalized = target.to_s.strip
|
|
130
|
+
raise Error, "reset requires TARGET" if normalized.empty?
|
|
131
|
+
if release_lockfiles_target?(normalized)
|
|
132
|
+
paths = lockfile_paths
|
|
133
|
+
raise Error, "no release lockfiles found" if paths.empty?
|
|
134
|
+
|
|
135
|
+
return paths
|
|
136
|
+
end
|
|
137
|
+
supported = SUPPORTED_TARGETS.reject { |candidate| candidate == RELEASE_LOCKFILES_TARGET }
|
|
138
|
+
match = supported.find { |candidate| candidate.casecmp(normalized).zero? }
|
|
139
|
+
unless match
|
|
140
|
+
raise Error, "reset target #{normalized.inspect} is not supported; supported targets: #{SUPPORTED_TARGETS.join(", ")}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
[File.join(root, match)]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def release_lockfiles_target?(target)
|
|
147
|
+
target.to_s.strip.casecmp(RELEASE_LOCKFILES_TARGET).zero?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def gemfile_for_lockfile(path)
|
|
151
|
+
basename = File.basename(path)
|
|
152
|
+
return File.join(root, "Gemfile") if basename == "Gemfile.lock"
|
|
153
|
+
|
|
154
|
+
path.delete_suffix(".lock")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def normalization_env
|
|
158
|
+
DEFAULT_DISABLED_ENV.merge(dynamic_local_path_env)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def dynamic_local_path_env
|
|
162
|
+
ENV.each_with_object({}) do |(key, value), env|
|
|
163
|
+
next unless key.end_with?("_DEV", "_LOCAL")
|
|
164
|
+
next unless local_path_env_value?(value)
|
|
165
|
+
|
|
166
|
+
env[key] = "false"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def local_path_env_value?(value)
|
|
171
|
+
text = value.to_s.strip
|
|
172
|
+
return false if text.empty? || text.casecmp("false").zero?
|
|
173
|
+
return true if text.start_with?("/", "./", "../", "~")
|
|
174
|
+
|
|
175
|
+
text.include?(File::SEPARATOR)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def has_local_path_remote?(path)
|
|
179
|
+
!local_path_remote_lines(path).empty?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def local_path_remote_lines(path)
|
|
183
|
+
in_path = false
|
|
184
|
+
File.readlines(path).filter_map.with_index(1) do |line, index|
|
|
185
|
+
stripped = line.strip
|
|
186
|
+
if stripped.match?(/\A[A-Z][A-Z ]*\z/)
|
|
187
|
+
in_path = stripped == "PATH"
|
|
188
|
+
next
|
|
189
|
+
end
|
|
190
|
+
next unless in_path
|
|
191
|
+
next unless stripped.start_with?("remote:")
|
|
192
|
+
next if stripped == "remote: ."
|
|
193
|
+
next unless stripped.start_with?("remote: /", "remote: ./", "remote: ../")
|
|
194
|
+
|
|
195
|
+
index
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def empty_registry_checksums(path)
|
|
200
|
+
path_gems = path_source_gems(path) | path_dependency_gems(path) | self_path_source_gems(path)
|
|
201
|
+
in_checksums = false
|
|
202
|
+
entries = File.readlines(path).filter_map.with_index(1) do |line, index|
|
|
203
|
+
stripped = line.strip
|
|
204
|
+
if stripped == "CHECKSUMS"
|
|
205
|
+
in_checksums = true
|
|
206
|
+
next
|
|
207
|
+
end
|
|
208
|
+
next unless in_checksums
|
|
209
|
+
next if stripped.empty?
|
|
210
|
+
next if stripped == "BUNDLED WITH"
|
|
211
|
+
|
|
212
|
+
match = stripped.match(/\A([A-Za-z0-9_.-]+) \(([^)]+)\)(?:\s+(sha256=.*))?\z/)
|
|
213
|
+
next unless match
|
|
214
|
+
|
|
215
|
+
name = match[1]
|
|
216
|
+
checksum = match[3].to_s
|
|
217
|
+
next unless checksum.empty?
|
|
218
|
+
next if path_gems.include?(name)
|
|
219
|
+
|
|
220
|
+
[name, match[2], index]
|
|
221
|
+
end
|
|
222
|
+
return [] unless has_any_sha_checksum?(path)
|
|
223
|
+
|
|
224
|
+
entries
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def has_any_sha_checksum?(path)
|
|
228
|
+
in_checksums = false
|
|
229
|
+
File.readlines(path).any? do |line|
|
|
230
|
+
stripped = line.strip
|
|
231
|
+
if stripped == "CHECKSUMS"
|
|
232
|
+
in_checksums = true
|
|
233
|
+
next false
|
|
234
|
+
end
|
|
235
|
+
next false unless in_checksums
|
|
236
|
+
|
|
237
|
+
stripped.include?("sha256=")
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def path_source_gems(path)
|
|
242
|
+
lockfile_parser(path).specs.each_with_object(Set.new) do |spec, gems|
|
|
243
|
+
source = spec.source
|
|
244
|
+
gems << spec.name if path_source?(source) && !self_path_source?(source)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def path_dependency_gems(path)
|
|
249
|
+
lockfile_parser(path).dependencies.each_value.each_with_object(Set.new) do |dependency, gems|
|
|
250
|
+
source = dependency.source
|
|
251
|
+
gems << dependency.name if path_source?(source) && !self_path_source?(source)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def self_path_source_gems(path)
|
|
256
|
+
lockfile_parser(path).specs.each_with_object(Set.new) do |spec, gems|
|
|
257
|
+
gems << spec.name if self_path_source?(spec.source)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def display_path(path)
|
|
262
|
+
Kettle::Dev.display_path(path)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
private
|
|
266
|
+
|
|
267
|
+
attr_reader :root, :command_runner
|
|
268
|
+
|
|
269
|
+
def lockfile_parser(path)
|
|
270
|
+
# Bundler validates checksum digest syntax while parsing, but this class
|
|
271
|
+
# only needs Bundler's source model here. Keep checksum diagnostics in
|
|
272
|
+
# the line scanner below so malformed or empty checksum entries can be
|
|
273
|
+
# reported without breaking source classification.
|
|
274
|
+
Bundler::LockfileParser.new(lockfile_source_content(path))
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def lockfile_source_content(path)
|
|
278
|
+
in_checksums = false
|
|
279
|
+
File.readlines(path).filter_map do |line|
|
|
280
|
+
stripped = line.strip
|
|
281
|
+
if stripped == "CHECKSUMS"
|
|
282
|
+
in_checksums = true
|
|
283
|
+
next
|
|
284
|
+
end
|
|
285
|
+
if in_checksums && stripped.match?(/\A[A-Z][A-Z ]*\z/)
|
|
286
|
+
in_checksums = false
|
|
287
|
+
end
|
|
288
|
+
next if in_checksums
|
|
289
|
+
|
|
290
|
+
line
|
|
291
|
+
end.join
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def path_source?(source)
|
|
295
|
+
source.instance_of?(::Bundler::Source::Path)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def self_path_source?(source)
|
|
299
|
+
path_source?(source) && source.path.to_s == "."
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def rebuild_lockfile(path)
|
|
303
|
+
backup = backup_path_for(path)
|
|
304
|
+
FileUtils.mkdir_p(File.dirname(backup))
|
|
305
|
+
FileUtils.cp(path, backup)
|
|
306
|
+
FileUtils.rm_f(path)
|
|
307
|
+
yield
|
|
308
|
+
FileUtils.mv(backup, path, force: true) unless File.file?(path)
|
|
309
|
+
FileUtils.rm_f(backup)
|
|
310
|
+
rescue
|
|
311
|
+
FileUtils.mv(backup, path, force: true) if File.file?(backup)
|
|
312
|
+
raise
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def backup_path_for(path)
|
|
316
|
+
backup_dir = File.join(root, "tmp", "kettle-reset")
|
|
317
|
+
"#{File.join(backup_dir, File.basename(path))}.#{Process.pid}.bak"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def uninstall_unreleased_local_gems(paths)
|
|
321
|
+
local_names = local_workspace_gem_names
|
|
322
|
+
return false if local_names.empty?
|
|
323
|
+
|
|
324
|
+
uninstalled = false
|
|
325
|
+
locked_registry_specs(paths).each do |name, version|
|
|
326
|
+
next unless local_names.include?(name)
|
|
327
|
+
next unless locally_installed?(name, version)
|
|
328
|
+
next if ruby_gems_version_available?(name, version)
|
|
329
|
+
|
|
330
|
+
uninstall_unreleased_local_gem(name, version)
|
|
331
|
+
uninstalled = true
|
|
332
|
+
end
|
|
333
|
+
uninstalled
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def uninstall_unreleased_local_gem(name, version)
|
|
337
|
+
command_runner.call(uninstall_command(name, version))
|
|
338
|
+
rescue => error
|
|
339
|
+
# Family release waves can ask several member processes to remove the same
|
|
340
|
+
# unreleased sibling version. If another process won that race, the reset
|
|
341
|
+
# goal is already satisfied; otherwise keep the original uninstall failure.
|
|
342
|
+
raise error if locally_installed?(name, version)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def locked_registry_specs(paths)
|
|
346
|
+
paths.flat_map { |path| registry_specs(path) }.uniq.sort
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def registry_specs(path)
|
|
350
|
+
specs = []
|
|
351
|
+
in_gem = false
|
|
352
|
+
in_specs = false
|
|
353
|
+
File.readlines(path).each do |line|
|
|
354
|
+
stripped = line.chomp
|
|
355
|
+
if stripped == "GEM"
|
|
356
|
+
in_gem = true
|
|
357
|
+
in_specs = false
|
|
358
|
+
next
|
|
359
|
+
end
|
|
360
|
+
next unless in_gem
|
|
361
|
+
break if !stripped.empty? && stripped == stripped.upcase && !stripped.start_with?(" ")
|
|
362
|
+
|
|
363
|
+
if stripped == " specs:"
|
|
364
|
+
in_specs = true
|
|
365
|
+
next
|
|
366
|
+
end
|
|
367
|
+
next unless in_specs
|
|
368
|
+
|
|
369
|
+
match = stripped.match(/\A ([A-Za-z0-9_.-]+) \(([^)]+)\)/)
|
|
370
|
+
specs << [match[1], match[2]] if match
|
|
371
|
+
end
|
|
372
|
+
specs
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def locally_installed?(name, version)
|
|
376
|
+
Gem::Specification.find_all_by_name(name, "= #{version}").any?
|
|
377
|
+
rescue
|
|
378
|
+
false
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def ruby_gems_version_available?(name, version)
|
|
382
|
+
version_text = version.to_s
|
|
383
|
+
platform = "ruby"
|
|
384
|
+
if version_text =~ /\A(.+)-([^-]+-[^-]+(?:-[^-]+)?)\z/
|
|
385
|
+
version_text = Regexp.last_match(1)
|
|
386
|
+
platform = Regexp.last_match(2)
|
|
387
|
+
end
|
|
388
|
+
ruby_gems_versions(name).any? do |entry|
|
|
389
|
+
entry.fetch("number", entry["version"]) == version_text &&
|
|
390
|
+
%W[ruby #{platform}].include?(entry.fetch("platform", "ruby").to_s)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def ruby_gems_versions(name)
|
|
395
|
+
@ruby_gems_versions ||= {}
|
|
396
|
+
@ruby_gems_versions.fetch(name) do
|
|
397
|
+
uri = URI("https://rubygems.org/api/v1/versions/#{URI.encode_www_form_component(name)}.json")
|
|
398
|
+
response = Net::HTTP.get_response(uri)
|
|
399
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
400
|
+
raise Error, "Could not verify released versions for #{name} from RubyGems.org (HTTP #{response.code})"
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
versions = JSON.parse(response.body)
|
|
404
|
+
@ruby_gems_versions[name] = versions
|
|
405
|
+
end
|
|
406
|
+
rescue JSON::ParserError => error
|
|
407
|
+
raise Error, "Could not parse released versions for #{name} from RubyGems.org: #{error.message}"
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def uninstall_command(name, version)
|
|
411
|
+
command = +"env"
|
|
412
|
+
UNBUNDLED_ENV_KEYS.each do |key|
|
|
413
|
+
command << " -u #{key}"
|
|
414
|
+
end
|
|
415
|
+
command << " gem uninstall #{Shellwords.escape(name)} -v #{Shellwords.escape(version)} -x -I"
|
|
416
|
+
command
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def local_workspace_gem_names
|
|
420
|
+
paths = dynamic_local_path_env.keys.filter_map do |key|
|
|
421
|
+
value = ENV[key].to_s.strip
|
|
422
|
+
value unless value.empty?
|
|
423
|
+
end
|
|
424
|
+
paths << File.expand_path("..", root)
|
|
425
|
+
paths.each_with_object(Set.new) do |path, names|
|
|
426
|
+
next unless File.directory?(path)
|
|
427
|
+
|
|
428
|
+
Dir[File.join(path, "*", "*.gemspec")].each do |gemspec|
|
|
429
|
+
names << File.basename(gemspec, ".gemspec")
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def validation_message(target, diagnostics)
|
|
435
|
+
<<~MSG
|
|
436
|
+
Reset #{target} failed validation:
|
|
437
|
+
#{diagnostics.map { |diagnostic| " - #{diagnostic}" }.join("\n")}
|
|
438
|
+
MSG
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
@@ -7,12 +7,13 @@ minitest_files = FileList["test/*test*.rb", "test/**/*test*.rb"].to_a.uniq
|
|
|
7
7
|
rspec_files = FileList["spec/*_spec.rb", "spec/**/*_spec.rb"].to_a.uniq
|
|
8
8
|
minitest_project = !minitest_files.empty?
|
|
9
9
|
rspec_project = !rspec_files.empty?
|
|
10
|
+
skip_test_tasks = Kettle::Dev.skip_test_tasks?
|
|
10
11
|
|
|
11
12
|
# Set up MiniTest
|
|
12
13
|
begin
|
|
13
14
|
require "rake/testtask"
|
|
14
15
|
|
|
15
|
-
if minitest_project && !Rake::Task.task_defined?(:test)
|
|
16
|
+
if minitest_project && !Rake::Task.task_defined?(:test) && !skip_test_tasks
|
|
16
17
|
Rake::TestTask.new(:test) do |t|
|
|
17
18
|
t.libs << "test"
|
|
18
19
|
t.test_files = minitest_files
|
|
@@ -36,6 +37,11 @@ setup_spec_task = ->(default:) {
|
|
|
36
37
|
unless Rake::Task.task_defined?(:spec)
|
|
37
38
|
desc("Run RSpec code examples")
|
|
38
39
|
task(:spec) do
|
|
40
|
+
if Kettle::Dev.skip_test_tasks?
|
|
41
|
+
puts "Skipping specs because KETTLE_DEV_SKIP_TESTS=true."
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
|
|
39
45
|
begin
|
|
40
46
|
skip_bundle_audit = ENV.delete("KETTLE_DEV_SKIP_BUNDLE_AUDIT")
|
|
41
47
|
sh("bundle", "exec", "kettle-test")
|
|
@@ -56,7 +62,7 @@ setup_spec_task = ->(default:) {
|
|
|
56
62
|
|
|
57
63
|
# Setup RSpec only when this project has specs, or when no MiniTest suite exists
|
|
58
64
|
# and the historical kettle-test/RSpec default remains the best available task.
|
|
59
|
-
if rspec_project || !minitest_project
|
|
65
|
+
if !skip_test_tasks && (rspec_project || !minitest_project)
|
|
60
66
|
if defined?(Kettle::Dev::IS_CI)
|
|
61
67
|
if Kettle::Dev::IS_CI
|
|
62
68
|
# then we should not have a coverage task, but do want a spec test.
|
|
@@ -25,6 +25,17 @@ begin
|
|
|
25
25
|
# yard-timekeeper not available - that's fine
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
namespace :yard do
|
|
29
|
+
desc "Lint YARD Documentation"
|
|
30
|
+
task :lint do
|
|
31
|
+
# Keep warning-only lint runs compact in default/release flows, but rerun
|
|
32
|
+
# with full output when lint fails so the blocking diagnostics are visible.
|
|
33
|
+
sh("bundle", "exec", "yard-lint", "lib") unless system("bundle", "exec", "yard-lint", "--quiet", "lib")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
task yard: "yard:lint"
|
|
38
|
+
Kettle::Dev.register_default("yard:lint")
|
|
28
39
|
Kettle::Dev.register_default("yard")
|
|
29
40
|
rescue LoadError
|
|
30
41
|
warn("[kettle-dev][yard.rake] failed to load yard") if Kettle::Dev::DEBUGGING
|