Class: Aidp::Watch::AutoMerger

Inherits:
Object
  • Object
show all
Includes:
MessageDisplay
Defined in:
lib/aidp/watch/auto_merger.rb

Overview

Automatically merges sub-issue PRs when CI passes and conditions are met. Never auto-merges parent PRs - those require human review.

Constant Summary collapse

PARENT_PR_LABEL =

Labels that indicate PR type

"aidp-parent-pr"
SUB_PR_LABEL =
"aidp-sub-pr"
DEFAULT_CONFIG =

Default configuration

{
  enabled: true,
  sub_issue_prs_only: true,
  require_ci_success: true,
  require_reviews: 0,
  merge_method: "squash",
  delete_branch: true
}.freeze

Constants included from MessageDisplay

MessageDisplay::COLOR_MAP, MessageDisplay::CRITICAL_TYPES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MessageDisplay

#display_message, included, #message_display_prompt, #quiet_mode?

Constructor Details

#initialize(repository_client:, state_store:, config: {}) ⇒ AutoMerger

Returns a new instance of AutoMerger.



28
29
30
31
32
# File 'lib/aidp/watch/auto_merger.rb', line 28

def initialize(repository_client:, state_store:, config: {})
  @repository_client = repository_client
  @state_store = state_store
  @config = DEFAULT_CONFIG.merge(config)
end

Instance Attribute Details

#repository_clientObject (readonly)

Returns the value of attribute repository_client.



26
27
28
# File 'lib/aidp/watch/auto_merger.rb', line 26

def repository_client
  @repository_client
end

#state_storeObject (readonly)

Returns the value of attribute state_store.



26
27
28
# File 'lib/aidp/watch/auto_merger.rb', line 26

def state_store
  @state_store
end

Instance Method Details

#can_auto_merge?(pr_number) ⇒ Hash

Check if a PR can be auto-merged

Parameters:

  • pr_number (Integer)

    The PR number

Returns:

  • (Hash)

    Result with :can_merge flag and :reason



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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/aidp/watch/auto_merger.rb', line 37

def can_auto_merge?(pr_number)
  Aidp.log_debug("auto_merger", "checking_can_auto_merge", pr_number: pr_number)

  return {can_merge: false, reason: "Auto-merge is disabled"} unless @config[:enabled]

  # Fetch PR details
  pr = begin
    @repository_client.fetch_pull_request(pr_number)
  rescue => e
    Aidp.log_error("auto_merger", "Failed to fetch PR", pr_number: pr_number, error: e.message)
    return {can_merge: false, reason: "Failed to fetch PR: #{e.message}"}
  end

  # Check if it's a parent PR (never auto-merge)
  if pr[:labels].include?(PARENT_PR_LABEL)
    Aidp.log_debug("auto_merger", "skipping_parent_pr", pr_number: pr_number)
    return {can_merge: false, reason: "Parent PRs require human review"}
  end

  # Check if sub-PRs only mode requires the sub-PR label
  if @config[:sub_issue_prs_only] && !pr[:labels].include?(SUB_PR_LABEL)
    Aidp.log_debug("auto_merger", "not_a_sub_pr", pr_number: pr_number)
    return {can_merge: false, reason: "Not a sub-issue PR (missing #{SUB_PR_LABEL} label)"}
  end

  # Check PR state
  unless pr[:state] == "open" || pr[:state] == "OPEN"
    return {can_merge: false, reason: "PR is not open (state: #{pr[:state]})"}
  end

  # Check mergeability
  if pr[:mergeable] == false
    return {can_merge: false, reason: "PR has merge conflicts"}
  end

  # Check CI status
  if @config[:require_ci_success]
    ci_status = @repository_client.fetch_ci_status(pr_number)
    unless ci_status[:state] == "success"
      Aidp.log_debug("auto_merger", "ci_not_passed",
        pr_number: pr_number, ci_state: ci_status[:state])
      return {can_merge: false, reason: "CI has not passed (status: #{ci_status[:state]})"}
    end
  end

  # All checks passed
  Aidp.log_debug("auto_merger", "can_auto_merge", pr_number: pr_number)
  {can_merge: true, reason: "All merge conditions met"}
end

#list_sub_pr_candidatesArray<Hash>

List all PRs with the sub-PR label that are candidates for auto-merge

Returns:

  • (Array<Hash>)

    PRs that might be eligible for auto-merge



162
163
164
165
166
167
# File 'lib/aidp/watch/auto_merger.rb', line 162

def list_sub_pr_candidates
  @repository_client.list_pull_requests(labels: [SUB_PR_LABEL], state: "open")
rescue => e
  Aidp.log_error("auto_merger", "Failed to list sub-PR candidates", error: e.message)
  []
end

#merge_pr(pr_number) ⇒ Hash

Attempt to merge a PR

Parameters:

  • pr_number (Integer)

    The PR number

Returns:

  • (Hash)

    Result with :success flag, :reason, and optional :merge_sha



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
# File 'lib/aidp/watch/auto_merger.rb', line 90

def merge_pr(pr_number)
  Aidp.log_debug("auto_merger", "attempting_merge", pr_number: pr_number)

  # Verify can merge
  eligibility = can_auto_merge?(pr_number)
  unless eligibility[:can_merge]
    return {success: false, reason: eligibility[:reason]}
  end

  begin
    result = @repository_client.merge_pull_request(
      pr_number,
      merge_method: @config[:merge_method]
    )

    Aidp.log_info("auto_merger", "pr_merged",
      pr_number: pr_number, merge_method: @config[:merge_method])
    display_message("✅ Auto-merged PR ##{pr_number}", type: :success)

    # Post comment about auto-merge
    post_merge_comment(pr_number)

    # Update parent issue/PR if this was a sub-issue PR
    update_parent_after_merge(pr_number)

    {success: true, reason: "Successfully merged", result: result}
  rescue => e
    Aidp.log_error("auto_merger", "merge_failed",
      pr_number: pr_number, error: e.message)
    display_message("❌ Failed to auto-merge PR ##{pr_number}: #{e.message}", type: :error)
    {success: false, reason: "Merge failed: #{e.message}"}
  end
end

#process_auto_merge_candidates(prs) ⇒ Hash

Process all eligible PRs for auto-merge

Parameters:

  • prs (Array<Hash>)

    Array of PR data with :number keys

Returns:

  • (Hash)

    Summary with :merged, :skipped, :failed counts



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
156
157
158
# File 'lib/aidp/watch/auto_merger.rb', line 127

def process_auto_merge_candidates(prs)
  Aidp.log_debug("auto_merger", "processing_candidates", count: prs.size)

  merged = 0
  skipped = 0
  failed = 0

  prs.each do |pr|
    pr_number = pr[:number]

    eligibility = can_auto_merge?(pr_number)
    unless eligibility[:can_merge]
      Aidp.log_debug("auto_merger", "skipping_pr",
        pr_number: pr_number, reason: eligibility[:reason])
      skipped += 1
      next
    end

    result = merge_pr(pr_number)
    if result[:success]
      merged += 1
    else
      failed += 1
    end
  end

  summary = {merged: merged, skipped: skipped, failed: failed}
  Aidp.log_info("auto_merger", "processing_complete", **summary)
  display_message("🔀 Auto-merge: #{merged} merged, #{skipped} skipped, #{failed} failed",
    type: :info)
  summary
end