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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60069f78cc45c33bf7d2616ea76c456e98cf9885da75331a553f5c9e2b96bbe8
4
- data.tar.gz: d58499d2f31cbffc408e15af977b93b3254f310d021822d90726c7407f4436c3
3
+ metadata.gz: fd42849120d31c48755d543918cd96ea2abb0f61d550983853ae6827940099f6
4
+ data.tar.gz: 5bfc7a99f63937031b7999db6d6a35aa9583f7fc3547db425196fb18d3819458
5
5
  SHA512:
6
- metadata.gz: b34e918fa0cce724b098ab97e46abd768a65938d52f908f87515a5f64b8c3c8d68e6717c05529045da5ce1670e407fa1fa955cfb3c9c2cbd6a4c72d46b64577b
7
- data.tar.gz: d9dcb4194c6f22c6f6ba1b3311b8f7e644766b52bb36f80d4f59130e48f32d2d4917541c3aebf25e253adb4e3914485b9d66b7f77ebf64b73e56804686dc4276
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.12...HEAD
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.408-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
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
- Kettle::Dev::ExecutableVersion.print_header(script_basename) unless ARGV.include?("--json")
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
- 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)
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] = true
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
- url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
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