brainiac-github 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 675324c048d76415c0f4cd440ff81ed8eea72aa2806451882ea2bbe24e26303a
4
- data.tar.gz: 5d30ef4b03cfdf213331c70b17ad0d275a07e3b8e7f1b869cde62665852cf0c5
3
+ metadata.gz: 49d9d73cefe9f24ff840f683ea53a4ff2276cbd89495037765dcc65fb3370fae
4
+ data.tar.gz: 9de4f939b32f3f7c6f802d64e5a5280cc2de108fc771212b865bb17b8a12d21e
5
5
  SHA512:
6
- metadata.gz: bef506b5391c0344d6b7f3cb25edc9c0cb4843b708fc3ddf7f2ec3fa37426855ed54d201872f41901e9e2773928ddf34527f25dbd7075b8575f7f3ed5b6538ec
7
- data.tar.gz: 6e5f83d6f1598ce872505ae9eb1bad25296868846f1e93cdfea77aa9012fe956e479577b0adaa90d43a4525f6961da9ec5d3fbb3fb0fe1673a1aa16b2b78cee6
6
+ metadata.gz: f3b54d02be22dbc9072aa3170a7073ef912b40f9555b5fdfde9a0cea3c4212fcf84c5e797026cdf094b1ef4e220804784a30361e64377e00470a785e6481d2e6
7
+ data.tar.gz: 6cc367893d43b6276e141902e21b17e48c416a84c76e989bc0d34620223ee47b52fd02ec01650684e3adde7410d39ac42e9de8eeece11a895c43aea2e7d1b414
@@ -4,6 +4,17 @@ module Brainiac
4
4
  module Plugins
5
5
  module Github
6
6
  module Handler
7
+ # Marker that starts every crash-notification comment (see register_crash_handler!).
8
+ CRASH_COMMENT_PREFIX = "💥"
9
+
10
+ # Circuit breaker: max comment dispatches per (repo, PR, agent) within the window.
11
+ # Stops any comment -> dispatch -> crash -> comment feedback loop, regardless of
12
+ # who posted the comment or what it says.
13
+ COMMENT_DISPATCH_LIMIT = 4
14
+ COMMENT_DISPATCH_WINDOW = 120 # seconds
15
+ COMMENT_DISPATCH_LOCK = Mutex.new
16
+ COMMENT_DISPATCH_TIMES = Hash.new { |h, k| h[k] = [] }
17
+
7
18
  class << self
8
19
  def handle_pr_merged(payload)
9
20
  pr = payload["pull_request"]
@@ -254,20 +265,8 @@ module Brainiac
254
265
  comment_user_type = comment.dig("user", "type")
255
266
  repo_name = payload.dig("repository", "full_name")
256
267
 
257
- unless issue["pull_request"]
258
- LOG.info "Issue comment on non-PR issue ##{issue["number"]}, ignoring"
259
- return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
260
- end
261
-
262
- # If the PR was opened by a remote agent bot (not local to this machine),
263
- # skip dispatch — the other machine that owns the agent will handle it.
264
- pr_author = issue.dig("user", "login")
265
- pr_author_type = issue.dig("user", "type")
266
- unless pr_author_owned_locally?(pr_author, pr_author_type)
267
- pr_number = issue["number"]
268
- LOG.info "[GitHub] PR ##{pr_number} owned by remote agent (#{pr_author}) — skipping dispatch"
269
- return [200, { status: "ignored", reason: "PR owned by remote agent" }.to_json]
270
- end
268
+ rejection = issue_comment_rejection(issue, comment_body)
269
+ return rejection if rejection
271
270
 
272
271
  project_result = identify_project_by_repo(repo_name)
273
272
  unless project_result
@@ -301,6 +300,12 @@ module Brainiac
301
300
  bot_ignore_reason = bot_comment_ignore_reason(comment_user, comment_user_type, agent_name, pr_number)
302
301
  return bot_ignore_reason if bot_ignore_reason
303
302
 
303
+ if comment_dispatch_throttled?(repo_name, pr_number, agent_name)
304
+ LOG.warn "[GitHub] PR ##{pr_number}: >#{COMMENT_DISPATCH_LIMIT} dispatches for #{agent_name} in " \
305
+ "#{COMMENT_DISPATCH_WINDOW}s — possible loop, ignoring comment"
306
+ return [200, { status: "ignored", reason: "dispatch rate limit" }.to_json]
307
+ end
308
+
304
309
  branch = fetch_pr_branch(repo_name, pr_number, agent_name, project_config)
