kettle-dev 2.4.1 → 2.4.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +51 -1
- data/README.md +1 -1
- data/exe/kettle-release +50 -3
- data/lib/kettle/dev/release_cli.rb +362 -9
- data/lib/kettle/dev/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +24 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0764aba0c8669f0c075868db0109a393fb508835db1540ac2b1f3cdf0c1b4cd0
|
|
4
|
+
data.tar.gz: a5d418a258af62045142025f314820330eaa18dba697036e2c4861bbb3874fad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4edf261a1f8819f89890b26eb1eb3666e388e771dde67bee0aa70d97d7920a7719493c0786267a6e651744c0a9c62462efa690105d0f7e124b612f386da9d09
|
|
7
|
+
data.tar.gz: 32423487dc0b9ea6af5d22695785f7d2ba2620b8128d6629f922f93433090d71600c4105ca5a1eecb96640c55699405edb78fea174e8bac46ef3eee1439e36cd
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,52 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [2.4.3] - 2026-07-25
|
|
34
|
+
|
|
35
|
+
- TAG: [v2.4.3][2.4.3t]
|
|
36
|
+
- COVERAGE: 92.17% -- 4082/4429 lines in 38 files
|
|
37
|
+
- BRANCH COVERAGE: 76.38% -- 1594/2087 branches in 38 files
|
|
38
|
+
- 59.16% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `kettle-release` now supports `--events[=TYPES]`, `--json`, and
|
|
43
|
+
`--report PATH` machine-readable release reporting backed by the shared
|
|
44
|
+
`kettle-ndjson` event toolkit.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- kettle-jem-template-20260725-001 - Generated JRuby and TruffleRuby workflow
|
|
49
|
+
files now run when pull request head branches start with `feature/release`,
|
|
50
|
+
so release CI monitoring does not report intentionally skipped engine
|
|
51
|
+
workflows as failures.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- `kettle-release` now retries transient git remote fetch failures during
|
|
56
|
+
strict pre-push trunk parity checks, and failures now include an explicit
|
|
57
|
+
`--skip-remotes` recovery hint for intentionally unavailable remotes.
|
|
58
|
+
- `kettle-release` now waits briefly and retries post-publish gem.coop
|
|
59
|
+
availability validation before failing, so normal registry mirror lag does
|
|
60
|
+
not abort immediately after a successful RubyGems push.
|
|
61
|
+
|
|
62
|
+
- Raised the `kettle-ndjson` runtime dependency floor to v0.1.1 so the
|
|
63
|
+
dependency remains installable on kettle-dev's Ruby 2.4+ support floor.
|
|
64
|
+
|
|
65
|
+
## [2.4.2] - 2026-07-24
|
|
66
|
+
|
|
67
|
+
- TAG: [v2.4.2][2.4.2t]
|
|
68
|
+
- COVERAGE: 92.36% -- 3991/4321 lines in 38 files
|
|
69
|
+
- BRANCH COVERAGE: 76.06% -- 1563/2055 branches in 38 files
|
|
70
|
+
- 59.00% documented
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- `kettle-release` now validates published gem availability with an isolated
|
|
75
|
+
Bundler probe against gem.coop before continuing past the publish step, and
|
|
76
|
+
uninstalls newly introduced local candidate gems when the release did not
|
|
77
|
+
happen or cannot be validated.
|
|
78
|
+
|
|
33
79
|
## [2.4.1] - 2026-07-24
|
|
34
80
|
|
|
35
81
|
- TAG: [v2.4.1][2.4.1t]
|
|
@@ -2660,7 +2706,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
2660
2706
|
- Selecting will run the selected workflow via `act`
|
|
2661
2707
|
- This may move to its own gem in the future.
|
|
2662
2708
|
|
|
2663
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.
|
|
2709
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.3...HEAD
|
|
2710
|
+
[2.4.3]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.2...v2.4.3
|
|
2711
|
+
[2.4.3t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.4.3
|
|
2712
|
+
[2.4.2]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.1...v2.4.2
|
|
2713
|
+
[2.4.2t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.4.2
|
|
2664
2714
|
[2.4.1]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.0...v2.4.1
|
|
2665
2715
|
[2.4.1t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.4.1
|
|
2666
2716
|
[2.4.0]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.11...v2.4.0
|
data/README.md
CHANGED
|
@@ -891,7 +891,7 @@ Thanks for RTFM. ☺️
|
|
|
891
891
|
[📌gitmoji]: https://gitmoji.dev
|
|
892
892
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
893
893
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
894
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.
|
|
894
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.429-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
895
895
|
[🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
|
|
896
896
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
897
897
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/exe/kettle-release
CHANGED
|
@@ -35,8 +35,10 @@ $LOAD_PATH.unshift(repo_lib) if File.directory?(repo_lib) && !$LOAD_PATH.include
|
|
|
35
35
|
begin
|
|
36
36
|
require "kettle/dev"
|
|
37
37
|
require "kettle/dev/executable_version"
|
|
38
|
+
require "kettle/dev/release_cli"
|
|
38
39
|
Kettle::Dev::ExecutableVersion.print_and_exit!(script_basename, ARGV, value_option: true)
|
|
39
|
-
|
|
40
|
+
machine_output = ARGV.any? { |arg| arg == "--json" || arg.start_with?("--events") }
|
|
41
|
+
Kettle::Dev::ExecutableVersion.print_header(script_basename) unless machine_output
|
|
40
42
|
rescue LoadError => e
|
|
41
43
|
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
42
44
|
warn("Hint: Ensure the host project has kettle-dev as a dependency and run bundle install.")
|
|
@@ -47,7 +49,7 @@ end
|
|
|
47
49
|
# Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
|
|
48
50
|
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
49
51
|
puts <<~USAGE
|
|
50
|
-
Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [--ci-workflows WORKFLOWS] [--skip-remotes REMOTES] [start_step=<0-19>]
|
|
52
|
+
Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [--ci-workflows WORKFLOWS] [--skip-remotes REMOTES] [--json|--events[=TYPES]] [--report PATH] [start_step=<0-19>]
|
|
51
53
|
|
|
52
54
|
Automates the release flow for a Ruby gem in the host project.
|
|
53
55
|
|
|
@@ -87,6 +89,9 @@ if ARGV.include?("-h") || ARGV.include?("--help")
|
|
|
87
89
|
--skip-bundle-audit # Skip bundle:audit/update during release rake checks
|
|
88
90
|
--ci-workflows WORKFLOWS # Comma-separated workflow files or stems to monitor at CI step 10
|
|
89
91
|
--skip-remotes REMOTES # Comma-separated git remote names to exclude from release fetch/push parity
|
|
92
|
+
--json # Print final machine-readable release report as JSON
|
|
93
|
+
--events[=TYPES] # Print newline-delimited JSON release events, optionally filtered by type
|
|
94
|
+
--report PATH # Write final machine-readable release report as JSON
|
|
90
95
|
--local-ci # Sensitive release mode: run act locally, publish before any git push,
|
|
91
96
|
# create the git tag locally, then push commits and tags after publish
|
|
92
97
|
|
|
@@ -195,18 +200,60 @@ def extract_skip_remotes_arg!(argv)
|
|
|
195
200
|
remotes
|
|
196
201
|
end
|
|
197
202
|
|
|
203
|
+
def extract_report_arg!(argv)
|
|
204
|
+
report_path = nil
|
|
205
|
+
if (idx = argv.index("--report"))
|
|
206
|
+
report_path = argv[idx + 1]
|
|
207
|
+
if report_path.to_s.empty? || report_path.to_s.start_with?("-")
|
|
208
|
+
Kettle::Dev::ExitAdapter.abort("--report requires a path")
|
|
209
|
+
end
|
|
210
|
+
argv.slice!(idx, 2)
|
|
211
|
+
end
|
|
212
|
+
argv.delete_if do |arg|
|
|
213
|
+
if arg.start_with?("--report=", "report=")
|
|
214
|
+
report_path = arg.split("=", 2)[1]
|
|
215
|
+
Kettle::Dev::ExitAdapter.abort("--report requires a path") if report_path.to_s.empty?
|
|
216
|
+
true
|
|
217
|
+
else
|
|
218
|
+
false
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
report_path
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def extract_events_arg!(argv)
|
|
225
|
+
events = false
|
|
226
|
+
event_types = nil
|
|
227
|
+
argv.delete_if do |arg|
|
|
228
|
+
if arg == "--events"
|
|
229
|
+
events = true
|
|
230
|
+
elsif arg.start_with?("--events=")
|
|
231
|
+
events = true
|
|
232
|
+
event_types = arg.split("=", 2)[1]
|
|
233
|
+
else
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
[events, event_types]
|
|
238
|
+
end
|
|
239
|
+
|
|
198
240
|
local_ci = ARGV.include?("--local-ci")
|
|
199
241
|
appraisal_task = ARGV.delete("--appraisal-update") ? "appraisal:update" : nil
|
|
200
242
|
skip_bundle_audit = !!ARGV.delete("--skip-bundle-audit")
|
|
243
|
+
json_output = !!ARGV.delete("--json")
|
|
244
|
+
events_output, event_types = extract_events_arg!(ARGV)
|
|
245
|
+
Kettle::Dev::ExitAdapter.abort("--events cannot be combined with --json") if events_output && json_output
|
|
201
246
|
start_step_arg = ARGV.find { |a| a.start_with?("start_step=") }
|
|
202
247
|
start_step = start_step_arg ? start_step_arg.split("=", 2)[1].to_i : 0
|
|
203
248
|
version_override = extract_version_arg!(ARGV)
|
|
204
249
|
skip_steps = extract_skip_steps_arg!(ARGV)
|
|
205
250
|
ci_workflows = extract_ci_workflows_arg!(ARGV)
|
|
206
251
|
skip_remotes = extract_skip_remotes_arg!(ARGV)
|
|
252
|
+
report_path = extract_report_arg!(ARGV)
|
|
207
253
|
|
|
208
254
|
begin
|
|
209
|
-
Kettle::
|
|
255
|
+
event_stream = events_output ? Kettle::Ndjson.event_stream($stdout, types: event_types) : nil
|
|
256
|
+
Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task, skip_steps: skip_steps, skip_bundle_audit: skip_bundle_audit, ci_workflows: ci_workflows, skip_remotes: skip_remotes, event_stream: event_stream, json_output: json_output, json_io: $stdout, report_path: report_path).run
|
|
210
257
|
rescue LoadError => e
|
|
211
258
|
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
212
259
|
warn(Array(e.backtrace).join("\n")) if ENV["DEBUG"]
|
|
@@ -13,6 +13,7 @@ require "yaml"
|
|
|
13
13
|
require "set"
|
|
14
14
|
|
|
15
15
|
# External gems
|
|
16
|
+
require "kettle/ndjson"
|
|
16
17
|
require "kettle/rb/compat_matrix"
|
|
17
18
|
require "ruby-progressbar"
|
|
18
19
|
|
|
@@ -40,6 +41,8 @@ module Kettle
|
|
|
40
41
|
"BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES" => "true"
|
|
41
42
|
}.freeze
|
|
42
43
|
DEBUG_TRUE_VALUES = %w[1 true yes on].freeze
|
|
44
|
+
RELEASE_VALIDATION_SOURCE = "https://gem.coop"
|
|
45
|
+
|
|
43
46
|
class << self
|
|
44
47
|
def run_cmd!(cmd)
|
|
45
48
|
# For Bundler-invoked build/release, explicitly prefix SKIP_GEM_SIGNING so
|
|
@@ -121,14 +124,44 @@ module Kettle
|
|
|
121
124
|
@skip_bundle_audit = truthy_value?(skip_bundle_audit) || truthy_value?(ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"])
|
|
122
125
|
@version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
|
|
123
126
|
@appraisal_task = normalize_appraisal_task(appraisal_task || ENV["KETTLE_RELEASE_APPRAISAL_TASK"])
|
|
127
|
+
@release_candidate = nil
|
|
128
|
+
@event_stream = options[:event_stream]
|
|
129
|
+
@event_recorder = Kettle::Ndjson.event_recorder(@event_stream, phase_timings: [])
|
|
130
|
+
@report_path = options[:report_path]
|
|
131
|
+
@json_output = !!options[:json_output]
|
|
132
|
+
@json_io = options[:json_io] || $stdout
|
|
133
|
+
@command_events = []
|
|
134
|
+
@diagnostics = []
|
|
135
|
+
@started_at = nil
|
|
136
|
+
@finished_report = nil
|
|
124
137
|
end
|
|
125
138
|
|
|
126
139
|
def run
|
|
140
|
+
@started_at = monotonic_time
|
|
141
|
+
emit_run_start
|
|
142
|
+
status = "ok"
|
|
143
|
+
error = nil
|
|
127
144
|
with_bundle_audit_skip_env do
|
|
128
|
-
|
|
145
|
+
with_machine_stdout_redirect do
|
|
146
|
+
run_with_release_environment
|
|
147
|
+
end
|
|
129
148
|
end
|
|
149
|
+
rescue SystemExit => e
|
|
150
|
+
status = e.status.to_i.zero? ? "ok" : "failed"
|
|
151
|
+
error = e
|
|
152
|
+
record_diagnostic("release_exit", e.message, severity: (status == "ok") ? "info" : "error", blocking: status != "ok")
|
|
153
|
+
raise
|
|
154
|
+
rescue => e
|
|
155
|
+
status = "failed"
|
|
156
|
+
error = e
|
|
157
|
+
record_diagnostic("release_error", "#{e.class}: #{e.message}", severity: "error", blocking: true)
|
|
158
|
+
raise
|
|
159
|
+
ensure
|
|
160
|
+
finish_release_report(status: status, error: error)
|
|
130
161
|
end
|
|
131
162
|
|
|
163
|
+
attr_reader :finished_report
|
|
164
|
+
|
|
132
165
|
def run_with_release_environment
|
|
133
166
|
run_pre_release_checks! if run_step?(0)
|
|
134
167
|
|
|
@@ -346,13 +379,18 @@ module Kettle
|
|
|
346
379
|
|
|
347
380
|
# 15. release and tag
|
|
348
381
|
if run_step?(15)
|
|
382
|
+
version ||= detect_version
|
|
383
|
+
gem_name = detect_gem_name
|
|
384
|
+
@release_candidate = build_release_candidate(gem_name, version)
|
|
349
385
|
if local_ci?
|
|
350
|
-
version
|
|
351
|
-
release_gem_and_tag_locally!(version)
|
|
386
|
+
with_unpublished_candidate_cleanup { release_gem_and_tag_locally!(version) }
|
|
352
387
|
else
|
|
353
388
|
puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
|
|
354
|
-
|
|
355
|
-
|
|
389
|
+
with_unpublished_candidate_cleanup do
|
|
390
|
+
run_cmd!("bundle exec rake release")
|
|
391
|
+
confirm_release_candidate_available!(@release_candidate)
|
|
392
|
+
end
|
|
393
|
+
mark_gem_coop_release_cache_bust(version)
|
|
356
394
|
end
|
|
357
395
|
end
|
|
358
396
|
|
|
@@ -387,11 +425,11 @@ module Kettle
|
|
|
387
425
|
begin
|
|
388
426
|
version ||= detect_version
|
|
389
427
|
gem_name = detect_gem_name
|
|
390
|
-
puts "\n🚀 Release #{gem_name} v#{version} Complete 🚀"
|
|
428
|
+
human_output.puts "\n🚀 Release #{gem_name} v#{version} Complete 🚀"
|
|
391
429
|
rescue => e
|
|
392
430
|
Kettle::Dev.debug_error(e, __method__)
|
|
393
431
|
# Fallback if detection fails for any reason
|
|
394
|
-
puts "\n🚀 Release v#{version || "unknown"} Complete 🚀"
|
|
432
|
+
human_output.puts "\n🚀 Release v#{version || "unknown"} Complete 🚀"
|
|
395
433
|
end
|
|
396
434
|
end
|
|
397
435
|
|
|
@@ -999,9 +1037,19 @@ module Kettle
|
|
|
999
1037
|
|
|
1000
1038
|
def run_cmd!(cmd)
|
|
1001
1039
|
cmd = bundle_audit_skip_command(cmd)
|
|
1040
|
+
emit_command_event(cmd, "started")
|
|
1002
1041
|
with_bundle_audit_skip_env do
|
|
1003
|
-
|
|
1042
|
+
with_machine_stdout_redirect do
|
|
1043
|
+
self.class.run_cmd!(cmd)
|
|
1044
|
+
end
|
|
1004
1045
|
end
|
|
1046
|
+
emit_command_event(cmd, "ok")
|
|
1047
|
+
rescue SystemExit => e
|
|
1048
|
+
emit_command_event(cmd, "failed", reason: e.message)
|
|
1049
|
+
raise
|
|
1050
|
+
rescue => e
|
|
1051
|
+
emit_command_event(cmd, "failed", reason: "#{e.class}: #{e.message}")
|
|
1052
|
+
raise
|
|
1005
1053
|
end
|
|
1006
1054
|
|
|
1007
1055
|
def bundle_audit_skip_command(cmd)
|
|
@@ -1144,9 +1192,147 @@ module Kettle
|
|
|
1144
1192
|
|
|
1145
1193
|
puts "Publishing #{File.basename(gem_path)} to RubyGems without pushing git refs..."
|
|
1146
1194
|
run_cmd!("gem push #{Shellwords.escape(gem_path)}")
|
|
1195
|
+
confirm_release_candidate_available!(@release_candidate || build_release_candidate(gem_name_from_gem_path(gem_path, version), version))
|
|
1147
1196
|
mark_gem_coop_release_cache_bust(version, gem_name: gem_name_from_gem_path(gem_path, version))
|
|
1148
1197
|
end
|
|
1149
1198
|
|
|
1199
|
+
def build_release_candidate(gem_name, version)
|
|
1200
|
+
ReleaseCandidate.new(
|
|
1201
|
+
gem_name: gem_name.to_s,
|
|
1202
|
+
version: version.to_s,
|
|
1203
|
+
installed_before: gem_version_installed?(gem_name, version),
|
|
1204
|
+
published: false
|
|
1205
|
+
)
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def with_unpublished_candidate_cleanup
|
|
1209
|
+
yield
|
|
1210
|
+
rescue SystemExit, StandardError
|
|
1211
|
+
cleanup_unpublished_release_candidate(@release_candidate)
|
|
1212
|
+
raise
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
def cleanup_unpublished_release_candidate(candidate)
|
|
1216
|
+
return unless candidate
|
|
1217
|
+
return if candidate.published
|
|
1218
|
+
return if candidate.installed_before
|
|
1219
|
+
return unless gem_version_installed?(candidate.gem_name, candidate.version)
|
|
1220
|
+
|
|
1221
|
+
warn(
|
|
1222
|
+
"Release of #{candidate.gem_name} #{candidate.version} was not validated; " \
|
|
1223
|
+
"uninstalling the local candidate gem so future bundle updates cannot resolve an unreleased version."
|
|
1224
|
+
)
|
|
1225
|
+
uninstall_release_candidate(candidate)
|
|
1226
|
+
end
|
|
1227
|
+
|
|
1228
|
+
def uninstall_release_candidate(candidate)
|
|
1229
|
+
run_cmd!(
|
|
1230
|
+
"gem uninstall #{Shellwords.escape(candidate.gem_name)} " \
|
|
1231
|
+
"-v #{Shellwords.escape(candidate.version)} -x -I --ignore-dependencies"
|
|
1232
|
+
)
|
|
1233
|
+
rescue SystemExit, StandardError => error
|
|
1234
|
+
warn("Could not uninstall local candidate #{candidate.gem_name} #{candidate.version}: #{error.message}")
|
|
1235
|
+
warn("Manual cleanup: gem uninstall #{candidate.gem_name} -v #{candidate.version} -x -I --ignore-dependencies")
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
def gem_version_installed?(gem_name, version)
|
|
1239
|
+
Gem::Specification.reset
|
|
1240
|
+
Gem::Specification.find_all_by_name(gem_name.to_s, "= #{version}").any?
|
|
1241
|
+
rescue Gem::LoadError
|
|
1242
|
+
false
|
|
1243
|
+
end
|
|
1244
|
+
|
|
1245
|
+
def confirm_release_candidate_available!(candidate)
|
|
1246
|
+
run_release_availability_probe(candidate)
|
|
1247
|
+
candidate.published = true
|
|
1248
|
+
true
|
|
1249
|
+
rescue SystemExit
|
|
1250
|
+
raise
|
|
1251
|
+
rescue => error
|
|
1252
|
+
abort(<<~MSG)
|
|
1253
|
+
Published gem availability could not be validated for #{candidate.gem_name} #{candidate.version}: #{error.class}: #{error.message}
|
|
1254
|
+
kettle-release will clean up any local candidate install that was introduced by this failed release attempt.
|
|
1255
|
+
If the push actually succeeded, resolve the registry/checksum state manually before resuming.
|
|
1256
|
+
MSG
|
|
1257
|
+
end
|
|
1258
|
+
|
|
1259
|
+
def run_release_availability_probe(candidate)
|
|
1260
|
+
script_path = write_release_availability_probe(candidate)
|
|
1261
|
+
attempts = release_availability_probe_attempts
|
|
1262
|
+
last_stderr = nil
|
|
1263
|
+
last_status = nil
|
|
1264
|
+
sleep(release_availability_probe_initial_delay)
|
|
1265
|
+
attempts.times do |index|
|
|
1266
|
+
attempt = index + 1
|
|
1267
|
+
puts("Validating #{candidate.gem_name} #{candidate.version} from #{RELEASE_VALIDATION_SOURCE} (attempt #{attempt}/#{attempts})")
|
|
1268
|
+
stdout_str = nil
|
|
1269
|
+
stderr_str = nil
|
|
1270
|
+
status = nil
|
|
1271
|
+
with_unbundled_release_probe_env do
|
|
1272
|
+
stdout_str, stderr_str, status = Open3.capture3(self.class.send(:command_env), Gem.ruby, script_path)
|
|
1273
|
+
end
|
|
1274
|
+
$stdout.print(stdout_str) unless stdout_str.to_s.empty?
|
|
1275
|
+
return true if status.success?
|
|
1276
|
+
|
|
1277
|
+
last_stderr = stderr_str
|
|
1278
|
+
last_status = status
|
|
1279
|
+
sleep(release_availability_probe_interval) if attempt < attempts
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
diag = last_stderr.to_s.empty? ? "" : "\n--- STDERR ---\n#{last_stderr}".rstrip
|
|
1283
|
+
abort("Published gem availability probe failed for #{candidate.gem_name} #{candidate.version} after #{attempts} attempt(s) (exit #{last_status.exitstatus})#{diag}")
|
|
1284
|
+
ensure
|
|
1285
|
+
FileUtils.rm_f(script_path) if script_path
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
def release_availability_probe_attempts
|
|
1289
|
+
15
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
def release_availability_probe_initial_delay
|
|
1293
|
+
5
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
def release_availability_probe_interval
|
|
1297
|
+
10
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
def with_unbundled_release_probe_env(&block)
|
|
1301
|
+
if defined?(Bundler)
|
|
1302
|
+
Bundler.with_unbundled_env(&block)
|
|
1303
|
+
else
|
|
1304
|
+
yield
|
|
1305
|
+
end
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
def write_release_availability_probe(candidate)
|
|
1309
|
+
dir = File.join(@root, "tmp", "kettle-release")
|
|
1310
|
+
FileUtils.mkdir_p(dir)
|
|
1311
|
+
path = File.join(dir, "validate-#{candidate.gem_name}-#{candidate.version}-#{$$}.rb")
|
|
1312
|
+
File.write(path, release_availability_probe_script(candidate))
|
|
1313
|
+
path
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
def release_availability_probe_script(candidate)
|
|
1317
|
+
<<~RUBY
|
|
1318
|
+
# frozen_string_literal: true
|
|
1319
|
+
|
|
1320
|
+
gem_name = #{candidate.gem_name.dump}
|
|
1321
|
+
version = #{candidate.version.dump}
|
|
1322
|
+
|
|
1323
|
+
require "bundler/inline"
|
|
1324
|
+
|
|
1325
|
+
gemfile(true) do
|
|
1326
|
+
source #{RELEASE_VALIDATION_SOURCE.dump}
|
|
1327
|
+
gem gem_name, "= \#{version}"
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
spec = Gem.loaded_specs.fetch(gem_name)
|
|
1331
|
+
abort("loaded \#{gem_name} \#{spec.version}, expected \#{version}") unless spec.version.to_s == version
|
|
1332
|
+
puts "Validated \#{gem_name} \#{version} from #{RELEASE_VALIDATION_SOURCE}"
|
|
1333
|
+
RUBY
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1150
1336
|
def detect_version
|
|
1151
1337
|
Kettle::Dev::Versioning.detect_version(@root, override: @version_override)
|
|
1152
1338
|
end
|
|
@@ -1247,6 +1433,118 @@ module Kettle
|
|
|
1247
1433
|
end
|
|
1248
1434
|
end
|
|
1249
1435
|
|
|
1436
|
+
def emit_run_start
|
|
1437
|
+
Kettle::Ndjson.emit_event(
|
|
1438
|
+
@event_recorder,
|
|
1439
|
+
"run_start",
|
|
1440
|
+
command: "release",
|
|
1441
|
+
root: @root,
|
|
1442
|
+
start_step: @start_step,
|
|
1443
|
+
skipped_steps: @skip_steps,
|
|
1444
|
+
local_ci: @local_ci,
|
|
1445
|
+
ci_workflows: @ci_workflows,
|
|
1446
|
+
skipped_remotes: @skip_remotes
|
|
1447
|
+
)
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
def emit_command_event(command, status, reason: nil)
|
|
1451
|
+
event = {
|
|
1452
|
+
phase: "release",
|
|
1453
|
+
index: @command_events.length + 1,
|
|
1454
|
+
name: command_event_name(command),
|
|
1455
|
+
status: status,
|
|
1456
|
+
reason: reason,
|
|
1457
|
+
command: command,
|
|
1458
|
+
changed_files: []
|
|
1459
|
+
}
|
|
1460
|
+
@command_events << event unless status == "started"
|
|
1461
|
+
Kettle::Ndjson.emit_step_event(@event_recorder, "command_step", event, phase: "release", index: event[:index])
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
def command_event_name(command)
|
|
1465
|
+
command.to_s.split(/\s+/, 3).first(2).join("_").gsub(/[^A-Za-z0-9_]+/, "_").sub(/_+\z/, "")
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
def record_diagnostic(kind, message, severity:, blocking:)
|
|
1469
|
+
diagnostic = {
|
|
1470
|
+
kind: kind,
|
|
1471
|
+
severity: severity,
|
|
1472
|
+
message: message.to_s,
|
|
1473
|
+
blocking: blocking
|
|
1474
|
+
}
|
|
1475
|
+
@diagnostics << diagnostic
|
|
1476
|
+
Kettle::Ndjson.emit_diagnostic_event(@event_recorder, diagnostic, index: @diagnostics.length)
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1479
|
+
def finish_release_report(status:, error:)
|
|
1480
|
+
return if @finished_report
|
|
1481
|
+
|
|
1482
|
+
duration_ms = @started_at ? duration_ms_since(@started_at) : nil
|
|
1483
|
+
@finished_report = {
|
|
1484
|
+
command: "release",
|
|
1485
|
+
root: @root,
|
|
1486
|
+
version: safe_detect_version,
|
|
1487
|
+
gem_name: safe_detect_gem_name,
|
|
1488
|
+
start_step: @start_step,
|
|
1489
|
+
skipped_steps: @skip_steps,
|
|
1490
|
+
local_ci: @local_ci,
|
|
1491
|
+
ci_workflows: @ci_workflows,
|
|
1492
|
+
skipped_remotes: @skip_remotes,
|
|
1493
|
+
status: status,
|
|
1494
|
+
error_class: error&.class&.name,
|
|
1495
|
+
error_message: error&.message,
|
|
1496
|
+
duration_ms: duration_ms,
|
|
1497
|
+
command_events: @command_events,
|
|
1498
|
+
diagnostics: @diagnostics,
|
|
1499
|
+
diagnostics_count: @diagnostics.length,
|
|
1500
|
+
phase_timings: @event_recorder.phase_timings
|
|
1501
|
+
}.compact
|
|
1502
|
+
Kettle::Ndjson.emit_summary_event(@event_recorder, @finished_report)
|
|
1503
|
+
write_release_report(@finished_report)
|
|
1504
|
+
@json_io.puts(JSON.pretty_generate(@finished_report)) if @json_output
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1507
|
+
def write_release_report(report)
|
|
1508
|
+
return unless @report_path
|
|
1509
|
+
|
|
1510
|
+
FileUtils.mkdir_p(File.dirname(@report_path))
|
|
1511
|
+
File.write(@report_path, "#{JSON.pretty_generate(report)}\n")
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
def with_machine_stdout_redirect
|
|
1515
|
+
return yield unless @json_output || @event_stream
|
|
1516
|
+
|
|
1517
|
+
previous_stdout = $stdout
|
|
1518
|
+
$stdout = $stderr
|
|
1519
|
+
yield
|
|
1520
|
+
ensure
|
|
1521
|
+
$stdout = previous_stdout if previous_stdout
|
|
1522
|
+
end
|
|
1523
|
+
|
|
1524
|
+
def human_output
|
|
1525
|
+
(@json_output || @event_stream) ? $stderr : $stdout
|
|
1526
|
+
end
|
|
1527
|
+
|
|
1528
|
+
def safe_detect_version
|
|
1529
|
+
detect_version
|
|
1530
|
+
rescue
|
|
1531
|
+
nil
|
|
1532
|
+
end
|
|
1533
|
+
|
|
1534
|
+
def safe_detect_gem_name
|
|
1535
|
+
detect_gem_name
|
|
1536
|
+
rescue
|
|
1537
|
+
nil
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1540
|
+
def monotonic_time
|
|
1541
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
def duration_ms_since(started_at)
|
|
1545
|
+
((monotonic_time - started_at) * 1000).round(3)
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1250
1548
|
def push_tags!
|
|
1251
1549
|
# After release, push tags to remotes according to policy:
|
|
1252
1550
|
# 1) If a remote named "all" exists, push tags only to it.
|
|
@@ -1384,7 +1682,7 @@ module Kettle
|
|
|
1384
1682
|
remotes.each do |remote|
|
|
1385
1683
|
next if remote == "all"
|
|
1386
1684
|
|
|
1387
|
-
|
|
1685
|
+
fetch_remote_for_parity!(remote)
|
|
1388
1686
|
end
|
|
1389
1687
|
missing_from = []
|
|
1390
1688
|
remotes.each do |r|
|
|
@@ -1460,6 +1758,50 @@ module Kettle
|
|
|
1460
1758
|
end
|
|
1461
1759
|
end
|
|
1462
1760
|
|
|
1761
|
+
def fetch_remote_for_parity!(remote)
|
|
1762
|
+
command = "git fetch #{Shellwords.escape(remote)}"
|
|
1763
|
+
attempts = remote_fetch_parity_attempts
|
|
1764
|
+
last_error = nil
|
|
1765
|
+
attempts.times do |index|
|
|
1766
|
+
attempt = index + 1
|
|
1767
|
+
puts "Fetching remote '#{remote}' for release parity (attempt #{attempt}/#{attempts})"
|
|
1768
|
+
begin
|
|
1769
|
+
run_cmd!(command)
|
|
1770
|
+
return true
|
|
1771
|
+
rescue SystemExit => error
|
|
1772
|
+
last_error = error
|
|
1773
|
+
rescue => error
|
|
1774
|
+
last_error = error
|
|
1775
|
+
end
|
|
1776
|
+
sleep(remote_fetch_parity_interval) if attempt < attempts
|
|
1777
|
+
end
|
|
1778
|
+
|
|
1779
|
+
abort(remote_fetch_failure_message(remote, last_error&.message.to_s))
|
|
1780
|
+
end
|
|
1781
|
+
|
|
1782
|
+
def remote_fetch_parity_attempts
|
|
1783
|
+
3
|
|
1784
|
+
end
|
|
1785
|
+
|
|
1786
|
+
def remote_fetch_parity_interval
|
|
1787
|
+
5
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
def remote_fetch_failure_message(remote, detail)
|
|
1791
|
+
<<~MSG
|
|
1792
|
+
Unable to fetch git remote '#{remote}' during release parity checks.
|
|
1793
|
+
kettle-release enforces trunk parity across active remotes before publishing.
|
|
1794
|
+
|
|
1795
|
+
If this remote is temporarily unavailable or intentionally not part of this release,
|
|
1796
|
+
rerun with one of:
|
|
1797
|
+
kettle-release --skip-remotes #{remote}
|
|
1798
|
+
K_RELEASE_SKIP_REMOTES=#{remote} kettle-release
|
|
1799
|
+
|
|
1800
|
+
Original failure:
|
|
1801
|
+
#{detail}
|
|
1802
|
+
MSG
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1463
1805
|
def merge_feature_into_trunk_and_push!(trunk, feature)
|
|
1464
1806
|
return if feature.nil? || feature == trunk
|
|
1465
1807
|
|
|
@@ -1709,3 +2051,14 @@ module Kettle
|
|
|
1709
2051
|
end
|
|
1710
2052
|
end
|
|
1711
2053
|
end
|
|
2054
|
+
|
|
2055
|
+
class Kettle::Dev::ReleaseCLI::ReleaseCandidate
|
|
2056
|
+
attr_accessor :gem_name, :version, :installed_before, :published
|
|
2057
|
+
|
|
2058
|
+
def initialize(gem_name:, version:, installed_before:, published:)
|
|
2059
|
+
@gem_name = gem_name
|
|
2060
|
+
@version = version
|
|
2061
|
+
@installed_before = installed_before
|
|
2062
|
+
@published = published
|
|
2063
|
+
end
|
|
2064
|
+
end
|
data/lib/kettle/dev/version.rb
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kettle-dev
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -71,6 +71,26 @@ dependencies:
|
|
|
71
71
|
- - ">="
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
73
|
version: 0.3.1
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: kettle-ndjson
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0.1'
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 0.1.1
|
|
84
|
+
type: :runtime
|
|
85
|
+
prerelease: false
|
|
86
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - "~>"
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0.1'
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: 0.1.1
|
|
74
94
|
- !ruby/object:Gem::Dependency
|
|
75
95
|
name: kettle-rb
|
|
76
96
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -349,10 +369,10 @@ licenses:
|
|
|
349
369
|
- AGPL-3.0-only
|
|
350
370
|
metadata:
|
|
351
371
|
homepage_uri: https://kettle-dev.galtzo.com
|
|
352
|
-
source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.4.
|
|
353
|
-
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.4.
|
|
372
|
+
source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.4.3
|
|
373
|
+
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.4.3/CHANGELOG.md
|
|
354
374
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
|
|
355
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.4.
|
|
375
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.4.3
|
|
356
376
|
funding_uri: https://github.com/sponsors/pboling
|
|
357
377
|
wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
|
|
358
378
|
news_uri: https://www.railsbling.com/tags/kettle-dev
|
metadata.gz.sig
CHANGED
|
Binary file
|