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

Class Method Details

.abort(msg) ⇒ Object

Abort helper (delegates through ExitAdapter so specs can trap exits)



24
25
26
# File 'lib/kettle/dev/ci_monitor.rb', line 24

def abort(msg)
  Kettle::Dev::ExitAdapter.abort(msg)
end

.github_remote_candidatesObject



169
170
171
# File 'lib/kettle/dev/ci_monitor.rb', line 169

def github_remote_candidates
  remotes_with_urls.select { |n, u| u.include?("github.com") }.keys
end

.gitlab_remote_candidatesObject



174
175
176
# File 'lib/kettle/dev/ci_monitor.rb', line 174

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.

Parameters:

  • restart_hint (String) (defaults to: "bundle exec kettle-release start_step=10")

    guidance command shown on failure



34
35
36
37
38
39
# File 'lib/kettle/dev/ci_monitor.rb', line 34

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_github_internal!(restart_hint:) ⇒ Object

– internals –



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/kettle/dev/ci_monitor.rb', line 52

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}'"
  pbar = 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
          pbar&.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
  pbar&.finish unless pbar&.finished?
  puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
  true
end

.monitor_gitlab!(restart_hint: "bundle exec rake ci:act") ⇒ Boolean

Monitor only the GitLab pipeline for current project/branch.
Used by ci:act after running ‘act’.

Parameters:

  • restart_hint (String) (defaults to: "bundle exec rake ci:act")

    guidance command shown on failure

Returns:

  • (Boolean)

    true if check performed (gitlab configured), false otherwise



46
47
48
# File 'lib/kettle/dev/ci_monitor.rb', line 46

def monitor_gitlab!(restart_hint: "bundle exec rake ci:act")
  monitor_gitlab_internal!(restart_hint: restart_hint)
end

.monitor_gitlab_internal!(restart_hint:) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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
# File 'lib/kettle/dev/ci_monitor.rb', line 109

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}'"
  pbar = 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)
        pbar&.increment unless pbar&.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."
          pbar&.finish unless pbar&.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."
        pbar&.finish unless pbar&.finished?
        break
      end
    end
    sleep(1)
  end
  pbar&.finish unless pbar&.finished?
  puts "\nGitLab pipeline passing."
  true
end

.parse_github_owner_repo(url) ⇒ Object



189
190
191
192
193
194
195
196
197
198
# File 'lib/kettle/dev/ci_monitor.rb', line 189

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_remoteObject



179
180
181
182
183
184
185
186
# File 'lib/kettle/dev/ci_monitor.rb', line 179

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



164
165
166
# File 'lib/kettle/dev/ci_monitor.rb', line 164

def remote_url(name)
  Kettle::Dev::GitAdapter.new.remote_url(name)
end

.remotes_with_urlsObject

– tiny wrappers around GitAdapter-like helpers used by ReleaseCLI –



159
160
161
# File 'lib/kettle/dev/ci_monitor.rb', line 159

def remotes_with_urls
  Kettle::Dev::GitAdapter.new.remotes_with_urls
end