Class: ClaudeTaskMaster::GitHub

Inherits:
Object
  • Object
show all
Defined in:
lib/claude_task_master/github.rb

Overview

GitHub operations via gh CLI and Octokit Handles PR creation, CI status, comments, and merging

Class Method Summary collapse

Class Method Details

.actionable_comments(pr_number) ⇒ Object

Get actionable comments (unresolved + actionable severity)



142
143
144
145
146
147
148
149
# File 'lib/claude_task_master/github.rb', line 142

def actionable_comments(pr_number)
  comments = pr_comments(pr_number)
  unresolved_ids = unresolved_threads(pr_number).map { |t| t[:id] }

  comments.select do |comment|
    comment.actionable? || unresolved_ids.include?(comment.id)
  end
end

.available?Boolean

Check if gh CLI is available and authenticated

Returns:

  • (Boolean)


13
14
15
16
# File 'lib/claude_task_master/github.rb', line 13

def available?
  _, status = Open3.capture2('gh auth status')
  status.success?
end

.clientObject

Get Octokit client using gh token



19
20
21
22
23
24
25
26
# File 'lib/claude_task_master/github.rb', line 19

def client
  @client ||= begin
    token = gh_token
    raise ConfigError, 'GitHub token not found. Run: gh auth login' unless token

    Octokit::Client.new(access_token: token, auto_paginate: true)
  end
end

.create_pr(title:, body:, base: 'main', head: nil) ⇒ Object

Create a PR Returns [success, pr_number_or_error]



45
46
47
48
49
50
51
52
53
54
# File 'lib/claude_task_master/github.rb', line 45

def create_pr(title:, body:, base: 'main', head: nil)
  head ||= current_branch
  repo = current_repo
  return [false, 'Not in a git repository'] unless repo

  pr = client.create_pull_request(repo, base, head, title, body)
  [true, pr.number]
rescue Octokit::Error => e
  [false, e.message]
end

.current_repoObject

Get current repository (owner/repo format)



34
35
36
37
38
39
40
41
# File 'lib/claude_task_master/github.rb', line 34

def current_repo
  @current_repo ||= begin
    stdout, status = Open3.capture2('gh repo view --json nameWithOwner -q .nameWithOwner')
    return nil unless status.success?

    stdout.strip
  end
end

.merge_pr(pr_number, method: :squash, delete_branch: true) ⇒ Object

Merge PR



197
198
199
200
201
202
203
204
205
206
207
# File 'lib/claude_task_master/github.rb', line 197

def merge_pr(pr_number, method: :squash, delete_branch: true)
  repo = current_repo
  return false unless repo

  client.merge_pull_request(repo, pr_number, '', merge_method: method)
  client.delete_branch(repo, pr_branch_name(pr_number)) if delete_branch
  true
rescue Octokit::Error => e
  warn "Failed to merge: #{e.message}"
  false
end

.open_prsObject

List open PRs



230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/claude_task_master/github.rb', line 230

def open_prs
  repo = current_repo
  return [] unless repo

  client.pull_requests(repo, state: 'open').map do |pr|
    {
      number: pr.number,
      title: pr.title,
      head_ref: pr.head.ref
    }
  end
rescue Octokit::Error
  []
end

.pr_comments(pr_number) ⇒ Object

Get all PR review comments



82
83
84
85
86
87
88
89
90
91
# File 'lib/claude_task_master/github.rb', line 82

def pr_comments(pr_number)
  repo = current_repo
  return [] unless repo

  comments = client.pull_request_comments(repo, pr_number)
  PRComment.from_api_response(comments.map(&:to_h))
rescue Octokit::Error => e
  warn "Failed to fetch comments: #{e.message}"
  []
end

.pr_info(pr_number) ⇒ Object

Get PR info



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/claude_task_master/github.rb', line 210

def pr_info(pr_number)
  repo = current_repo
  return nil unless repo

  pr = client.pull_request(repo, pr_number)
  {
    number: pr.number,
    title: pr.title,
    state: pr.state,
    url: pr.html_url,
    head_ref: pr.head.ref,
    base_ref: pr.base.ref,
    mergeable: pr.mergeable,
    merged: pr.merged
  }
