Class: Ace::Review::Molecules::GhCommentResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/review/molecules/gh_comment_resolver.rb

Overview

Resolve PR comments by replying and/or resolving threads Used by the review-pr workflow to mark feedback as addressed

Constant Summary collapse

THREAD_ID_PATTERN =

Valid thread ID pattern (GitHub GraphQL node IDs) PRRT_ = Pull Request Review Thread

/\APRRT_[A-Za-z0-9_-]+\z/

Class Method Summary collapse

Class Method Details

.reply(pr_identifier, commit_sha, message: nil, options: {}) ⇒ Hash

Reply to a PR with a comment indicating a fix

Parameters:

  • pr_identifier (String)

    PR identifier (number, URL, or owner/repo#number)

  • commit_sha (String)

    Commit SHA that addresses the feedback

  • message (String, nil) (defaults to: nil)

    Optional custom message (default: “Fixed in sha”)

  • options (Hash) (defaults to: {})

    Options

Options Hash (options:):

  • :timeout (Integer)

    Timeout in seconds (default: 30)

Returns:

  • (Hash)

    Result with :success, :comment_url, :error



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/ace/review/molecules/gh_comment_resolver.rb', line 19

def self.reply(pr_identifier, commit_sha, message: nil, options: {})
  # Guard: require either commit_sha or custom message
  if (commit_sha.nil? || commit_sha.to_s.strip.empty?) && (message.nil? || message.strip.empty?)
    return {success: false, error: "Commit SHA or message required"}
  end

  # Parse identifier using ace-git
  parsed = Ace::Git::Atoms::PrIdentifierParser.parse(pr_identifier)
  gh_format = parsed.gh_format

  # Build message
  short_sha = commit_sha.to_s[0..6]
  body = message || "Fixed in #{short_sha}"

  # Default timeout
  timeout = options[:timeout] || 30

  # Post comment using gh CLI
  result = Ace::Git::Molecules::GhCliExecutor.execute(
    "pr",
    ["comment", gh_format, "--body", body],
    timeout: timeout
  )

  if result[:success]
    # Try to extract comment URL from output
    comment_url = extract_comment_url(result[:stdout])
    {
      success: true,
      comment_url: comment_url,
      message: body
    }
  else
    {
      success: false,
      error: "Failed to post reply: #{result[:stderr]}"
    }
  end
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError,
  Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
  raise
rescue => e
  {
    success: false,
    error: "Failed to post reply: #{e.message}"
  }
end

.reply_and_resolve(pr_identifier, commit_sha, thread_id: nil, message: nil, options: {}) ⇒ Hash

Reply to PR and resolve thread in one operation

Parameters:

  • pr_identifier (String)

    PR identifier

  • thread_id (String, nil) (defaults to: nil)

    Thread ID to resolve (optional)

  • commit_sha (String)

    Commit SHA that addresses the feedback

  • message (String, nil) (defaults to: nil)

    Optional custom message

  • options (Hash) (defaults to: {})

    Options

Returns:

  • (Hash)

    Result with :success, :reply_result, :resolve_result, :error



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/ace/review/molecules/gh_comment_resolver.rb', line 140

def self.reply_and_resolve(pr_identifier, commit_sha, thread_id: nil, message: nil, options: {})
  results = {success: true}

  # Step 1: Reply with commit reference
  reply_result = reply(pr_identifier, commit_sha, message: message, options: options)
  results[:reply_result] = reply_result

  unless reply_result[:success]
    results[:success] = false
    results[:error] = reply_result[:error]
    return results
  end

  # Step 2: Resolve thread if thread_id provided
  if thread_id && !thread_id.empty?
    resolve_result = resolve_thread(thread_id, options: options)
    results[:resolve_result] = resolve_result

    # Thread resolution failure is not fatal - reply succeeded
    unless resolve_result[:success]
      results[:partial] = true
      results[:warning] = "Reply posted but thread not resolved: #{resolve_result[:error]}"
    end
  end

  results
end

.resolve_thread(thread_id, options: {}) ⇒ Hash

Resolve a review thread by thread ID using GitHub GraphQL API

Note: This requires the thread’s node ID (starts with “PRRT_”) which can be obtained from the GhPrCommentFetcher results

Parameters:

  • thread_id (String)

    GraphQL node ID of the review thread (e.g., “PRRT_abc123”)

  • options (Hash) (defaults to: {})

    Options

Options Hash (options:):

  • :timeout (Integer)

    Timeout in seconds (default: 30)

Returns:

  • (Hash)

    Result with :success, :resolved, :error



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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/ace/review/molecules/gh_comment_resolver.rb', line 80

def self.resolve_thread(thread_id, options: {})
  return {success: false, error: "Thread ID required"} if thread_id.nil? || thread_id.empty?

  # Validate thread_id format to prevent GraphQL injection
  unless thread_id.match?(THREAD_ID_PATTERN)
    return {success: false, error: "Invalid thread ID format. Expected PRRT_xxx pattern."}
  end

  # Default timeout
  timeout = options[:timeout] || 30

  # Build and execute GraphQL mutation
  mutation = build_resolve_thread_mutation(thread_id)

  # Execute via gh api graphql
  result = Ace::Git::Molecules::GhCliExecutor.execute(
    "api",
    ["graphql", "-f", "query=#{mutation}"],
    timeout: timeout
  )

  if result[:success]
    begin
      response = JSON.parse(result[:stdout])
      is_resolved = response.dig("data", "resolveReviewThread", "thread", "isResolved")

      if is_resolved
        {success: true, resolved: true}
      elsif response["errors"]
        {success: false, error: response["errors"].first["message"]}
      else
        {success: false, error: "Thread not resolved"}
      end
    rescue JSON::ParserError => e
      {success: false, error: "Failed to parse response: #{e.message}"}
    end
  else
    {
      success: false,
      error: "Failed to resolve thread: #{result[:stderr]}"
    }
  end
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError,
  Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
  raise
rescue => e
  {
    success: false,
    error: "Failed to resolve thread: #{e.message}"
  }
end