305
310
  result = find_work_item_by_branch(branch)
306
311
 
@@ -421,6 +426,42 @@ module Brainiac
421
426
  pr_author_owned_locally?(pr_author, pr_author_type)
422
427
  end
423
428
 
429
+ # Early-exit guards for issue_comment events. Returns a Rack-style response
430
+ # array when the comment should be ignored, or nil to continue dispatch.
431
+ def issue_comment_rejection(issue, comment_body)
432
+ unless issue["pull_request"]
433
+ LOG.info "Issue comment on non-PR issue ##{issue["number"]}, ignoring"
434
+ return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
435
+ end
436
+
437
+ # If the PR was opened by a remote agent bot (not local to this machine),
438
+ # skip dispatch — the other machine that owns the agent will handle it.
439
+ pr_author = issue.dig("user", "login")
440
+ pr_author_type = issue.dig("user", "type")
441
+ unless pr_author_owned_locally?(pr_author, pr_author_type)
442
+ LOG.info "[GitHub] PR ##{issue["number"]} owned by remote agent (#{pr_author}) — skipping dispatch"
443
+ return [200, { status: "ignored", reason: "PR owned by remote agent" }.to_json]
444
+ end
445
+
446
+ # Crash notifications are posted via `gh` as the human's account when the app
447
+ # client isn't configured, so the bot-login checks below can't catch them.
448
+ # Without this guard each crash comment re-dispatches the agent, which crashes
449
+ # again, forever.
450
+ if crash_notification_comment?(comment_body)
451
+ LOG.info "[GitHub] Ignoring crash notification comment on PR ##{issue["number"]}"
452
+ return [200, { status: "ignored", reason: "crash notification" }.to_json]
453
+ end
454
+
455
+ nil
456
+ end
457
+
458
+ # True when a comment is a crash notification posted by the crash handler.
459
+ # These are posted via `gh` as the human account when no app client is
460
+ # configured, so the bot-login checks can't catch them — match on the prefix.
461
+ def crash_notification_comment?(comment_body)
462
+ comment_body.to_s.lstrip.start_with?(CRASH_COMMENT_PREFIX)
463
+ end
464
+
424
465
  # Check PR ownership given the author login and account type.
425
466
  # Used by both pull_request_review (has full PR object) and issue_comment
426
467
  # (has issue.user which is the PR author on PR-linked issues).
@@ -631,6 +672,20 @@ module Brainiac
631
672
  # 1. Self-triggered: bot matches the dispatching agent (prevents loops)
632
673
  # 2. Remote agent: bot matches a known agent that isn't local (other machine handles it)
633
674
  # Allows: cross-agent LOCAL bot comments (e.g. GLaDOS commenting on Galen's PR)
675
+ # Records a dispatch attempt; true when the (repo, PR, agent) is over the limit.
676
+ def comment_dispatch_throttled?(repo_name, pr_number, agent_name)
677
+ key = [repo_name, pr_number, agent_name.to_s.downcase]
678
+ now = Time.now.to_f
679
+ COMMENT_DISPATCH_LOCK.synchronize do
680
+ times = COMMENT_DISPATCH_TIMES[key]
681
+ times.reject! { |t| now - t > COMMENT_DISPATCH_WINDOW }
682
+ return true if times.size >= COMMENT_DISPATCH_LIMIT
683
+
684
+ times << now
685
+ false
686
+ end
687
+ end
688
+
634
689
  def bot_comment_ignore_reason(comment_user, comment_user_type, agent_name, pr_number)
635
690
  return nil unless comment_user_type == "Bot"
636
691
 
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Github
6
- VERSION = "0.3.3"
6
+ VERSION = "0.3.4"
7
7
  end
8
8
  end
9
9
  end
@@ -67,7 +67,8 @@ module Brainiac
67
67
  agent_key = ctx[:agent_key]
68
68
  snippet = ctx[:snippet]
69
69
  snippet_block = snippet ? "\n```\n#{snippet[-1500..]}\n```" : ""
70
- comment_body = "💥 **#{agent_display} crashed** (exit code #{ctx[:exit_status]})\n\nLog: `#{ctx[:log_file]}`#{snippet_block}"
70
+ comment_body = "#{Handler::CRASH_COMMENT_PREFIX} **#{agent_display} crashed** " \
71
+ "(exit code #{ctx[:exit_status]})\n\nLog: `#{ctx[:log_file]}`#{snippet_block}"
71
72
 
72
73
  begin
73
74
  if AppClient.configured?(agent_key)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-github
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis