Class: Aidp::Watch::CiFixProcessor

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

Overview

Handles the aidp-fix-ci label trigger by analyzing CI failures and automatically fixing them with commits pushed to the PR branch.

Constant Summary collapse

DEFAULT_CI_FIX_LABEL =

Default label names

"aidp-fix-ci"
COMMENT_HEADER =
"## 🤖 AIDP CI Fix"
MAX_FIX_ATTEMPTS =
3

Constants included from MessageDisplay

MessageDisplay::COLOR_MAP

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MessageDisplay

#display_message, included, #message_display_prompt

Constructor Details

#initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false) ⇒ CiFixProcessor

Returns a new instance of CiFixProcessor.



33
34
35
36
37
38
39
40
41
42
43
# File 'lib/aidp/watch/ci_fix_processor.rb', line 33

def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false)
  @repository_client = repository_client
  @state_store = state_store
  @state_extractor = GitHubStateExtractor.new(repository_client: repository_client)
  @provider_name = provider_name
  @project_dir = project_dir
  @verbose = verbose

  # Load label configuration
  @ci_fix_label = label_config[:ci_fix_trigger] || label_config["ci_fix_trigger"] || DEFAULT_CI_FIX_LABEL
end

Instance Attribute Details

#ci_fix_labelObject (readonly)

Returns the value of attribute ci_fix_label.



31
32
33
# File 'lib/aidp/watch/ci_fix_processor.rb', line 31

def ci_fix_label
  @ci_fix_label
end

Instance Method Details

#process(pr) ⇒ Object



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
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
131
132
133
134
135
# File 'lib/aidp/watch/ci_fix_processor.rb', line 45

def process(pr)
  number = pr[:number]

  Aidp.log_debug("ci_fix_processor", "process_started", pr_number: number, pr_title: pr[:title])

  # Check if already processed successfully via GitHub comments
  if @state_extractor.ci_fix_completed?(pr)
    display_message("â„šī¸  CI fix for PR ##{number} already completed. Skipping.", type: :muted)
    Aidp.log_debug("ci_fix_processor", "already_completed", pr_number: number)
    return
  end

  display_message("🔧 Analyzing CI failures for PR ##{number} (#{pr[:title]})", type: :info)

  # Fetch PR details
  pr_data = @repository_client.fetch_pull_request(number)
  ci_status = @repository_client.fetch_ci_status(number)

  Aidp.log_debug("ci_fix_processor", "ci_status_fetched",
    pr_number: number,
    ci_state: ci_status[:state],
    check_count: ci_status[:checks]&.length || 0,
    checks: ci_status[:checks]&.map { |c| {name: c[:name], status: c[:status], conclusion: c[:conclusion]} })

  # Check if there are failures
  if ci_status[:state] == "success"
    display_message("✅ CI is passing for PR ##{number}. No fixes needed.", type: :success)
    Aidp.log_debug("ci_fix_processor", "ci_passing", pr_number: number)
    post_success_comment(pr_data)
    @state_store.record_ci_fix(number, {status: "no_failures", timestamp: Time.now.utc.iso8601})
    begin
      @repository_client.remove_labels(number, @ci_fix_label)
    rescue
      nil
    end
    return
  end

  if ci_status[:state] == "pending"
    display_message("âŗ CI is still running for PR ##{number}. Skipping for now.", type: :muted)
    Aidp.log_debug("ci_fix_processor", "ci_pending", pr_number: number)
    return
  end

  # Get failed checks
  failed_checks = ci_status[:checks].select { |check| check[:conclusion] == "failure" }

  Aidp.log_debug("ci_fix_processor", "failed_checks_filtered",
    pr_number: number,
    total_checks: ci_status[:checks]&.length || 0,
    failed_count: failed_checks.length,
    failed_checks: failed_checks.map { |c| c[:name] })

  if failed_checks.empty?
    display_message("âš ī¸  No specific failed checks found for PR ##{number}.", type: :warn)
    Aidp.log_debug("ci_fix_processor", "no_failed_checks",
      pr_number: number,
      ci_state: ci_status[:state],
      all_checks: ci_status[:checks]&.map { |c| {name: c[:name], conclusion: c[:conclusion]} })
    return
  end

  display_message("Found #{failed_checks.length} failed check(s):", type: :info)
  failed_checks.each do |check|
    display_message("  - #{check[:name]}", type: :muted)
  end

  # Analyze failures and generate fixes
  fix_result = analyze_and_fix(pr_data: pr_data, ci_status: ci_status, failed_checks: failed_checks)

  # Log the fix attempt
  log_ci_fix(number, fix_result)

  if fix_result[:success]
    handle_success(pr: pr_data, fix_result: fix_result)
  else
    handle_failure(pr: pr_data, fix_result: fix_result)
  end
rescue => e
  display_message("❌ CI fix failed: #{e.message}", type: :error)
  Aidp.log_error("ci_fix_processor", "CI fix failed", pr: pr[:number], error: e.message, backtrace: e.backtrace&.first(10))

  # Record failure state internally but DON'T post error to GitHub
  # (per issue #280 - error messages should never appear on issues)
  @state_store.record_ci_fix(pr[:number], {
    status: "error",
    error: e.message,
    error_class: e.class.name,
    timestamp: Time.now.utc.iso8601
  })
end