rescue Octokit::Error
  nil
end

.pr_status(pr_number) ⇒ Object

Get PR status (CI checks) Returns hash with :status (:pending, :passing, :failing) and :checks array



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/claude_task_master/github.rb', line 58

def pr_status(pr_number)
  repo = current_repo
  return { status: :unknown, checks: [] } unless repo

  # Get check runs for the PR's head SHA
  pr = client.pull_request(repo, pr_number)
  checks = client.check_runs_for_ref(repo, pr.head.sha)

  check_results = checks.check_runs.map do |run|
    {
      name: run.name,
      status: run.status,
      conclusion: run.conclusion
    }
  end

  overall = determine_ci_status(check_results)
  { status: overall, checks: check_results }
rescue Octokit::Error
  # Fallback to gh CLI
  gh_pr_status(pr_number)
end

.reply_to_comment(pr_number, comment_id, body) ⇒ Object

Reply to a PR comment



173
174
175
176
177
178
179
180
181
182
# File 'lib/claude_task_master/github.rb', line 173

def reply_to_comment(pr_number, comment_id, body)
  repo = current_repo
  return false unless repo

  client.create_pull_request_comment_reply(repo, pr_number, body, comment_id)
  true
rescue Octokit::Error => e
  warn "Failed to reply: #{e.message}"
  false
end

.reset_client!Object

Reset client (useful for testing)



29
30
31
# File 'lib/claude_task_master/github.rb', line 29

def reset_client!
  @client = nil
end

.resolve_thread(thread_id) ⇒ Object

Resolve a review thread

Raises:



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/claude_task_master/github.rb', line 152

def resolve_thread(thread_id)
  mutation = <<~GRAPHQL
    mutation {
      resolveReviewThread(input: {threadId: "#{thread_id}"}) {
        thread {
          id
          isResolved
        }
      }
    }
  GRAPHQL

  response = client.post('/graphql', { query: mutation }.to_json)
  errors = response[:errors]

  return true unless errors

  raise GitHubError, errors.map { |e| e[:message] }.join(', ')
end

.unresolved_threads(pr_number) ⇒ Object

Get unresolved review threads via GraphQL



94
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/claude_task_master/github.rb', line 94

def unresolved_threads(pr_number)
  repo = current_repo
  return [] unless repo

  owner, name = repo.split('/')

  query = <<~GRAPHQL
    query {
      repository(owner: "#{owner}", name: "#{name}") {
        pullRequest(number: #{pr_number}) {
          reviewThreads(first: 100) {
            nodes {
              id
              isResolved
              comments(first: 10) {
                nodes {
                  body
                  author { login }
                  path
                  line
                }
              }
            }
          }
        }
      }
    }
  GRAPHQL

  response = client.post('/graphql', { query: query }.to_json)
  threads = response.dig(:data, :repository, :pullRequest, :reviewThreads, :nodes) || []

  threads.reject { |t| t[:isResolved] }.map do |thread|
    first_comment = thread.dig(:comments, :nodes)&.first
    {
      id: thread[:id],
      author: first_comment&.dig(:author, :login),
      body: first_comment&.dig(:body),
      file_path: first_comment&.dig(:path),
      line: first_comment&.dig(:line)
    }
  end
rescue Octokit::Error, StandardError => e
  # Fallback to gh CLI
  gh_unresolved_threads(pr_number)
end

.wait_for_ci(pr_number, timeout: 600) ⇒ Object

Wait for CI to complete (blocking)



185
186
187
188
189
190
191
192
193
194
# File 'lib/claude_task_master/github.rb', line 185

def wait_for_ci(pr_number, timeout: 600)
  cmd = ['gh', 'pr', 'checks', pr_number.to_s, '--watch', '--fail-fast']

  Timeout.timeout(timeout) do
    _, status = Open3.capture2(*cmd)
    status.success? ? :passing : :failing
  end
rescue Timeout::Error
  :timeout
end