Module: Kettle::Dev::CIMonitor
- Defined in:
- lib/kettle/dev/ci_monitor.rb
Overview
CIMonitor centralizes CI monitoring logic (GitHub Actions and GitLab pipelines)
so it can be reused by both kettle-release and Rake tasks (e.g., ci:act).
Public API is intentionally small and based on environment/project introspection
via CIHelpers, matching the behavior historically implemented in ReleaseCLI.
Class Method Summary collapse
-
.abort(msg) ⇒ Object
Abort helper (delegates through ExitAdapter so specs can trap exits).
-
.collect_all ⇒ Hash
Non-aborting collection across GH and GL, returning a compact results hash.
-
.collect_github ⇒ Object
— Collectors —.
-
.collect_gitlab ⇒ Object
-
.github_remote_candidates ⇒ Object
-
.gitlab_remote_candidates ⇒ Object
-
.monitor_all!(restart_hint: "bundle exec kettle-release start_step=10") ⇒ void
Monitor both GitHub and GitLab CI for the current project/branch.
-
.monitor_and_prompt_for_release!(restart_hint: "bundle exec kettle-release start_step=10") ⇒ void
Prompt user to continue or quit when failures are present; otherwise return.
-
.monitor_github_internal!(restart_hint:) ⇒ Object
– internals (abort-on-failure legacy paths used elsewhere) –.
-
.monitor_gitlab!(restart_hint: "bundle exec kettle-release start_step=10") ⇒ Boolean
Public wrapper to monitor GitLab pipeline with abort-on-failure semantics.
-
.monitor_gitlab_internal!(restart_hint:) ⇒ Object
-
.parse_github_owner_repo(url) ⇒ Object
-
.preferred_github_remote ⇒ Object
-
.remote_url(name) ⇒ Object
-
.remotes_with_urls ⇒ Object
– tiny wrappers around GitAdapter-like helpers used by ReleaseCLI –.
-
.status_emoji(status, conclusion) ⇒ String
Small helper to map CI run status/conclusion to an emoji.
-
.summarize_results(results) ⇒ Boolean
Print a concise summary like ci:act and return whether everything is green.
Class Method Details
.abort(msg) ⇒ Object
Abort helper (delegates through ExitAdapter so specs can trap exits)
19 20 21 |
# File 'lib/kettle/dev/ci_monitor.rb', line 19 def abort(msg) Kettle::Dev::ExitAdapter.abort(msg) end |
.collect_all ⇒ Hash
Non-aborting collection across GH and GL, returning a compact results hash.
Results format:
/>
github: [ {workflow: “file.yml”, status: “completed”, conclusion: “success”|”failure”|nil, url: String ],
gitlab: { status: “success”|”failed”|”blocked”|”unknown”|nil, url: String }
}
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/kettle/dev/ci_monitor.rb', line 74 def collect_all results = {github: [], gitlab: nil} begin gh = collect_github results[:github] = gh if gh rescue StandardError => e Kettle::Dev.debug_error(e, __method__) end begin gl = collect_gitlab results[:gitlab] = gl if gl rescue StandardError => e Kettle::Dev.debug_error(e, __method__) end results end |
.collect_github ⇒ Object
— Collectors —
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/kettle/dev/ci_monitor.rb', line 164 def collect_github root = Kettle::Dev::CIHelpers.project_root workflows = Kettle::Dev::CIHelpers.workflows_list(root) gh_remote = preferred_github_remote return unless gh_remote && !workflows.empty? branch = Kettle::Dev::CIHelpers.current_branch abort("Could not determine current branch for CI checks.") unless branch url = remote_url(gh_remote) owner, repo = parse_github_owner_repo(url) return unless owner && repo total = workflows.size return [] if total.zero? puts "Checking GitHub Actions workflows on #{branch} (#{owner}/#{repo}) via remote '#{gh_remote}'" = if defined?(ProgressBar) ProgressBar.create(title: "GHA", total: total, format: "%t %b %c/%C", length: 30) end # Initial sleep same as aborting path begin initial_sleep = Integer(ENV["K_RELEASE_CI_INITIAL_SLEEP"]) rescue initial_sleep = nil end sleep((initial_sleep && initial_sleep >= 0) ? initial_sleep : 3) results = {} idx = 0 loop do wf = workflows[idx] run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch) if run if Kettle::Dev::CIHelpers.success?(run) unless results[wf] status = run["status"] || "completed" conclusion = run["conclusion"] || "success" results[wf] = {workflow: wf, status: status, conclusion: conclusion, url: run["html_url"]} &.increment end elsif Kettle::Dev::CIHelpers.failed?(run) unless results[wf] results[wf] = {workflow: wf, status: run["status"], conclusion: run["conclusion"] || "failure", url: run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}"} &.increment end end end break if results.size == total idx = (idx + 1) % total sleep(1) end &.finish unless &.finished? results.values end |
.collect_gitlab ⇒ Object
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
# File 'lib/kettle/dev/ci_monitor.rb', line 222 def collect_gitlab root = Kettle::Dev::CIHelpers.project_root gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml")) gl_remote = gitlab_remote_candidates.first return unless gitlab_ci && gl_remote branch = Kettle::Dev::CIHelpers.current_branch abort("Could not determine current branch for CI checks.") unless branch owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab return unless owner && repo puts "Checking GitLab pipeline on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'" = if defined?(ProgressBar) ProgressBar.create(title: "GL", total: 1, format: "%t %b %c/%C", length: 30) end result = {status: "unknown", url: nil} loop do pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch) if pipe result[:url] ||= pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines" if Kettle::Dev::CIHelpers.gitlab_success?(pipe) result[:status] = "success" &.increment unless &.finished? elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe) reason = (pipe["failure_reason"] || "").to_s if reason =~ /insufficient|quota|minute/i result[:status] = "unknown" &.finish unless &.finished? else result[:status] = "failed" &.increment unless &.finished? end elsif pipe["status"] == "blocked" result[:status] = "blocked" &.finish unless &.finished? end break end sleep(1) end &.finish unless &.finished? result end |
.github_remote_candidates ⇒ Object
388 389 390 |
# File 'lib/kettle/dev/ci_monitor.rb', line 388 def github_remote_candidates remotes_with_urls.select { |n, u| u.include?("github.com") }.keys end |
.gitlab_remote_candidates ⇒ Object
393 394 395 |
# File 'lib/kettle/dev/ci_monitor.rb', line 393 def gitlab_remote_candidates remotes_with_urls.select { |n, u| u.include?("gitlab.com") }.keys end |
.monitor_all!(restart_hint: "bundle exec kettle-release start_step=10") ⇒ void
This method returns an undefined value.
Monitor both GitHub and GitLab CI for the current project/branch.
This mirrors ReleaseCLI behavior and aborts on first failure.
50 51 52 53 54 55 |
# File 'lib/kettle/dev/ci_monitor.rb', line 50 def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10") checks_any = false checks_any |= monitor_github_internal!(restart_hint: restart_hint) checks_any |= monitor_gitlab_internal!(restart_hint: restart_hint) abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any end |
.monitor_and_prompt_for_release!(restart_hint: "bundle exec kettle-release start_step=10") ⇒ void
This method returns an undefined value.
Prompt user to continue or quit when failures are present; otherwise return.
Designed for kettle-release.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/kettle/dev/ci_monitor.rb', line 128 def monitor_and_prompt_for_release!(restart_hint: "bundle exec kettle-release start_step=10") results = collect_all any_checks = !(results[:github].nil? || results[:github].empty?) || !!results[:gitlab] abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless any_checks ok = summarize_results(results) return if ok # Non-interactive environments default to quitting unless explicitly allowed env_val = ENV.fetch("K_RELEASE_CI_CONTINUE", "false") non_interactive_continue = !!(Kettle::Dev::ENV_TRUE_RE =~ env_val) if !$stdin.tty? abort("CI checks reported failures. Fix and restart from CI validation (#{restart_hint}).") unless non_interactive_continue puts "CI checks reported failures, but continuing due to K_RELEASE_CI_CONTINUE=true." return end # Prompt exactly once; avoid repeated printing in case of unexpected input buffering. # Accept c/continue to proceed or q/quit to abort. Any other input defaults to quit with a message. print("One or more CI checks failed. (c)ontinue or (q)uit? ") ans = Kettle::Dev::InputAdapter.gets if ans.nil? abort("Aborting (no input available). Fix CI, then restart with: #{restart_hint}") end ans = ans.strip.downcase if ans == "c" || ans == "continue" puts "Continuing release despite CI failures." elsif ans == "q" || ans == "quit" abort("Aborting per user choice. Fix CI, then restart with: #{restart_hint}") else abort("Unrecognized input '#{ans}'. Aborting. Fix CI, then restart with: #{restart_hint}") end end |
.monitor_github_internal!(restart_hint:) ⇒ Object
– internals (abort-on-failure legacy paths used elsewhere) –
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 |
# File 'lib/kettle/dev/ci_monitor.rb', line 270 def monitor_github_internal!(restart_hint:) root = Kettle::Dev::CIHelpers.project_root workflows = Kettle::Dev::CIHelpers.workflows_list(root) gh_remote = preferred_github_remote return false unless gh_remote && !workflows.empty? branch = Kettle::Dev::CIHelpers.current_branch abort("Could not determine current branch for CI checks.") unless branch url = remote_url(gh_remote) owner, repo = parse_github_owner_repo(url) return false unless owner && repo total = workflows.size abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero? passed = {} puts "Ensuring GitHub Actions workflows pass on #{branch} (#{owner}/#{repo}) via remote '#{gh_remote}'" = if defined?(ProgressBar) ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30) end # Small initial delay to allow GitHub to register the newly pushed commit and enqueue workflows. # Configurable via K_RELEASE_CI_INITIAL_SLEEP (seconds); defaults to 3s. begin initial_sleep = begin Integer(ENV["K_RELEASE_CI_INITIAL_SLEEP"]) rescue nil end end sleep((initial_sleep && initial_sleep >= 0) ? initial_sleep : 3) idx = 0 loop do wf = workflows[idx] run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch) if run if Kettle::Dev::CIHelpers.success?(run) unless passed[wf] passed[wf] = true &.increment end elsif Kettle::Dev::CIHelpers.failed?(run) puts wf_url = run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}" abort("Workflow failed: #{wf} -> #{wf_url} Fix the workflow, then restart this tool from CI validation with: #{restart_hint}") end end break if passed.size == total idx = (idx + 1) % total sleep(1) end &.finish unless &.finished? puts "\nAll GitHub workflows passing (#{passed.size}/#{total})." true end |
.monitor_gitlab!(restart_hint: "bundle exec kettle-release start_step=10") ⇒ Boolean
Public wrapper to monitor GitLab pipeline with abort-on-failure semantics.
Matches RBS and call sites expecting ::monitor_gitlab!
Returns false when GitLab is not configured for this repo/branch.
62 63 64 |
# File 'lib/kettle/dev/ci_monitor.rb', line 62 def monitor_gitlab!(restart_hint: "bundle exec kettle-release start_step=10") monitor_gitlab_internal!(restart_hint: restart_hint) end |
.monitor_gitlab_internal!(restart_hint:) ⇒ Object
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
# File 'lib/kettle/dev/ci_monitor.rb', line 328 def monitor_gitlab_internal!(restart_hint:) root = Kettle::Dev::CIHelpers.project_root gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml")) gl_remote = gitlab_remote_candidates.first return false unless gitlab_ci && gl_remote branch = Kettle::Dev::CIHelpers.current_branch abort("Could not determine current branch for CI checks.") unless branch owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab return false unless owner && repo puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'" = if defined?(ProgressBar) ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30) end loop do pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch) if pipe if Kettle::Dev::CIHelpers.gitlab_success?(pipe) &.increment unless &.finished? break elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe) # Special-case: if failure is due to exhausted minutes/insufficient quota, treat as unknown and continue reason = (pipe["failure_reason"] || "").to_s if reason =~ /insufficient|quota|minute/i puts "\nGitLab reports pipeline cannot run due to quota/minutes exhaustion. Result is unknown; continuing." &.finish unless &.finished? break else puts url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines" abort("Pipeline failed: #{url} Fix the pipeline, then restart this tool from CI validation with: #{restart_hint}") end elsif pipe["status"] == "blocked" # Blocked pipeline (e.g., awaiting approvals) — treat as unknown and continue puts "\nGitLab pipeline is blocked. Result is unknown; continuing." &.finish unless &.finished? break end end sleep(1) end &.finish unless &.finished? puts "\nGitLab pipeline passing." true end |
.parse_github_owner_repo(url) ⇒ Object
410 411 412 413 414 415 416 417 418 419 420 |
# File 'lib/kettle/dev/ci_monitor.rb', line 410 def parse_github_owner_repo(url) return [nil, nil] unless url if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$} [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")] elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$} [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")] else [nil, nil] end end |
.preferred_github_remote ⇒ Object
398 399 400 401 402 403 404 405 406 407 |
# File 'lib/kettle/dev/ci_monitor.rb', line 398 def preferred_github_remote cands = github_remote_candidates return if cands.empty? explicit = cands.find { |n| n == "github" } || cands.find { |n| n == "gh" } return explicit if explicit return "origin" if cands.include?("origin") cands.first end |
.remote_url(name) ⇒ Object
383 384 385 |
# File 'lib/kettle/dev/ci_monitor.rb', line 383 def remote_url(name) Kettle::Dev::GitAdapter.new.remote_url(name) end |
.remotes_with_urls ⇒ Object
– tiny wrappers around GitAdapter-like helpers used by ReleaseCLI –
378 379 380 |
# File 'lib/kettle/dev/ci_monitor.rb', line 378 def remotes_with_urls Kettle::Dev::GitAdapter.new.remotes_with_urls end |
.status_emoji(status, conclusion) ⇒ String
Small helper to map CI run status/conclusion to an emoji.
Reused by ci:act and release summary.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/kettle/dev/ci_monitor.rb', line 29 def status_emoji(status, conclusion) case status.to_s when "queued" then "⏳️" when "in_progress", "running" then "👟" when "completed" (conclusion.to_s == "success") ? "✅" : "🍅" else # Some APIs report only a final state string like "success"/"failed" return "✅" if conclusion.to_s == "success" || status.to_s == "success" return "🍅" if conclusion.to_s == "failure" || status.to_s == "failed" "⏳️" end end |
.summarize_results(results) ⇒ Boolean
Print a concise summary like ci:act and return whether everything is green.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/kettle/dev/ci_monitor.rb', line 95 def summarize_results(results) all_ok = true gh_items = results[:github] || [] unless gh_items.empty? puts "GitHub Actions:" gh_items.each do |it| emoji = status_emoji(it[:status], it[:conclusion]) details = [it[:status], it[:conclusion]].compact.join("/") wf = it[:workflow] puts " - #{wf}: #{emoji} (#{details}) #{"-> #{it[:url]}" if it[:url]}" all_ok &&= (it[:conclusion] == "success") end end gl = results[:gitlab] if gl status = if gl[:status] == "success" "success" else ((gl[:status] == "failed") ? "failure" : nil) end emoji = status_emoji(gl[:status], status) details = gl[:status].to_s puts "GitLab Pipeline: #{emoji} (#{details}) #{"-> #{gl[:url]}" if gl[:url]}" all_ok &&= (gl[:status] != "failed") end all_ok end |