Class: ClaudeTaskMaster::GitHub
- Inherits:
-
Object
- Object
- ClaudeTaskMaster::GitHub
- 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
-
.actionable_comments(pr_number) ⇒ Object
Get actionable comments (unresolved + actionable severity).
-
.available? ⇒ Boolean
Check if gh CLI is available and authenticated.
-
.client ⇒ Object
Get Octokit client using gh token.
-
.create_pr(title:, body:, base: 'main', head: nil) ⇒ Object
Create a PR Returns [success, pr_number_or_error].
-
.current_repo ⇒ Object
Get current repository (owner/repo format).
-
.merge_pr(pr_number, method: :squash, delete_branch: true) ⇒ Object
Merge PR.
-
.open_prs ⇒ Object
List open PRs.
-
.pr_comments(pr_number) ⇒ Object
Get all PR review comments.
-
.pr_info(pr_number) ⇒ Object
Get PR info.
-
.pr_status(pr_number) ⇒ Object
Get PR status (CI checks) Returns hash with :status (:pending, :passing, :failing) and :checks array.
-
.reply_to_comment(pr_number, comment_id, body) ⇒ Object
Reply to a PR comment.
-
.reset_client! ⇒ Object
Reset client (useful for testing).
-
.resolve_thread(thread_id) ⇒ Object
Resolve a review thread.
-
.unresolved_threads(pr_number) ⇒ Object
Get unresolved review threads via GraphQL.
-
.wait_for_ci(pr_number, timeout: 600) ⇒ Object
Wait for CI to complete (blocking).
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
13 14 15 16 |
# File 'lib/claude_task_master/github.rb', line 13 def available? _, status = Open3.capture2('gh auth status') status.success? end |
.client ⇒ Object
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.] end |
.current_repo ⇒ Object
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.}" false end |
.open_prs ⇒ Object
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.}" [] 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.}" 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
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 |