kettle-dev 2.5.12 → 2.5.13
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 +45 -1
- data/README.md +1 -1
- data/exe/kettle-changelog +25 -3
- data/exe/kettle-release +26 -2
- data/lib/kettle/dev/changelog_cli.rb +43 -1
- data/lib/kettle/dev/ci_monitor.rb +206 -7
- data/lib/kettle/dev/pre_release_cli.rb +41 -2
- data/lib/kettle/dev/release_cli.rb +278 -20
- data/lib/kettle/dev/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +6 -6
- 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: 76cf339458385418bcf752559dbb9c6278f3b95ebfa27b26781591564179f263
|
|
4
|
+
data.tar.gz: 66cae3bfb62ecd922bf2b6ecc16049389a860a20dbaa8492fd6578bdcf3b4922
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ae8aa5d0113e226dab9dbb332b42328bfe738c84ce88a21d919c3a0249d2ad3f70d905aa4cbd76c903608846da8a485a6e68c710934e7eac2443517d71d6a96
|
|
7
|
+
data.tar.gz: 4470d0bd47c7c95a616b8513a6b590911a4086e6d6f31330e96c45059774ac4d172a0c98ccf5aad53384208cf40fb02de237c68c6326a430acaf89adfe9a89c2
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,48 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [2.5.13] - 2026-07-31
|
|
34
|
+
|
|
35
|
+
- TAG: [v2.5.13][2.5.13t]
|
|
36
|
+
- COVERAGE: 91.81% -- 5137/5595 lines in 46 files
|
|
37
|
+
- BRANCH COVERAGE: 76.06% -- 1944/2556 branches in 46 files
|
|
38
|
+
- 50.41% documented
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- `kettle-release` now treats `origin` as the default required parity remote
|
|
43
|
+
while allowing optional mirror remotes to be skipped after fetch retries,
|
|
44
|
+
so a transient Codeberg/GitLab mirror outage does not block publishing.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `kettle-release --events` now emits `remote_parity` events for remote parity
|
|
49
|
+
fetch attempts, skipped optional remotes, required failures, and parity
|
|
50
|
+
completion.
|
|
51
|
+
- `kettle-release --events` now emits `ci_monitor` events around CI monitoring
|
|
52
|
+
start, per-workflow GitHub Actions progress, GitLab pipeline outcomes,
|
|
53
|
+
success, and failure.
|
|
54
|
+
- `kettle-release --events` now emits `pre_release` events around
|
|
55
|
+
`kettle-pre-release` checks and selected check summaries.
|
|
56
|
+
- `kettle-changelog --events` now emits `changelog` events around release plan,
|
|
57
|
+
coverage generation, and changelog update activity, and `kettle-release
|
|
58
|
+
--events` forwards those child events.
|
|
59
|
+
- `kettle-release --events` now emits `release_lockfile` and `release_probe`
|
|
60
|
+
events for release lockfile reset/validation and published-gem availability
|
|
61
|
+
probes.
|
|
62
|
+
- `kettle-dev` now requires `kettle-ndjson` 0.1.9 or newer so release event
|
|
63
|
+
streams resolve against the protocol that includes remote parity and CI
|
|
64
|
+
monitor, pre-release, changelog, release lockfile, and release probe events.
|
|
65
|
+
|
|
66
|
+
- `kettle-release --events` now emits GitHub CI wait/start/tick monitor events
|
|
67
|
+
while waiting for workflow runs to appear and complete.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- `kettle-release --events` now emits clearer `command_step` names and compact
|
|
72
|
+
per-command summaries for common release commands instead of shell-derived
|
|
73
|
+
labels such as `env__u`.
|
|
74
|
+
|
|
33
75
|
## [2.5.12] - 2026-07-31
|
|
34
76
|
|
|
35
77
|
- TAG: [v2.5.12][2.5.12t]
|
|
@@ -3118,7 +3160,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
3118
3160
|
- Selecting will run the selected workflow via `act`
|
|
3119
3161
|
- This may move to its own gem in the future.
|
|
3120
3162
|
|
|
3121
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.
|
|
3163
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.13...HEAD
|
|
3164
|
+
[2.5.13]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.12...v2.5.13
|
|
3165
|
+
[2.5.13t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.13
|
|
3122
3166
|
[2.5.12]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.11...v2.5.12
|
|
3123
3167
|
[2.5.12t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.12
|
|
3124
3168
|
[2.5.11]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.10...v2.5.11
|
data/README.md
CHANGED
|
@@ -1005,7 +1005,7 @@ Thanks for RTFM. ☺️
|
|
|
1005
1005
|
[📌gitmoji]: https://gitmoji.dev
|
|
1006
1006
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1007
1007
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1008
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-5.
|
|
1008
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-5.595-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1009
1009
|
[🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
|
|
1010
1010
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1011
1011
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/exe/kettle-changelog
CHANGED
|
@@ -50,9 +50,11 @@ begin
|
|
|
50
50
|
|
|
51
51
|
# This library
|
|
52
52
|
require "kettle/dev"
|
|
53
|
+
require "kettle/ndjson"
|
|
53
54
|
require "kettle/dev/executable_version"
|
|
54
55
|
Kettle::Dev::ExecutableVersion.print_and_exit!(script_basename, ARGV, value_option: true)
|
|
55
|
-
|
|
56
|
+
machine_output = ARGV.any? { |arg| arg == "--json" || arg.start_with?("--events") }
|
|
57
|
+
Kettle::Dev::ExecutableVersion.print_header(script_basename) unless machine_output
|
|
56
58
|
rescue LoadError => e
|
|
57
59
|
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
58
60
|
warn("Hint: Ensure the host project has kettle-dev as a dependency and run bundle install.")
|
|
@@ -61,7 +63,7 @@ end
|
|
|
61
63
|
|
|
62
64
|
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
63
65
|
puts <<~USAGE
|
|
64
|
-
Usage: kettle-changelog [--version VERSION] [--yes] [--update-prep] [--pending-release] [--release-state] [--refresh-cache] [--add-unreleased-entry --section SECTION --entry TEXT] [--json] [--no-strict] [--no-coverage-threshold]
|
|
66
|
+
Usage: kettle-changelog [--version VERSION] [--yes] [--update-prep] [--pending-release] [--release-state] [--refresh-cache] [--add-unreleased-entry --section SECTION --entry TEXT] [--json|--events[=TYPES]] [--no-strict] [--no-coverage-threshold]
|
|
65
67
|
|
|
66
68
|
Detects the current version from lib/**/version.rb, the latest live release, and
|
|
67
69
|
the most recent CHANGELOG.md release section, then prompts to confirm the selected plan:
|
|
@@ -82,6 +84,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
|
|
|
82
84
|
--section SECTION Unreleased section to receive the entry (Added, Changed, Deprecated, Removed, Fixed, Security)
|
|
83
85
|
--entry TEXT Changelog entry text; "- " is added when omitted
|
|
84
86
|
--json Print query output as JSON
|
|
87
|
+
--events[=TYPES] Print newline-delimited JSON changelog events
|
|
85
88
|
--no-strict Allow missing coverage and yard data (warnings only, no errors)
|
|
86
89
|
--no-coverage-threshold Generate coverage without hard-failing below configured thresholds
|
|
87
90
|
|
|
@@ -137,6 +140,22 @@ begin
|
|
|
137
140
|
Kettle::Dev::Versioning.normalize_explicit_version(version)
|
|
138
141
|
end
|
|
139
142
|
|
|
143
|
+
def extract_events_arg!(argv)
|
|
144
|
+
events = false
|
|
145
|
+
event_types = nil
|
|
146
|
+
argv.delete_if do |arg|
|
|
147
|
+
if arg == "--events"
|
|
148
|
+
events = true
|
|
149
|
+
elsif arg.start_with?("--events=")
|
|
150
|
+
events = true
|
|
151
|
+
event_types = arg.split("=", 2)[1]
|
|
152
|
+
else
|
|
153
|
+
false
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
[events, event_types]
|
|
157
|
+
end
|
|
158
|
+
|
|
140
159
|
# Determine if strict mode is enabled (default: true)
|
|
141
160
|
strict_mode = !ARGV.include?("--no-strict") && ENV.fetch("K_CHANGELOG_STRICT", "true").downcase != "false"
|
|
142
161
|
update_prep = ARGV.delete("--update-prep")
|
|
@@ -146,6 +165,8 @@ begin
|
|
|
146
165
|
refresh_cache = ARGV.delete("--refresh-cache")
|
|
147
166
|
add_unreleased_entry = ARGV.delete("--add-unreleased-entry") || ARGV.delete("--add-changelog-entry")
|
|
148
167
|
json_output = ARGV.delete("--json")
|
|
168
|
+
events_output, event_types = extract_events_arg!(ARGV)
|
|
169
|
+
Kettle::Dev::ExitAdapter.abort("--events cannot be combined with --json") if events_output && json_output
|
|
149
170
|
coverage_threshold_disabled = ARGV.include?("--no-coverage-threshold") || ARGV.include?("--no-coverage-thresholds")
|
|
150
171
|
coverage_hard = !coverage_threshold_disabled && ENV.fetch("K_CHANGELOG_COVERAGE_HARD", "true").downcase != "false"
|
|
151
172
|
version_override = extract_version_arg!(ARGV)
|
|
@@ -180,7 +201,8 @@ begin
|
|
|
180
201
|
exit(0)
|
|
181
202
|
end
|
|
182
203
|
|
|
183
|
-
|
|
204
|
+
event_stream = events_output ? Kettle::Ndjson.event_stream($stdout, types: event_types) : nil
|
|
205
|
+
cli = Kettle::Dev::ChangelogCLI.new(strict: strict_mode, enforce_coverage_thresholds: coverage_hard, update_prep: update_prep, version: version_override, refresh_cache: refresh_cache, yes: yes, event_stream: event_stream)
|
|
184
206
|
if release_state_query
|
|
185
207
|
state = cli.release_state
|
|
186
208
|
puts(json_output ? JSON.pretty_generate(state) : cli.release_state_table(state))
|
data/exe/kettle-release
CHANGED
|
@@ -49,7 +49,7 @@ end
|
|
|
49
49
|
# Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
|
|
50
50
|
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
51
51
|
puts <<~USAGE
|
|
52
|
-
Usage: kettle-release [--version VERSION] [--local-ci] [--yes] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [--ci-workflows WORKFLOWS] [--skip-remotes REMOTES] [--secrets-provider NAME] [--json|--events[=TYPES]] [--report PATH] [start_step=<0-19>]
|
|
52
|
+
Usage: kettle-release [--version VERSION] [--local-ci] [--yes] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [--ci-workflows WORKFLOWS] [--skip-remotes REMOTES] [--required-remotes REMOTES] [--secrets-provider NAME] [--json|--events[=TYPES]] [--report PATH] [start_step=<0-19>]
|
|
53
53
|
|
|
54
54
|
Automates the release flow for a Ruby gem in the host project.
|
|
55
55
|
|
|
@@ -90,6 +90,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
|
|
|
90
90
|
--skip-bundle-audit # Skip bundle:audit/update during release rake checks
|
|
91
91
|
--ci-workflows WORKFLOWS # Comma-separated workflow files or stems to monitor at CI step 10
|
|
92
92
|
--skip-remotes REMOTES # Comma-separated git remote names to exclude from release fetch/push parity
|
|
93
|
+
--required-remotes REMOTES # Comma-separated remote names that must fetch for release parity; defaults to origin
|
|
93
94
|
--secrets-provider NAME # Release secrets provider: interactive, 1password
|
|
94
95
|
--json # Print final machine-readable release report as JSON
|
|
95
96
|
--events[=TYPES] # Print newline-delimited JSON release events, optionally filtered by type
|
|
@@ -104,6 +105,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
|
|
|
104
105
|
K_RELEASE_LOCAL_CI_WORKFLOW # Name of workflow (without .yml) for local CI; defaults to locked_deps or first
|
|
105
106
|
K_RELEASE_CI_WORKFLOWS # Comma-separated workflow files or stems to monitor at CI step 10
|
|
106
107
|
K_RELEASE_SKIP_REMOTES # Comma-separated git remote names to exclude from release fetch/push parity
|
|
108
|
+
K_RELEASE_REQUIRED_REMOTES # Comma-separated remotes that must fetch for release parity; defaults to origin
|
|
107
109
|
KETTLE_RELEASE_SECRETS_PROVIDER=1password
|
|
108
110
|
# Load release secrets through the configured provider
|
|
109
111
|
KETTLE_RELEASE_1PASSWORD_ITEM=Rubygems
|
|
@@ -215,6 +217,27 @@ def extract_skip_remotes_arg!(argv)
|
|
|
215
217
|
remotes
|
|
216
218
|
end
|
|
217
219
|
|
|
220
|
+
def extract_required_remotes_arg!(argv)
|
|
221
|
+
remotes = nil
|
|
222
|
+
if (idx = argv.index("--required-remotes"))
|
|
223
|
+
remotes = argv[idx + 1]
|
|
224
|
+
if remotes.to_s.empty? || remotes.to_s.start_with?("-")
|
|
225
|
+
Kettle::Dev::ExitAdapter.abort("--required-remotes requires a comma-separated remote list")
|
|
226
|
+
end
|
|
227
|
+
argv.slice!(idx, 2)
|
|
228
|
+
end
|
|
229
|
+
argv.delete_if do |arg|
|
|
230
|
+
if arg.start_with?("--required-remotes=", "required_remotes=")
|
|
231
|
+
remotes = arg.split("=", 2)[1]
|
|
232
|
+
Kettle::Dev::ExitAdapter.abort("--required-remotes requires a comma-separated remote list") if remotes.to_s.empty?
|
|
233
|
+
true
|
|
234
|
+
else
|
|
235
|
+
false
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
remotes
|
|
239
|
+
end
|
|
240
|
+
|
|
218
241
|
def extract_report_arg!(argv)
|
|
219
242
|
report_path = nil
|
|
220
243
|
if (idx = argv.index("--report"))
|
|
@@ -286,12 +309,13 @@ version_override = extract_version_arg!(ARGV)
|
|
|
286
309
|
skip_steps = extract_skip_steps_arg!(ARGV)
|
|
287
310
|
ci_workflows = extract_ci_workflows_arg!(ARGV)
|
|
288
311
|
skip_remotes = extract_skip_remotes_arg!(ARGV)
|
|
312
|
+
required_remotes = extract_required_remotes_arg!(ARGV)
|
|
289
313
|
report_path = extract_report_arg!(ARGV)
|
|
290
314
|
secrets_provider = extract_secrets_provider_arg!(ARGV)
|
|
291
315
|
|
|
292
316
|
begin
|
|
293
317
|
event_stream = events_output ? Kettle::Ndjson.event_stream($stdout, types: event_types) : nil
|
|
294
|
-
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, secrets_provider_name: secrets_provider, yes: yes, event_stream: event_stream, json_output: json_output, json_io: $stdout, report_path: report_path).run
|
|
318
|
+
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, required_remotes: required_remotes, secrets_provider_name: secrets_provider, yes: yes, event_stream: event_stream, json_output: json_output, json_io: $stdout, report_path: report_path).run
|
|
295
319
|
rescue LoadError => e
|
|
296
320
|
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
297
321
|
warn(Array(e.backtrace).join("\n")) if ENV["DEBUG"]
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "net/http"
|
|
6
6
|
require "uri"
|
|
7
7
|
require "fileutils"
|
|
8
|
+
require "kettle/ndjson"
|
|
8
9
|
|
|
9
10
|
module Kettle
|
|
10
11
|
module Dev
|
|
@@ -28,7 +29,7 @@ module Kettle
|
|
|
28
29
|
# @param update_prep [Boolean] when true, update the most recent prepared release section in place
|
|
29
30
|
# @param version [String, nil] explicit version override for gems without a literal VERSION constant
|
|
30
31
|
# @param yes [Boolean] when true, approve the selected release plan without prompting
|
|
31
|
-
def initialize(strict: true, enforce_coverage_thresholds: true, update_prep: false, version: nil, root: Kettle::Dev::CIHelpers.project_root, refresh_cache: false, yes: false)
|
|
32
|
+
def initialize(strict: true, enforce_coverage_thresholds: true, update_prep: false, version: nil, root: Kettle::Dev::CIHelpers.project_root, refresh_cache: false, yes: false, event_stream: nil)
|
|
32
33
|
@root = root
|
|
33
34
|
@changelog_path = resolved_changelog_path
|
|
34
35
|
@coverage_root = resolved_coverage_root
|
|
@@ -39,6 +40,7 @@ module Kettle
|
|
|
39
40
|
@version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
|
|
40
41
|
@refresh_cache = refresh_cache
|
|
41
42
|
@yes = !!yes
|
|
43
|
+
@event_recorder = Kettle::Ndjson.event_recorder(event_stream, phase_timings: [])
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
# Main entry point to update CHANGELOG.md
|
|
@@ -70,6 +72,7 @@ module Kettle
|
|
|
70
72
|
|
|
71
73
|
if plan.fetch(:action) == :update_prepared_release
|
|
72
74
|
update_prepared_release!(changelog, today, owner, repo, line_cov_line, branch_cov_line, yard_line)
|
|
75
|
+
emit_changelog_event(action: "update", status: "ok", version: version, plan: plan.fetch(:action).to_s)
|
|
73
76
|
return
|
|
74
77
|
end
|
|
75
78
|
|
|
@@ -140,6 +143,7 @@ module Kettle
|
|
|
140
143
|
updated = updated.rstrip + "\n"
|
|
141
144
|
|
|
142
145
|
File.write(@changelog_path, updated)
|
|
146
|
+
emit_changelog_event(action: "update", status: "ok", version: version, plan: plan.fetch(:action).to_s)
|
|
143
147
|
puts "CHANGELOG.md updated with v#{version} section."
|
|
144
148
|
end
|
|
145
149
|
|
|
@@ -348,6 +352,16 @@ module Kettle
|
|
|
348
352
|
puts " latest released for current series: #{plan.fetch(:latest_for_series) || "unknown"}"
|
|
349
353
|
puts " latest CHANGELOG.md release: #{plan.fetch(:latest_changelog_version) || "none"}"
|
|
350
354
|
puts " gem: #{plan.fetch(:gem_name) || "unknown"}"
|
|
355
|
+
emit_changelog_event(
|
|
356
|
+
action: "plan",
|
|
357
|
+
status: "ok",
|
|
358
|
+
plan: plan.fetch(:action).to_s,
|
|
359
|
+
version: plan.fetch(:version),
|
|
360
|
+
gem_name: plan.fetch(:gem_name),
|
|
361
|
+
latest_overall: plan.fetch(:latest_overall),
|
|
362
|
+
latest_for_series: plan.fetch(:latest_for_series),
|
|
363
|
+
latest_changelog_version: plan.fetch(:latest_changelog_version)
|
|
364
|
+
)
|
|
351
365
|
if @yes
|
|
352
366
|
puts("Continue with this plan? [y/N]: y")
|
|
353
367
|
return
|
|
@@ -665,20 +679,25 @@ module Kettle
|
|
|
665
679
|
coverage_dir = File.dirname(@coverage_path)
|
|
666
680
|
if Dir.exist?(coverage_dir)
|
|
667
681
|
puts "Cleaning old coverage data from #{Kettle::Dev.display_path(coverage_dir)}..."
|
|
682
|
+
emit_changelog_event(action: "coverage_clean", status: "started", path: Kettle::Dev.display_path(coverage_dir))
|
|
668
683
|
Dir.glob(File.join(coverage_dir, "*")).each do |file|
|
|
669
684
|
File.delete(file) if File.file?(file)
|
|
670
685
|
end
|
|
686
|
+
emit_changelog_event(action: "coverage_clean", status: "ok", path: Kettle::Dev.display_path(coverage_dir))
|
|
671
687
|
end
|
|
672
688
|
|
|
673
689
|
puts "Generating fresh coverage data by running: bundle exec kettle-test"
|
|
690
|
+
emit_changelog_event(action: "coverage", status: "started", command: "bundle exec kettle-test", root: Kettle::Dev.display_path(@coverage_root))
|
|
674
691
|
|
|
675
692
|
success = system(changelog_coverage_env, "bundle", "exec", "kettle-test", chdir: @coverage_root)
|
|
676
693
|
|
|
677
694
|
unless success
|
|
695
|
+
emit_changelog_event(action: "coverage", status: "failed", command: "bundle exec kettle-test", root: Kettle::Dev.display_path(@coverage_root), reason: "exit status #{$?.exitstatus || "unknown"}")
|
|
678
696
|
raise "bundle exec kettle-test failed with exit status #{$?.exitstatus || "unknown"}"
|
|
679
697
|
end
|
|
680
698
|
|
|
681
699
|
puts "Coverage generation complete."
|
|
700
|
+
emit_changelog_event(action: "coverage", status: "ok", command: "bundle exec kettle-test", root: Kettle::Dev.display_path(@coverage_root))
|
|
682
701
|
|
|
683
702
|
ensure_changelog_coverage_json!
|
|
684
703
|
else
|
|
@@ -739,6 +758,29 @@ module Kettle
|
|
|
739
758
|
end
|
|
740
759
|
end
|
|
741
760
|
|
|
761
|
+
def emit_changelog_event(action:, status:, **payload)
|
|
762
|
+
mark = case status.to_s
|
|
763
|
+
when "started"
|
|
764
|
+
">"
|
|
765
|
+
when "ok", "skipped"
|
|
766
|
+
"."
|
|
767
|
+
when "failed"
|
|
768
|
+
"!"
|
|
769
|
+
else
|
|
770
|
+
"?"
|
|
771
|
+
end
|
|
772
|
+
Kettle::Ndjson.emit_event(
|
|
773
|
+
@event_recorder,
|
|
774
|
+
"changelog",
|
|
775
|
+
payload.merge(
|
|
776
|
+
action: action,
|
|
777
|
+
phase: "release",
|
|
778
|
+
status: status,
|
|
779
|
+
mark: mark
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
end
|
|
783
|
+
|
|
742
784
|
def changelog_coverage_env
|
|
743
785
|
env = BundlerEnvGuard.unbundled_env.merge(
|
|
744
786
|
"PATH" => ENV.fetch("PATH", ""),
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "uri"
|
|
5
5
|
require "json"
|
|
6
6
|
require "net/http"
|
|
7
|
+
require "kettle/ndjson"
|
|
7
8
|
|
|
8
9
|
module Kettle
|
|
9
10
|
module Dev
|
|
@@ -47,10 +48,10 @@ module Kettle
|
|
|
47
48
|
#
|
|
48
49
|
# @param restart_hint [String] guidance command shown on failure
|
|
49
50
|
# @return [void]
|
|
50
|
-
def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10", workflows: nil, keepalive: nil, **options)
|
|
51
|
+
def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10", workflows: nil, keepalive: nil, event_recorder: nil, **options)
|
|
51
52
|
checks_any = false
|
|
52
|
-
checks_any |= monitor_github_internal!(restart_hint: restart_hint, workflows: workflows, keepalive: keepalive)
|
|
53
|
-
checks_any |= monitor_gitlab_internal!(restart_hint: restart_hint, keepalive: keepalive)
|
|
53
|
+
checks_any |= monitor_github_internal!(restart_hint: restart_hint, workflows: workflows, keepalive: keepalive, event_recorder: event_recorder)
|
|
54
|
+
checks_any |= monitor_gitlab_internal!(restart_hint: restart_hint, keepalive: keepalive, event_recorder: event_recorder)
|
|
54
55
|
abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
|
|
55
56
|
end
|
|
56
57
|
|
|
@@ -267,7 +268,7 @@ module Kettle
|
|
|
267
268
|
end
|
|
268
269
|
module_function :collect_gitlab
|
|
269
270
|
|
|
270
|
-
def monitor_github_internal!(restart_hint:, workflows: nil, keepalive: nil)
|
|
271
|
+
def monitor_github_internal!(restart_hint:, workflows: nil, keepalive: nil, event_recorder: nil)
|
|
271
272
|
root = Kettle::Dev::CIHelpers.project_root
|
|
272
273
|
workflows = Array(workflows).empty? ? Kettle::Dev::CIHelpers.workflows_list(root) : Array(workflows)
|
|
273
274
|
gh_remote = preferred_github_remote
|
|
@@ -288,8 +289,31 @@ module Kettle
|
|
|
288
289
|
|
|
289
290
|
passed = {}
|
|
290
291
|
started = {}
|
|
292
|
+
emit_ci_monitor_event(
|
|
293
|
+
event_recorder,
|
|
294
|
+
action: "github_discover",
|
|
295
|
+
status: "started",
|
|
296
|
+
provider: "github",
|
|
297
|
+
remote: gh_remote,
|
|
298
|
+
owner: owner,
|
|
299
|
+
repo: repo,
|
|
300
|
+
branch: branch,
|
|
301
|
+
head_sha: head_sha,
|
|
302
|
+
workflows: workflows,
|
|
303
|
+
total: total
|
|
304
|
+
)
|
|
291
305
|
puts "Ensuring GitHub Actions workflows pass on #{branch} (#{owner}/#{repo}) via remote '#{gh_remote}'"
|
|
292
306
|
puts "Waiting for GitHub Actions runs to start for HEAD #{head_sha[0, 12]}."
|
|
307
|
+
emit_ci_monitor_event(
|
|
308
|
+
event_recorder,
|
|
309
|
+
action: "github_wait",
|
|
310
|
+
status: "started",
|
|
311
|
+
provider: "github",
|
|
312
|
+
branch: branch,
|
|
313
|
+
head_sha: head_sha,
|
|
314
|
+
completed: 0,
|
|
315
|
+
total: total
|
|
316
|
+
)
|
|
293
317
|
pbar = if defined?(ProgressBar)
|
|
294
318
|
ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
|
|
295
319
|
end
|
|
@@ -308,27 +332,105 @@ module Kettle
|
|
|
308
332
|
start_deadline = monotonic_time + start_timeout
|
|
309
333
|
keepalive_timer = keepalive_timer(keepalive)
|
|
310
334
|
idx = 0
|
|
335
|
+
all_started_emitted = false
|
|
311
336
|
loop do
|
|
312
337
|
keepalive_timer.call
|
|
313
338
|
wf = workflows[idx]
|
|
314
339
|
run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch, require_head: true, head_sha: head_sha)
|
|
340
|
+
completed_workflow = nil
|
|
315
341
|
if run
|
|
316
|
-
started[wf]
|
|
342
|
+
unless started[wf]
|
|
343
|
+
started[wf] = true
|
|
344
|
+
emit_ci_monitor_event(
|
|
345
|
+
event_recorder,
|
|
346
|
+
action: "github_workflow",
|
|
347
|
+
status: "started",
|
|
348
|
+
provider: "github",
|
|
349
|
+
workflow: wf,
|
|
350
|
+
url: run["html_url"],
|
|
351
|
+
run_status: run["status"],
|
|
352
|
+
conclusion: run["conclusion"],
|
|
353
|
+
completed: passed.size,
|
|
354
|
+
total: total
|
|
355
|
+
)
|
|
356
|
+
end
|
|
357
|
+
if started.size == total && !all_started_emitted
|
|
358
|
+
all_started_emitted = true
|
|
359
|
+
emit_ci_monitor_event(
|
|
360
|
+
event_recorder,
|
|
361
|
+
action: "github_started",
|
|
362
|
+
status: "ok",
|
|
363
|
+
provider: "github",
|
|
364
|
+
completed: passed.size,
|
|
365
|
+
total: total,
|
|
366
|
+
started: started.size
|
|
367
|
+
)
|
|
368
|
+
end
|
|
317
369
|
if Kettle::Dev::CIHelpers.success?(run)
|
|
318
370
|
unless passed[wf]
|
|
319
371
|
passed[wf] = true
|
|
372
|
+
completed_workflow = wf
|
|
320
373
|
pbar&.increment
|
|
374
|
+
emit_ci_monitor_event(
|
|
375
|
+
event_recorder,
|
|
376
|
+
action: "github_workflow",
|
|
377
|
+
status: "ok",
|
|
378
|
+
provider: "github",
|
|
379
|
+
workflow: wf,
|
|
380
|
+
url: run["html_url"],
|
|
381
|
+
run_status: run["status"],
|
|
382
|
+
conclusion: run["conclusion"],
|
|
383
|
+
completed: passed.size,
|
|
384
|
+
total: total
|
|
385
|
+
)
|
|
321
386
|
end
|
|
322
387
|
elsif Kettle::Dev::CIHelpers.failed?(run)
|
|
323
388
|
puts
|
|
324
389
|
wf_url = run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}"
|
|
390
|
+
emit_ci_monitor_event(
|
|
391
|
+
event_recorder,
|
|
392
|
+
action: "github_workflow",
|
|
393
|
+
status: "failed",
|
|
394
|
+
provider: "github",
|
|
395
|
+
workflow: wf,
|
|
396
|
+
url: wf_url,
|
|
397
|
+
run_status: run["status"],
|
|
398
|
+
conclusion: run["conclusion"],
|
|
399
|
+
completed: passed.size,
|
|
400
|
+
total: total,
|
|
401
|
+
reason: "Workflow failed"
|
|
402
|
+
)
|
|
325
403
|
abort("Workflow failed: #{wf} -> #{wf_url} Fix the workflow, then restart this tool from CI validation with: #{restart_hint}")
|
|
326
404
|
end
|
|
327
405
|
end
|
|
406
|
+
emit_ci_monitor_event(
|
|
407
|
+
event_recorder,
|
|
408
|
+
action: "github_tick",
|
|
409
|
+
status: "started",
|
|
410
|
+
provider: "github",
|
|
411
|
+
workflow: wf,
|
|
412
|
+
run_status: run && run["status"],
|
|
413
|
+
conclusion: run && run["conclusion"],
|
|
414
|
+
completed_workflow: completed_workflow,
|
|
415
|
+
completed: passed.size,
|
|
416
|
+
total: total,
|
|
417
|
+
started: started.size
|
|
418
|
+
)
|
|
328
419
|
break if passed.size == total
|
|
329
420
|
if started.size < total && monotonic_time >= start_deadline
|
|
330
421
|
missing = (workflows - started.keys).join(", ")
|
|
331
422
|
puts
|
|
423
|
+
emit_ci_monitor_event(
|
|
424
|
+
event_recorder,
|
|
425
|
+
action: "github_timeout",
|
|
426
|
+
status: "failed",
|
|
427
|
+
provider: "github",
|
|
428
|
+
missing: workflows - started.keys,
|
|
429
|
+
head_sha: head_sha,
|
|
430
|
+
total: total,
|
|
431
|
+
completed: passed.size,
|
|
432
|
+
reason: "Timed out waiting for workflows to start"
|
|
433
|
+
)
|
|
332
434
|
abort("Timed out after #{start_timeout}s waiting for GitHub Actions workflows to start for HEAD #{head_sha[0, 12]}: #{missing}. Confirm GitHub Actions started, then restart this tool from CI validation with: #{restart_hint}")
|
|
333
435
|
end
|
|
334
436
|
|
|
@@ -336,6 +438,15 @@ module Kettle
|
|
|
336
438
|
sleep(poll_interval)
|
|
337
439
|
end
|
|
338
440
|
pbar&.finish unless pbar&.finished?
|
|
441
|
+
emit_ci_monitor_event(
|
|
442
|
+
event_recorder,
|
|
443
|
+
action: "github_complete",
|
|
444
|
+
status: "ok",
|
|
445
|
+
provider: "github",
|
|
446
|
+
workflows: workflows,
|
|
447
|
+
completed: passed.size,
|
|
448
|
+
total: total
|
|
449
|
+
)
|
|
339
450
|
puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
|
|
340
451
|
true
|
|
341
452
|
end
|
|
@@ -391,7 +502,7 @@ module Kettle
|
|
|
391
502
|
end
|
|
392
503
|
module_function :keepalive_interval
|
|
393
504
|
|
|
394
|
-
def monitor_gitlab_internal!(restart_hint:, keepalive: nil)
|
|
505
|
+
def monitor_gitlab_internal!(restart_hint:, keepalive: nil, event_recorder: nil)
|
|
395
506
|
root = Kettle::Dev::CIHelpers.project_root
|
|
396
507
|
gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
|
|
397
508
|
gl_remote = gitlab_remote_candidates.first
|
|
@@ -403,6 +514,17 @@ module Kettle
|
|
|
403
514
|
owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
|
|
404
515
|
return false unless owner && repo
|
|
405
516
|
|
|
517
|
+
emit_ci_monitor_event(
|
|
518
|
+
event_recorder,
|
|
519
|
+
action: "gitlab_discover",
|
|
520
|
+
status: "started",
|
|
521
|
+
provider: "gitlab",
|
|
522
|
+
remote: gl_remote,
|
|
523
|
+
owner: owner,
|
|
524
|
+
repo: repo,
|
|
525
|
+
branch: branch,
|
|
526
|
+
total: 1
|
|
527
|
+
)
|
|
406
528
|
puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
|
|
407
529
|
pbar = if defined?(ProgressBar)
|
|
408
530
|
ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
|
|
@@ -412,8 +534,19 @@ module Kettle
|
|
|
412
534
|
keepalive_timer.call
|
|
413
535
|
pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
|
|
414
536
|
if pipe
|
|
537
|
+
pipe_url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
|
|
415
538
|
if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
|
|
416
539
|
pbar&.increment unless pbar&.finished?
|
|
540
|
+
emit_ci_monitor_event(
|
|
541
|
+
event_recorder,
|
|
542
|
+
action: "gitlab_pipeline",
|
|
543
|
+
status: "ok",
|
|
544
|
+
provider: "gitlab",
|
|
545
|
+
url: pipe_url,
|
|
546
|
+
run_status: pipe["status"],
|
|
547
|
+
completed: 1,
|
|
548
|
+
total: 1
|
|
549
|
+
)
|
|
417
550
|
break
|
|
418
551
|
elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
|
|
419
552
|
# Special-case: if failure is due to exhausted minutes/insufficient quota, treat as unknown and continue
|
|
@@ -421,27 +554,93 @@ module Kettle
|
|
|
421
554
|
if /insufficient|quota|minute/i.match?(reason)
|
|
422
555
|
puts "\nGitLab reports pipeline cannot run due to quota/minutes exhaustion. Result is unknown; continuing."
|
|
423
556
|
pbar&.finish unless pbar&.finished?
|
|
557
|
+
emit_ci_monitor_event(
|
|
558
|
+
event_recorder,
|
|
559
|
+
action: "gitlab_pipeline",
|
|
560
|
+
status: "skipped",
|
|
561
|
+
provider: "gitlab",
|
|
562
|
+
url: pipe_url,
|
|
563
|
+
run_status: pipe["status"],
|
|
564
|
+
reason: reason,
|
|
565
|
+
completed: 0,
|
|
566
|
+
total: 1
|
|
567
|
+
)
|
|
424
568
|
break
|
|
425
569
|
else
|
|
426
570
|
puts
|
|
427
|
-
|
|
571
|
+
emit_ci_monitor_event(
|
|
572
|
+
event_recorder,
|
|
573
|
+
action: "gitlab_pipeline",
|
|
574
|
+
status: "failed",
|
|
575
|
+
provider: "gitlab",
|
|
576
|
+
url: pipe_url,
|
|
577
|
+
run_status: pipe["status"],
|
|
578
|
+
reason: reason,
|
|
579
|
+
completed: 0,
|
|
580
|
+
total: 1
|
|
581
|
+
)
|
|
582
|
+
url = pipe_url
|
|
428
583
|
abort("Pipeline failed: #{url} Fix the pipeline, then restart this tool from CI validation with: #{restart_hint}")
|
|
429
584
|
end
|
|
430
585
|
elsif pipe["status"] == "blocked"
|
|
431
586
|
# Blocked pipeline (e.g., awaiting approvals) — treat as unknown and continue
|
|
432
587
|
puts "\nGitLab pipeline is blocked. Result is unknown; continuing."
|
|
433
588
|
pbar&.finish unless pbar&.finished?
|
|
589
|
+
emit_ci_monitor_event(
|
|
590
|
+
event_recorder,
|
|
591
|
+
action: "gitlab_pipeline",
|
|
592
|
+
status: "skipped",
|
|
593
|
+
provider: "gitlab",
|
|
594
|
+
url: pipe_url,
|
|
595
|
+
run_status: pipe["status"],
|
|
596
|
+
reason: "blocked",
|
|
597
|
+
completed: 0,
|
|
598
|
+
total: 1
|
|
599
|
+
)
|
|
434
600
|
break
|
|
435
601
|
end
|
|
436
602
|
end
|
|
437
603
|
sleep(1)
|
|
438
604
|
end
|
|
439
605
|
pbar&.finish unless pbar&.finished?
|
|
606
|
+
emit_ci_monitor_event(
|
|
607
|
+
event_recorder,
|
|
608
|
+
action: "gitlab_complete",
|
|
609
|
+
status: "ok",
|
|
610
|
+
provider: "gitlab",
|
|
611
|
+
completed: 1,
|
|
612
|
+
total: 1
|
|
613
|
+
)
|
|
440
614
|
puts "\nGitLab pipeline passing."
|
|
441
615
|
true
|
|
442
616
|
end
|
|
443
617
|
module_function :monitor_gitlab_internal!
|
|
444
618
|
|
|
619
|
+
def emit_ci_monitor_event(event_recorder, action:, status:, provider:, **payload)
|
|
620
|
+
mark = case status.to_s
|
|
621
|
+
when "started"
|
|
622
|
+
">"
|
|
623
|
+
when "ok", "skipped"
|
|
624
|
+
"."
|
|
625
|
+
when "failed"
|
|
626
|
+
"!"
|
|
627
|
+
else
|
|
628
|
+
"?"
|
|
629
|
+
end
|
|
630
|
+
Kettle::Ndjson.emit_event(
|
|
631
|
+
event_recorder,
|
|
632
|
+
"ci_monitor",
|
|
633
|
+
payload.merge(
|
|
634
|
+
action: action,
|
|
635
|
+
phase: "release",
|
|
636
|
+
provider: provider,
|
|
637
|
+
status: status,
|
|
638
|
+
mark: mark
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
end
|
|
642
|
+
module_function :emit_ci_monitor_event
|
|
643
|
+
|
|
445
644
|
# -- tiny wrappers around GitAdapter-like helpers used by ReleaseCLI --
|
|
446
645
|
def remotes_with_urls
|
|
447
646
|
Kettle::Dev::GitAdapter.new.remotes_with_urls
|
|
@@ -10,6 +10,7 @@ require "net/http"
|
|
|
10
10
|
require "openssl"
|
|
11
11
|
require "time"
|
|
12
12
|
require_relative "cache_progress"
|
|
13
|
+
require "kettle/ndjson"
|
|
13
14
|
begin
|
|
14
15
|
require "addressable/uri"
|
|
15
16
|
rescue LoadError
|
|
@@ -271,9 +272,10 @@ module Kettle
|
|
|
271
272
|
end
|
|
272
273
|
|
|
273
274
|
# @param check_num [Integer] 1-based index to resume from
|
|
274
|
-
def initialize(check_num: 1)
|
|
275
|
+
def initialize(check_num: 1, event_recorder: nil)
|
|
275
276
|
@check_num = (check_num || 1).to_i
|
|
276
277
|
@check_num = 1 if @check_num < 1
|
|
278
|
+
@event_recorder = event_recorder
|
|
277
279
|
@image_url_cache_path = configured_image_url_cache_path
|
|
278
280
|
@refresh_image_url_cache = env_truthy?(ENV["KETTLE_IMAGE_URL_CACHE_REFRESH"])
|
|
279
281
|
@image_url_skip_patterns = configured_image_url_skip_patterns
|
|
@@ -294,7 +296,14 @@ module Kettle
|
|
|
294
296
|
checks[begin_idx..-1].each_with_index do |check, i|
|
|
295
297
|
idx = begin_idx + i + 1
|
|
296
298
|
puts "[kettle-pre-release] Running check ##{idx} of #{checks.size}"
|
|
297
|
-
check.
|
|
299
|
+
emit_pre_release_event(action: "check", status: "started", check: check_name(check), index: idx, total: checks.size)
|
|
300
|
+
begin
|
|
301
|
+
check.call
|
|
302
|
+
emit_pre_release_event(action: "check", status: "ok", check: check_name(check), index: idx, total: checks.size)
|
|
303
|
+
rescue => error
|
|
304
|
+
emit_pre_release_event(action: "check", status: "failed", check: check_name(check), index: idx, total: checks.size, reason: "#{error.class}: #{error.message}")
|
|
305
|
+
raise
|
|
306
|
+
end
|
|
298
307
|
end
|
|
299
308
|
nil
|
|
300
309
|
end
|
|
@@ -355,6 +364,7 @@ module Kettle
|
|
|
355
364
|
end
|
|
356
365
|
|
|
357
366
|
puts "[kettle-pre-release] Normalization candidates: #{total_candidates}. Files changed: #{changed.uniq.size}."
|
|
367
|
+
emit_pre_release_event(action: "markdown_urls", status: "ok", candidates: total_candidates, changed_files: changed.uniq.size)
|
|
358
368
|
nil
|
|
359
369
|
end
|
|
360
370
|
|
|
@@ -408,10 +418,12 @@ module Kettle
|
|
|
408
418
|
puts "[kettle-pre-release] Image URL checks: #{progress.cached_count} cached, #{progress.live_count} live."
|
|
409
419
|
puts "[kettle-pre-release] Skipped #{progress.skipped_count} image URL check(s)." if skipped.any?
|
|
410
420
|
if failures.any?
|
|
421
|
+
emit_pre_release_event(action: "image_links", status: "failed", urls: urls.size, cached: progress.cached_count, live: progress.live_count, skipped: progress.skipped_count, failures: failures.size)
|
|
411
422
|
warn("[kettle-pre-release] #{failures.size} image URL(s) failed validation:")
|
|
412
423
|
failures.each { |u| warn(" - #{u}") }
|
|
413
424
|
Kettle::Dev::ExitAdapter.abort("Image link validation failed")
|
|
414
425
|
else
|
|
426
|
+
emit_pre_release_event(action: "image_links", status: "ok", urls: urls.size, cached: progress.cached_count, live: progress.live_count, skipped: progress.skipped_count, failures: 0)
|
|
415
427
|
puts "[kettle-pre-release] All image links validated."
|
|
416
428
|
end
|
|
417
429
|
nil
|
|
@@ -419,6 +431,33 @@ module Kettle
|
|
|
419
431
|
|
|
420
432
|
private
|
|
421
433
|
|
|
434
|
+
def check_name(check)
|
|
435
|
+
check.name.to_s.sub(/\Acheck_/, "").sub(/!\z/, "")
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def emit_pre_release_event(action:, status:, **payload)
|
|
439
|
+
mark = case status.to_s
|
|
440
|
+
when "started"
|
|
441
|
+
">"
|
|
442
|
+
when "ok", "skipped"
|
|
443
|
+
"."
|
|
444
|
+
when "failed"
|
|
445
|
+
"!"
|
|
446
|
+
else
|
|
447
|
+
"?"
|
|
448
|
+
end
|
|
449
|
+
Kettle::Ndjson.emit_event(
|
|
450
|
+
@event_recorder,
|
|
451
|
+
"pre_release",
|
|
452
|
+
payload.merge(
|
|
453
|
+
action: action,
|
|
454
|
+
phase: "release",
|
|
455
|
+
status: status,
|
|
456
|
+
mark: mark
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
end
|
|
460
|
+
|
|
422
461
|
def image_url_cache
|
|
423
462
|
return nil if @image_url_cache_path.to_s.empty?
|
|
424
463
|
|
|
@@ -117,14 +117,15 @@ module Kettle
|
|
|
117
117
|
|
|
118
118
|
public
|
|
119
119
|
|
|
120
|
-
def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil, skip_bundle_audit: nil, ci_workflows: nil, skip_remotes: nil, secrets_provider_name: nil, yes: false, **options)
|
|
120
|
+
def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil, skip_bundle_audit: nil, ci_workflows: nil, skip_remotes: nil, required_remotes: nil, secrets_provider_name: nil, yes: false, **options)
|
|
121
121
|
@root = Kettle::Dev::CIHelpers.project_root
|
|
122
122
|
@git = Kettle::Dev::GitAdapter.new(@root)
|
|
123
123
|
@start_step = (start_step || 0).to_i
|
|
124
124
|
@start_step = 0 if @start_step < 0
|
|
125
125
|
@skip_steps = normalize_skip_steps(skip_steps)
|
|
126
126
|
@ci_workflows = normalize_ci_workflows(ci_workflows || ENV["K_RELEASE_CI_WORKFLOWS"])
|
|
127
|
-
@skip_remotes =
|
|
127
|
+
@skip_remotes = normalize_remote_names(skip_remotes || ENV["K_RELEASE_SKIP_REMOTES"], "skip remotes")
|
|
128
|
+
@required_remotes = normalize_required_remotes(required_remotes)
|
|
128
129
|
@local_ci = !!local_ci
|
|
129
130
|
@skip_bundle_audit = truthy_value?(skip_bundle_audit) || truthy_value?(ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"])
|
|
130
131
|
@yes = !!yes
|
|
@@ -479,14 +480,19 @@ module Kettle
|
|
|
479
480
|
workflows.map { |workflow| workflow.match?(/\.ya?ml\z/) ? workflow : "#{workflow}.yml" }.uniq
|
|
480
481
|
end
|
|
481
482
|
|
|
482
|
-
def
|
|
483
|
+
def normalize_remote_names(value, label)
|
|
483
484
|
remotes = Array(value).flat_map { |part| part.to_s.split(",") }.map(&:strip).reject(&:empty?)
|
|
484
485
|
invalid = remotes.find { |remote| !remote.match?(/\A[A-Za-z0-9_.-]+\z/) }
|
|
485
|
-
abort("Invalid
|
|
486
|
+
abort("Invalid #{label} value #{invalid.inspect}; use comma-separated git remote names.") if invalid
|
|
486
487
|
|
|
487
488
|
remotes.uniq
|
|
488
489
|
end
|
|
489
490
|
|
|
491
|
+
def normalize_required_remotes(value)
|
|
492
|
+
remotes = normalize_remote_names(value.nil? ? ENV["K_RELEASE_REQUIRED_REMOTES"] : value, "required remotes")
|
|
493
|
+
remotes.empty? ? ["origin"] : remotes
|
|
494
|
+
end
|
|
495
|
+
|
|
490
496
|
private
|
|
491
497
|
|
|
492
498
|
def local_ci?
|
|
@@ -571,7 +577,7 @@ module Kettle
|
|
|
571
577
|
|
|
572
578
|
def run_pre_release_checks!
|
|
573
579
|
puts "Running pre-release checks via kettle-pre-release..."
|
|
574
|
-
Kettle::Dev::PreReleaseCLI.new(check_num: 1).run
|
|
580
|
+
Kettle::Dev::PreReleaseCLI.new(check_num: 1, event_recorder: @event_recorder).run
|
|
575
581
|
run_changelog!
|
|
576
582
|
end
|
|
577
583
|
|
|
@@ -590,9 +596,11 @@ module Kettle
|
|
|
590
596
|
attempts = release_lockfile_reset_attempts(stage)
|
|
591
597
|
attempts.times do |index|
|
|
592
598
|
attempt = index + 1
|
|
599
|
+
emit_release_lockfile_event(action: "reset", status: "started", stage: stage, attempt: attempt, attempts: attempts)
|
|
593
600
|
puts "Resetting release lockfiles with local path dependencies disabled #{stage} (attempt #{attempt}/#{attempts})..."
|
|
594
601
|
begin
|
|
595
602
|
lockfile_reset.reset(Kettle::Dev::LockfileReset::RELEASE_LOCKFILES_TARGET)
|
|
603
|
+
emit_release_lockfile_event(action: "reset", status: "ok", stage: stage, attempt: attempt, attempts: attempts)
|
|
596
604
|
break
|
|
597
605
|
rescue Kettle::Dev::Error => error
|
|
598
606
|
raise unless error.message.start_with?("Reset #{Kettle::Dev::LockfileReset::RELEASE_LOCKFILES_TARGET} failed validation:")
|
|
@@ -600,9 +608,26 @@ module Kettle
|
|
|
600
608
|
# Keep release-facing failures shaped like the lockfile validation
|
|
601
609
|
# guard, even when the shared reset helper is the component that
|
|
602
610
|
# detects the unrepaired lockfile.
|
|
611
|
+
emit_release_lockfile_event(
|
|
612
|
+
action: "reset",
|
|
613
|
+
status: "blocked",
|
|
614
|
+
stage: stage,
|
|
615
|
+
attempt: attempt,
|
|
616
|
+
attempts: attempts,
|
|
617
|
+
reason: error.message
|
|
618
|
+
)
|
|
603
619
|
break
|
|
604
620
|
rescue => error
|
|
605
|
-
|
|
621
|
+
retryable = retryable_release_lockfile_reset_error?(error) && attempt < attempts
|
|
622
|
+
emit_release_lockfile_event(
|
|
623
|
+
action: "reset",
|
|
624
|
+
status: retryable ? "retrying" : "failed",
|
|
625
|
+
stage: stage,
|
|
626
|
+
attempt: attempt,
|
|
627
|
+
attempts: attempts,
|
|
628
|
+
reason: error.message
|
|
629
|
+
)
|
|
630
|
+
raise unless retryable
|
|
606
631
|
|
|
607
632
|
puts "Release lockfile reset could not resolve a gem from #{RELEASE_VALIDATION_SOURCE}; waiting before retry #{attempt + 1}/#{attempts}."
|
|
608
633
|
sleep(release_availability_probe_interval)
|
|
@@ -611,7 +636,16 @@ module Kettle
|
|
|
611
636
|
|
|
612
637
|
diagnostics = release_lockfile_paths.flat_map { |path| release_lockfile_diagnostics(path) }
|
|
613
638
|
if diagnostics.empty?
|
|
639
|
+
emit_release_lockfile_event(action: "validate", status: "ok", stage: stage, count: release_lockfile_paths.length)
|
|
614
640
|
puts "Release lockfile reset complete: #{release_lockfile_paths.length} lockfile(s) checked, no diagnostics remain."
|
|
641
|
+
else
|
|
642
|
+
emit_release_lockfile_event(
|
|
643
|
+
action: "validate",
|
|
644
|
+
status: "failed",
|
|
645
|
+
stage: stage,
|
|
646
|
+
count: diagnostics.length,
|
|
647
|
+
reason: "#{diagnostics.length} diagnostic(s)"
|
|
648
|
+
)
|
|
615
649
|
end
|
|
616
650
|
@release_lockfiles_reset_for_release_tasks = true if stage == "before release task bundle installs"
|
|
617
651
|
end
|
|
@@ -758,6 +792,7 @@ module Kettle
|
|
|
758
792
|
cmd = "bundle exec kettle-changelog"
|
|
759
793
|
cmd = "#{cmd} --version #{Shellwords.escape(@version_override)}" if @version_override
|
|
760
794
|
cmd = "#{cmd} --yes" if @yes
|
|
795
|
+
cmd = "#{cmd} --events=changelog" if @event_stream
|
|
761
796
|
run_cmd!(cmd)
|
|
762
797
|
@changelog_generated_coverage = true
|
|
763
798
|
end
|
|
@@ -1010,12 +1045,24 @@ module Kettle
|
|
|
1010
1045
|
def monitor_workflows_after_push!
|
|
1011
1046
|
ensure_github_pull_request_for_ci!
|
|
1012
1047
|
keep_release_secrets_alive!("CI monitoring")
|
|
1048
|
+
restart_hint = "bundle exec kettle-release start_step=10"
|
|
1049
|
+
emit_ci_monitor_event(action: "start", status: "started", workflows: @ci_workflows, restart_hint: restart_hint)
|
|
1013
1050
|
# Use abort-on-failure CI monitor to match historical behavior and specs
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1051
|
+
begin
|
|
1052
|
+
Kettle::Dev::CIMonitor.monitor_all!(
|
|
1053
|
+
restart_hint: restart_hint,
|
|
1054
|
+
workflows: @ci_workflows,
|
|
1055
|
+
keepalive: release_secrets_configured? ? -> { keep_release_secrets_alive!("CI monitoring") } : nil,
|
|
1056
|
+
event_recorder: @event_recorder
|
|
1057
|
+
)
|
|
1058
|
+
emit_ci_monitor_event(action: "finish", status: "ok", workflows: @ci_workflows, restart_hint: restart_hint)
|
|
1059
|
+
rescue SystemExit => error
|
|
1060
|
+
emit_ci_monitor_event(action: "finish", status: "failed", workflows: @ci_workflows, restart_hint: restart_hint, reason: error.message)
|
|
1061
|
+
raise
|
|
1062
|
+
rescue => error
|
|
1063
|
+
emit_ci_monitor_event(action: "finish", status: "failed", workflows: @ci_workflows, restart_hint: restart_hint, reason: "#{error.class}: #{error.message}")
|
|
1064
|
+
raise
|
|
1065
|
+
end
|
|
1019
1066
|
end
|
|
1020
1067
|
|
|
1021
1068
|
def ensure_github_pull_request_for_ci!
|
|
@@ -1412,6 +1459,7 @@ module Kettle
|
|
|
1412
1459
|
sleep(release_availability_probe_initial_delay)
|
|
1413
1460
|
attempts.times do |index|
|
|
1414
1461
|
attempt = index + 1
|
|
1462
|
+
emit_release_probe_event(action: "availability", status: "started", candidate: candidate, attempt: attempt, attempts: attempts)
|
|
1415
1463
|
puts("Validating #{candidate.gem_name} #{candidate.version} from #{RELEASE_VALIDATION_SOURCE} (attempt #{attempt}/#{attempts})")
|
|
1416
1464
|
stdout_str = nil
|
|
1417
1465
|
stderr_str = nil
|
|
@@ -1420,10 +1468,21 @@ module Kettle
|
|
|
1420
1468
|
stdout_str, stderr_str, status = Open3.capture3(self.class.send(:command_env), Gem.ruby, script_path)
|
|
1421
1469
|
end
|
|
1422
1470
|
$stdout.print(stdout_str) unless stdout_str.to_s.empty?
|
|
1423
|
-
|
|
1471
|
+
if status.success?
|
|
1472
|
+
emit_release_probe_event(action: "availability", status: "ok", candidate: candidate, attempt: attempt, attempts: attempts)
|
|
1473
|
+
return true
|
|
1474
|
+
end
|
|
1424
1475
|
|
|
1425
1476
|
last_stderr = stderr_str
|
|
1426
1477
|
last_status = status
|
|
1478
|
+
emit_release_probe_event(
|
|
1479
|
+
action: "availability",
|
|
1480
|
+
status: (attempt < attempts) ? "retrying" : "failed",
|
|
1481
|
+
candidate: candidate,
|
|
1482
|
+
attempt: attempt,
|
|
1483
|
+
attempts: attempts,
|
|
1484
|
+
reason: "exit #{status.exitstatus}"
|
|
1485
|
+
)
|
|
1427
1486
|
sleep(release_availability_probe_interval) if attempt < attempts
|
|
1428
1487
|
end
|
|
1429
1488
|
|
|
@@ -1590,6 +1649,7 @@ module Kettle
|
|
|
1590
1649
|
name: command_event_name(command),
|
|
1591
1650
|
status: status,
|
|
1592
1651
|
reason: reason,
|
|
1652
|
+
summary: command_event_summary(command),
|
|
1593
1653
|
command: command,
|
|
1594
1654
|
changed_files: []
|
|
1595
1655
|
}
|
|
@@ -1598,7 +1658,80 @@ module Kettle
|
|
|
1598
1658
|
end
|
|
1599
1659
|
|
|
1600
1660
|
def command_event_name(command)
|
|
1601
|
-
|
|
1661
|
+
words = Shellwords.split(command.to_s)
|
|
1662
|
+
effective_words = command_event_effective_words(words)
|
|
1663
|
+
joined = effective_words.join(" ")
|
|
1664
|
+
|
|
1665
|
+
return "kettle_changelog" if joined.start_with?("bundle exec kettle-changelog")
|
|
1666
|
+
return "gem_build" if joined == "bundle exec rake build"
|
|
1667
|
+
return "gem_release" if joined == "bundle exec rake release"
|
|
1668
|
+
return "gem_push" if effective_words.first(2) == ["gem", "push"]
|
|
1669
|
+
return "gem_checksums" if effective_words.first == "bin/gem_checksums"
|
|
1670
|
+
return "bundle_lock" if effective_words.first(2) == ["bundle", "lock"]
|
|
1671
|
+
return "bin_setup" if effective_words == ["bin/setup"]
|
|
1672
|
+
return "default_task" if effective_words == ["bin/rake"]
|
|
1673
|
+
return "appraisal_generate" if effective_words == ["bin/rake", "appraisal:generate"]
|
|
1674
|
+
return "appraisal_update" if effective_words == ["bin/rake", "appraisal:update"]
|
|
1675
|
+
return "yard" if effective_words == ["bin/rake", "yard"]
|
|
1676
|
+
return "git_fetch" if effective_words.first(2) == ["git", "fetch"]
|
|
1677
|
+
return "git_pull" if effective_words.first(2) == ["git", "pull"]
|
|
1678
|
+
return "git_rebase" if effective_words.first(2) == ["git", "rebase"]
|
|
1679
|
+
return "git_merge" if effective_words.first(2) == ["git", "merge"]
|
|
1680
|
+
return "git_push" if effective_words.first(2) == ["git", "push"]
|
|
1681
|
+
|
|
1682
|
+
effective_words.first(2).join("_").gsub(/[^A-Za-z0-9_]+/, "_").sub(/_+\z/, "")
|
|
1683
|
+
end
|
|
1684
|
+
|
|
1685
|
+
def command_event_effective_words(words)
|
|
1686
|
+
words = words.dup
|
|
1687
|
+
words.shift while words.first&.match?(/\A[A-Za-z_][A-Za-z0-9_]*=/)
|
|
1688
|
+
|
|
1689
|
+
if words.first == "env"
|
|
1690
|
+
words.shift
|
|
1691
|
+
while words.any?
|
|
1692
|
+
if words.first == "-u"
|
|
1693
|
+
words.shift(2)
|
|
1694
|
+
elsif words.first&.include?("=")
|
|
1695
|
+
words.shift
|
|
1696
|
+
else
|
|
1697
|
+
break
|
|
1698
|
+
end
|
|
1699
|
+
end
|
|
1700
|
+
end
|
|
1701
|
+
|
|
1702
|
+
words
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
def command_event_summary(command)
|
|
1706
|
+
words = Shellwords.split(command.to_s)
|
|
1707
|
+
effective_words = command_event_effective_words(words)
|
|
1708
|
+
joined = effective_words.join(" ")
|
|
1709
|
+
|
|
1710
|
+
return "changelog" if joined.start_with?("bundle exec kettle-changelog")
|
|
1711
|
+
return command_event_bundle_lock_summary(words) if effective_words.first(2) == ["bundle", "lock"]
|
|
1712
|
+
return "setup" if effective_words == ["bin/setup"]
|
|
1713
|
+
return "default task" if effective_words == ["bin/rake"]
|
|
1714
|
+
return "appraisals" if effective_words == ["bin/rake", "appraisal:generate"]
|
|
1715
|
+
return "appraisals" if effective_words == ["bin/rake", "appraisal:update"]
|
|
1716
|
+
return "documentation" if effective_words == ["bin/rake", "yard"]
|
|
1717
|
+
return "build gem" if joined == "bundle exec rake build"
|
|
1718
|
+
return "publish gem" if joined == "bundle exec rake release"
|
|
1719
|
+
return "push gem" if effective_words.first(2) == ["gem", "push"]
|
|
1720
|
+
return "checksums" if effective_words.first == "bin/gem_checksums"
|
|
1721
|
+
return effective_words[2] if effective_words.first(2) == ["git", "fetch"] && effective_words[2]
|
|
1722
|
+
return effective_words[2] if effective_words.first(2) == ["git", "push"] && effective_words[2]
|
|
1723
|
+
return effective_words[2] if effective_words.first(2) == ["git", "pull"] && effective_words[2]
|
|
1724
|
+
return effective_words[2] if effective_words.first(2) == ["git", "rebase"] && effective_words[2]
|
|
1725
|
+
return effective_words[2] if effective_words.first(2) == ["git", "merge"] && effective_words[2]
|
|
1726
|
+
|
|
1727
|
+
nil
|
|
1728
|
+
end
|
|
1729
|
+
|
|
1730
|
+
def command_event_bundle_lock_summary(words)
|
|
1731
|
+
gemfile = words.find { |word| word.start_with?("BUNDLE_GEMFILE=") }&.split("=", 2)&.last
|
|
1732
|
+
return "lockfile" unless gemfile
|
|
1733
|
+
|
|
1734
|
+
File.basename(gemfile)
|
|
1602
1735
|
end
|
|
1603
1736
|
|
|
1604
1737
|
def record_diagnostic(kind, message, severity:, blocking:)
|
|
@@ -1635,6 +1768,97 @@ module Kettle
|
|
|
1635
1768
|
)
|
|
1636
1769
|
end
|
|
1637
1770
|
|
|
1771
|
+
def emit_remote_parity_event(action:, remote: nil, status: nil, trunk: nil, attempt: nil, attempts: nil, required: nil, reason: nil, remotes: nil)
|
|
1772
|
+
mark = release_progress_mark(status)
|
|
1773
|
+
Kettle::Ndjson.emit_event(
|
|
1774
|
+
@event_recorder,
|
|
1775
|
+
"remote_parity",
|
|
1776
|
+
action: action,
|
|
1777
|
+
phase: "release",
|
|
1778
|
+
remote: remote,
|
|
1779
|
+
status: status,
|
|
1780
|
+
trunk: trunk,
|
|
1781
|
+
attempt: attempt,
|
|
1782
|
+
attempts: attempts,
|
|
1783
|
+
required: required,
|
|
1784
|
+
reason: reason,
|
|
1785
|
+
remotes: remotes,
|
|
1786
|
+
mark: mark
|
|
1787
|
+
)
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
def emit_release_lockfile_event(action:, status:, stage:, attempt: nil, attempts: nil, count: nil, reason: nil)
|
|
1791
|
+
Kettle::Ndjson.emit_event(
|
|
1792
|
+
@event_recorder,
|
|
1793
|
+
"release_lockfile",
|
|
1794
|
+
action: action,
|
|
1795
|
+
phase: "release",
|
|
1796
|
+
status: status,
|
|
1797
|
+
stage: stage,
|
|
1798
|
+
attempt: attempt,
|
|
1799
|
+
attempts: attempts,
|
|
1800
|
+
count: count,
|
|
1801
|
+
reason: reason,
|
|
1802
|
+
mark: release_progress_mark(status)
|
|
1803
|
+
)
|
|
1804
|
+
end
|
|
1805
|
+
|
|
1806
|
+
def emit_release_probe_event(action:, status:, candidate:, attempt:, attempts:, reason: nil)
|
|
1807
|
+
Kettle::Ndjson.emit_event(
|
|
1808
|
+
@event_recorder,
|
|
1809
|
+
"release_probe",
|
|
1810
|
+
action: action,
|
|
1811
|
+
phase: "release",
|
|
1812
|
+
status: status,
|
|
1813
|
+
gem: candidate.gem_name,
|
|
1814
|
+
version: candidate.version,
|
|
1815
|
+
source: RELEASE_VALIDATION_SOURCE,
|
|
1816
|
+
attempt: attempt,
|
|
1817
|
+
attempts: attempts,
|
|
1818
|
+
reason: reason,
|
|
1819
|
+
mark: release_progress_mark(status)
|
|
1820
|
+
)
|
|
1821
|
+
end
|
|
1822
|
+
|
|
1823
|
+
def release_progress_mark(status)
|
|
1824
|
+
case status.to_s
|
|
1825
|
+
when "started"
|
|
1826
|
+
">"
|
|
1827
|
+
when "ok", "skipped"
|
|
1828
|
+
"."
|
|
1829
|
+
when "failed", "blocked"
|
|
1830
|
+
"!"
|
|
1831
|
+
when "retrying"
|
|
1832
|
+
">"
|
|
1833
|
+
else
|
|
1834
|
+
">"
|
|
1835
|
+
end
|
|
1836
|
+
end
|
|
1837
|
+
|
|
1838
|
+
def emit_ci_monitor_event(action:, status:, workflows:, restart_hint:, reason: nil)
|
|
1839
|
+
mark = case status.to_s
|
|
1840
|
+
when "started"
|
|
1841
|
+
">"
|
|
1842
|
+
when "ok"
|
|
1843
|
+
"."
|
|
1844
|
+
when "failed"
|
|
1845
|
+
"!"
|
|
1846
|
+
else
|
|
1847
|
+
"?"
|
|
1848
|
+
end
|
|
1849
|
+
Kettle::Ndjson.emit_event(
|
|
1850
|
+
@event_recorder,
|
|
1851
|
+
"ci_monitor",
|
|
1852
|
+
action: action,
|
|
1853
|
+
phase: "release",
|
|
1854
|
+
status: status,
|
|
1855
|
+
workflows: workflows,
|
|
1856
|
+
restart_hint: restart_hint,
|
|
1857
|
+
reason: reason,
|
|
1858
|
+
mark: mark
|
|
1859
|
+
)
|
|
1860
|
+
end
|
|
1861
|
+
|
|
1638
1862
|
def finish_release_report(status:, error:)
|
|
1639
1863
|
return if @finished_report
|
|
1640
1864
|
|
|
@@ -1798,6 +2022,10 @@ module Kettle
|
|
|
1798
2022
|
@skip_remotes.include?(name)
|
|
1799
2023
|
end
|
|
1800
2024
|
|
|
2025
|
+
def required_remote?(name)
|
|
2026
|
+
@required_remotes.include?(name)
|
|
2027
|
+
end
|
|
2028
|
+
|
|
1801
2029
|
def active_remotes
|
|
1802
2030
|
list_remotes.reject { |remote| skipped_remote?(remote) }
|
|
1803
2031
|
end
|
|
@@ -1836,26 +2064,30 @@ module Kettle
|
|
|
1836
2064
|
if has_remote?("all")
|
|
1837
2065
|
remotes = active_remotes
|
|
1838
2066
|
skipped = list_remotes.select { |remote| skipped_remote?(remote) }
|
|
2067
|
+
required = remotes.select { |remote| required_remote?(remote) }
|
|
1839
2068
|
puts "Remote 'all' detected. Fetching from active remotes and enforcing strict trunk parity..."
|
|
1840
2069
|
puts "Skipping configured remotes: #{skipped.join(", ")}" unless skipped.empty?
|
|
2070
|
+
puts "Required release parity remotes: #{required.join(", ")}" unless required.empty?
|
|
2071
|
+
fetched_remotes = []
|
|
2072
|
+
emit_remote_parity_event(action: "start", status: "started", trunk: trunk, remotes: remotes - ["all"])
|
|
1841
2073
|
remotes.each do |remote|
|
|
1842
2074
|
next if remote == "all"
|
|
1843
2075
|
|
|
1844
|
-
fetch_remote_for_parity!(remote)
|
|
2076
|
+
fetched_remotes << remote if fetch_remote_for_parity!(remote)
|
|
1845
2077
|
end
|
|
1846
2078
|
missing_from = []
|
|
1847
|
-
|
|
1848
|
-
next if r == "all"
|
|
1849
|
-
|
|
2079
|
+
fetched_remotes.each do |r|
|
|
1850
2080
|
if remote_branch_exists?(r, trunk)
|
|
1851
2081
|
_ahead, behind = ahead_behind_counts(trunk, "#{r}/#{trunk}")
|
|
1852
2082
|
missing_from << r if behind.positive?
|
|
1853
2083
|
end
|
|
1854
2084
|
end
|
|
1855
2085
|
unless missing_from.empty?
|
|
2086
|
+
emit_remote_parity_event(action: "missing", status: "failed", trunk: trunk, remotes: missing_from, reason: "missing commits")
|
|
1856
2087
|
abort("Local #{trunk} is missing commits present on: #{missing_from.join(", ")}. Please sync trunk first.")
|
|
1857
2088
|
end
|
|
1858
|
-
puts "Local #{trunk} has all commits from remotes: #{
|
|
2089
|
+
puts "Local #{trunk} has all commits from remotes: #{fetched_remotes.join(", ")}"
|
|
2090
|
+
emit_remote_parity_event(action: "ok", status: "ok", trunk: trunk, remotes: fetched_remotes)
|
|
1859
2091
|
return
|
|
1860
2092
|
end
|
|
1861
2093
|
|
|
@@ -1924,8 +2156,10 @@ module Kettle
|
|
|
1924
2156
|
attempts.times do |index|
|
|
1925
2157
|
attempt = index + 1
|
|
1926
2158
|
puts "Fetching remote '#{remote}' for release parity (attempt #{attempt}/#{attempts})"
|
|
2159
|
+
emit_remote_parity_event(action: "fetch", remote: remote, status: "started", attempt: attempt, attempts: attempts, required: required_remote?(remote))
|
|
1927
2160
|
begin
|
|
1928
2161
|
run_cmd!(command)
|
|
2162
|
+
emit_remote_parity_event(action: "fetch", remote: remote, status: "ok", attempt: attempt, attempts: attempts, required: required_remote?(remote))
|
|
1929
2163
|
return true
|
|
1930
2164
|
rescue SystemExit => error
|
|
1931
2165
|
last_error = error
|
|
@@ -1935,7 +2169,17 @@ module Kettle
|
|
|
1935
2169
|
sleep(remote_fetch_parity_interval) if attempt < attempts
|
|
1936
2170
|
end
|
|
1937
2171
|
|
|
1938
|
-
|
|
2172
|
+
detail = last_error&.message.to_s
|
|
2173
|
+
if required_remote?(remote)
|
|
2174
|
+
emit_remote_parity_event(action: "fetch", remote: remote, status: "failed", attempts: attempts, required: true, reason: detail)
|
|
2175
|
+
abort(remote_fetch_failure_message(remote, detail))
|
|
2176
|
+
end
|
|
2177
|
+
|
|
2178
|
+
message = optional_remote_fetch_failure_message(remote, detail)
|
|
2179
|
+
warn(message)
|
|
2180
|
+
emit_remote_parity_event(action: "skip", remote: remote, status: "skipped", attempts: attempts, required: false, reason: detail)
|
|
2181
|
+
record_diagnostic("release_remote_skipped", message, severity: "warning", blocking: false)
|
|
2182
|
+
false
|
|
1939
2183
|
end
|
|
1940
2184
|
|
|
1941
2185
|
def remote_fetch_parity_attempts
|
|
@@ -1948,7 +2192,7 @@ module Kettle
|
|
|
1948
2192
|
|
|
1949
2193
|
def remote_fetch_failure_message(remote, detail)
|
|
1950
2194
|
<<~MSG
|
|
1951
|
-
Unable to fetch git remote '#{remote}' during release parity checks.
|
|
2195
|
+
Unable to fetch required git remote '#{remote}' during release parity checks.
|
|
1952
2196
|
kettle-release enforces trunk parity across active remotes before publishing.
|
|
1953
2197
|
|
|
1954
2198
|
If this remote is temporarily unavailable or intentionally not part of this release,
|
|
@@ -1961,6 +2205,20 @@ module Kettle
|
|
|
1961
2205
|
MSG
|
|
1962
2206
|
end
|
|
1963
2207
|
|
|
2208
|
+
def optional_remote_fetch_failure_message(remote, detail)
|
|
2209
|
+
<<~MSG
|
|
2210
|
+
Unable to fetch optional git remote '#{remote}' during release parity checks; skipping this remote for this release.
|
|
2211
|
+
Required remotes still block release parity checks.
|
|
2212
|
+
|
|
2213
|
+
To make this remote block releases, configure:
|
|
2214
|
+
kettle-release --required-remotes #{remote}
|
|
2215
|
+
K_RELEASE_REQUIRED_REMOTES=#{remote} kettle-release
|
|
2216
|
+
|
|
2217
|
+
Original failure:
|
|
2218
|
+
#{detail}
|
|
2219
|
+
MSG
|
|
2220
|
+
end
|
|
2221
|
+
|
|
1964
2222
|
def merge_feature_into_trunk_and_push!(trunk, feature)
|
|
1965
2223
|
return if feature.nil? || feature == trunk
|
|
1966
2224
|
|
data/lib/kettle/dev/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Kettle
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = "2.5.
|
|
8
|
+
VERSION = "2.5.13"
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
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.5.
|
|
4
|
+
version: 2.5.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -80,7 +80,7 @@ dependencies:
|
|
|
80
80
|
version: '0.1'
|
|
81
81
|
- - ">="
|
|
82
82
|
- !ruby/object:Gem::Version
|
|
83
|
-
version: 0.1.
|
|
83
|
+
version: 0.1.9
|
|
84
84
|
type: :runtime
|
|
85
85
|
prerelease: false
|
|
86
86
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -90,7 +90,7 @@ dependencies:
|
|
|
90
90
|
version: '0.1'
|
|
91
91
|
- - ">="
|
|
92
92
|
- !ruby/object:Gem::Version
|
|
93
|
-
version: 0.1.
|
|
93
|
+
version: 0.1.9
|
|
94
94
|
- !ruby/object:Gem::Dependency
|
|
95
95
|
name: kettle-rb
|
|
96
96
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -401,10 +401,10 @@ licenses:
|
|
|
401
401
|
- AGPL-3.0-only
|
|
402
402
|
metadata:
|
|
403
403
|
homepage_uri: https://kettle-dev.galtzo.com
|
|
404
|
-
source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.5.
|
|
405
|
-
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.5.
|
|
404
|
+
source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.5.13
|
|
405
|
+
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.5.13/CHANGELOG.md
|
|
406
406
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
|
|
407
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.5.
|
|
407
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.5.13
|
|
408
408
|
funding_uri: https://github.com/sponsors/pboling
|
|
409
409
|
wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
|
|
410
410
|
news_uri: https://www.railsbling.com/tags/kettle-dev
|
metadata.gz.sig
CHANGED
|
Binary file
|