kettle-dev 2.5.12 → 2.5.14
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 +60 -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/lockfile_reset.rb +12 -8
- 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 +3 -3
- 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: fd42849120d31c48755d543918cd96ea2abb0f61d550983853ae6827940099f6
|
|
4
|
+
data.tar.gz: 5bfc7a99f63937031b7999db6d6a35aa9583f7fc3547db425196fb18d3819458
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91ae2809e3092c93f6feaa5b20f4dbc562497917244f95f388ababfe72403f9af76f924314a561c85dc39f8e0f987ed4d19580675612bb01c117fa1e8f1be58e
|
|
7
|
+
data.tar.gz: 0c77a71e021fe4657903384d45ecf8da00c136b78bc0a6cb7531ecc584606bb9fa7abd0ad7a683ea844ee6d3a0be59ecfe388c3cdce1dafbf65989a72b4b6063
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,61 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [2.5.14] - 2026-07-31
|
|
34
|
+
|
|
35
|
+
- TAG: [v2.5.14][2.5.14t]
|
|
36
|
+
- COVERAGE: 91.94% -- 5146/5597 lines in 46 files
|
|
37
|
+
- BRANCH COVERAGE: 76.13% -- 1946/2556 branches in 46 files
|
|
38
|
+
- 50.41% documented
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- `kettle-reset release-lockfiles` now requires SHA-256 checksums only for
|
|
43
|
+
registry-sourced gems, allowing valid Git-sourced dependencies such as the
|
|
44
|
+
`simplecov` fork to remain checksumless.
|
|
45
|
+
|
|
46
|
+
## [2.5.13] - 2026-07-31
|
|
47
|
+
|
|
48
|
+
- TAG: [v2.5.13][2.5.13t]
|
|
49
|
+
- COVERAGE: 91.81% -- 5137/5595 lines in 46 files
|
|
50
|
+
- BRANCH COVERAGE: 76.06% -- 1944/2556 branches in 46 files
|
|
51
|
+
- 50.41% documented
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- `kettle-release` now treats `origin` as the default required parity remote
|
|
56
|
+
while allowing optional mirror remotes to be skipped after fetch retries,
|
|
57
|
+
so a transient Codeberg/GitLab mirror outage does not block publishing.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- `kettle-release --events` now emits `remote_parity` events for remote parity
|
|
62
|
+
fetch attempts, skipped optional remotes, required failures, and parity
|
|
63
|
+
completion.
|
|
64
|
+
- `kettle-release --events` now emits `ci_monitor` events around CI monitoring
|
|
65
|
+
start, per-workflow GitHub Actions progress, GitLab pipeline outcomes,
|
|
66
|
+
success, and failure.
|
|
67
|
+
- `kettle-release --events` now emits `pre_release` events around
|
|
68
|
+
`kettle-pre-release` checks and selected check summaries.
|
|
69
|
+
- `kettle-changelog --events` now emits `changelog` events around release plan,
|
|
70
|
+
coverage generation, and changelog update activity, and `kettle-release
|
|
71
|
+
--events` forwards those child events.
|
|
72
|
+
- `kettle-release --events` now emits `release_lockfile` and `release_probe`
|
|
73
|
+
events for release lockfile reset/validation and published-gem availability
|
|
74
|
+
probes.
|
|
75
|
+
- `kettle-dev` now requires `kettle-ndjson` 0.1.9 or newer so release event
|
|
76
|
+
streams resolve against the protocol that includes remote parity and CI
|
|
77
|
+
monitor, pre-release, changelog, release lockfile, and release probe events.
|
|
78
|
+
|
|
79
|
+
- `kettle-release --events` now emits GitHub CI wait/start/tick monitor events
|
|
80
|
+
while waiting for workflow runs to appear and complete.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- `kettle-release --events` now emits clearer `command_step` names and compact
|
|
85
|
+
per-command summaries for common release commands instead of shell-derived
|
|
86
|
+
labels such as `env__u`.
|
|
87
|
+
|
|
33
88
|
## [2.5.12] - 2026-07-31
|
|
34
89
|
|
|
35
90
|
- TAG: [v2.5.12][2.5.12t]
|
|
@@ -3118,7 +3173,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
3118
3173
|
- Selecting will run the selected workflow via `act`
|
|
3119
3174
|
- This may move to its own gem in the future.
|
|
3120
3175
|
|
|
3121
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.
|
|
3176
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.14...HEAD
|
|
3177
|
+
[2.5.14]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.13...v2.5.14
|
|
3178
|
+
[2.5.14t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.14
|
|
3179
|
+
[2.5.13]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.12...v2.5.13
|
|
3180
|
+
[2.5.13t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.13
|
|
3122
3181
|
[2.5.12]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.11...v2.5.12
|
|
3123
3182
|
[2.5.12t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.12
|
|
3124
3183
|
[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.597-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
|