brainiac 0.0.10 → 0.0.12
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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +134 -151
- data/bin/brainiac +181 -109
- data/brainiac.gemspec +4 -3
- data/docs/waybar-config.md +7 -27
- data/lib/brainiac/agents.rb +5 -5
- data/lib/brainiac/config.rb +0 -36
- data/lib/brainiac/cron.rb +15 -17
- data/lib/brainiac/helpers.rb +58 -31
- data/lib/brainiac/hooks.rb +1 -1
- data/lib/brainiac/notifications.rb +2 -2
- data/lib/brainiac/prompts.rb +11 -133
- data/lib/brainiac/restart.rb +0 -3
- data/lib/brainiac/routes/api.rb +6 -32
- data/lib/brainiac/sessions.rb +1 -2
- data/lib/brainiac/version.rb +1 -1
- data/monitor/shared.rb +1 -1
- data/receiver.rb +2 -227
- data/templates/agents.json.example +2 -2
- data/templates/brainiac.json.example +1 -6
- data/templates/testflight.json.example +1 -1
- data/templates/users.json.example +20 -82
- metadata +5 -9
- data/lib/brainiac/handlers/github.rb +0 -390
- data/lib/brainiac/handlers/zoho.rb +0 -429
- data/lib/brainiac/zoho_mail_api.rb +0 -110
- data/templates/github.json.example +0 -4
- data/templates/zoho.json.example +0 -27
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# GitHub webhook handlers: PR merges, reviews, and issue comments.
|
|
4
|
-
|
|
5
|
-
# Fallback column ID for backwards compatibility when no board config exists
|
|
6
|
-
DEFAULT_UAT_COLUMN_ID = "03fsmglsr6az06ppyotawsti8"
|
|
7
|
-
|
|
8
|
-
# Find a work item by matching the PR's head branch to a branch in the card map.
|
|
9
|
-
def find_work_item_by_branch(branch)
|
|
10
|
-
map = load_work_item_map
|
|
11
|
-
map.each do |internal_id, info|
|
|
12
|
-
next unless info["branch"] == branch
|
|
13
|
-
|
|
14
|
-
return [internal_id, info]
|
|
15
|
-
end
|
|
16
|
-
nil
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Track a newly opened PR in the work item map by matching its branch.
|
|
20
|
-
def track_pr_in_work_items(payload)
|
|
21
|
-
pr = payload["pull_request"]
|
|
22
|
-
branch = pr.dig("head", "ref")
|
|
23
|
-
pr_number = pr["number"]
|
|
24
|
-
pr_url = pr["html_url"]
|
|
25
|
-
|
|
26
|
-
result = find_work_item_by_branch(branch)
|
|
27
|
-
unless result
|
|
28
|
-
LOG.info "[PR Track] No card found for branch #{branch}"
|
|
29
|
-
return
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
internal_id, card_info = result
|
|
33
|
-
prs = card_info["prs"] || []
|
|
34
|
-
return if prs.any? { |p| p["number"] == pr_number }
|
|
35
|
-
|
|
36
|
-
prs << { "number" => pr_number, "url" => pr_url }
|
|
37
|
-
card_info["prs"] = prs
|
|
38
|
-
|
|
39
|
-
map = load_work_item_map
|
|
40
|
-
map[internal_id] = card_info
|
|
41
|
-
save_work_item_map(map)
|
|
42
|
-
LOG.info "[PR Track] Tracked PR ##{pr_number} on card ##{card_info["number"]} (branch: #{branch})"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Fetch review comments from a PR using GitHub CLI
|
|
46
|
-
def fetch_pr_review_comments(pr_number, repo)
|
|
47
|
-
output = run_cmd("gh", "api", "/repos/#{repo}/pulls/#{pr_number}/comments", "--jq", ".[] | {path, line, body, user: .user.login}")
|
|
48
|
-
output.lines.map { |line| JSON.parse(line) }
|
|
49
|
-
rescue StandardError => e
|
|
50
|
-
LOG.warn "Could not fetch PR review comments: #{e.message}"
|
|
51
|
-
[]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Check if a PR link is already present in the card's comments.
|
|
55
|
-
|
|
56
|
-
def handle_github_pr_merged(payload)
|
|
57
|
-
pr = payload["pull_request"]
|
|
58
|
-
branch = pr.dig("head", "ref")
|
|
59
|
-
base = pr.dig("base", "ref")
|
|
60
|
-
pr_url = pr["html_url"]
|
|
61
|
-
pr_title = pr["title"]
|
|
62
|
-
repo_full_name = payload.dig("repository", "full_name")
|
|
63
|
-
|
|
64
|
-
default_branch = payload.dig("repository", "default_branch") || "main"
|
|
65
|
-
unless base == default_branch
|
|
66
|
-
LOG.info "PR merged into #{base}, not #{default_branch} — ignoring"
|
|
67
|
-
return [200, { status: "ignored", reason: "not merged into #{default_branch}" }.to_json]
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
project_result = identify_project_by_repo(repo_full_name)
|
|
71
|
-
unless project_result
|
|
72
|
-
LOG.info "No project found for GitHub repo #{repo_full_name}"
|
|
73
|
-
return [200, { status: "ignored", reason: "no matching project" }.to_json]
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
project_key, project_config = project_result
|
|
77
|
-
repo_path = project_config["repo_path"]
|
|
78
|
-
|
|
79
|
-
result = find_work_item_by_branch(branch)
|
|
80
|
-
unless result
|
|
81
|
-
LOG.info "No card found for branch #{branch}"
|
|
82
|
-
return [200, { status: "ignored", reason: "no matching card" }.to_json]
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
internal_id, card_info = result
|
|
86
|
-
card_number = card_info["number"]
|
|
87
|
-
unless card_number
|
|
88
|
-
LOG.warn "Card #{internal_id} has no number — can't comment or move"
|
|
89
|
-
return [200, { status: "ignored", reason: "card has no number" }.to_json]
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
LOG.info "PR merged into main for card ##{card_number} (project: #{project_key}): #{pr_url}"
|
|
93
|
-
process_merged_pr(card_info, card_number, branch, pr, pr_url, pr_title, project_key, project_config, repo_path)
|
|
94
|
-
|
|
95
|
-
[200, { status: "processed", card: card_number, pr: pr_url, action: "merged_to_uat", project: project_key }.to_json]
|
|
96
|
-
rescue StandardError => e
|
|
97
|
-
LOG.error "Error handling merged PR: #{e.message}"
|
|
98
|
-
[500, { error: e.message }.to_json]
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def process_merged_pr(card_info, card_number, branch, pull_request, pr_url, pr_title, project_key, project_config, repo_path)
|
|
102
|
-
mark_work_item_merged(card_number)
|
|
103
|
-
cleanup_work_item_worktrees(card_number, repo_path: repo_path, primary_worktree: card_info["worktree"], primary_branch: branch)
|
|
104
|
-
|
|
105
|
-
# Emit hook — plugins handle their own post-merge actions
|
|
106
|
-
# (e.g., card plugin posts PR link, moves card column, dispatches UAT agent)
|
|
107
|
-
Brainiac.emit(:pr_merged,
|
|
108
|
-
card_number: card_number,
|
|
109
|
-
card_info: card_info,
|
|
110
|
-
branch: branch,
|
|
111
|
-
pull_request: pull_request,
|
|
112
|
-
pr_url: pr_url,
|
|
113
|
-
pr_title: pr_title,
|
|
114
|
-
project_key: project_key,
|
|
115
|
-
project_config: project_config,
|
|
116
|
-
repo_path: repo_path)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def handle_github_issue_comment(payload)
|
|
120
|
-
comment = payload["comment"]
|
|
121
|
-
issue = payload["issue"]
|
|
122
|
-
comment_body = comment["body"] || ""
|
|
123
|
-
comment_id = comment["id"]
|
|
124
|
-
comment_user = comment.dig("user", "login")
|
|
125
|
-
repo_name = payload.dig("repository", "full_name")
|
|
126
|
-
|
|
127
|
-
unless issue["pull_request"]
|
|
128
|
-
LOG.info "Issue comment on non-PR issue ##{issue["number"]}, ignoring"
|
|
129
|
-
return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
project_result = identify_project_by_repo(repo_name)
|
|
133
|
-
unless project_result
|
|
134
|
-
LOG.info "No project found for GitHub repo #{repo_name}"
|
|
135
|
-
return [200, { status: "ignored", reason: "no matching project" }.to_json]
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
project_key, project_config = project_result
|
|
139
|
-
pr_number = issue["number"]
|
|
140
|
-
|
|
141
|
-
pr_data = run_cmd("gh", "api", "/repos/#{repo_name}/pulls/#{pr_number}", "--jq", "{branch: .head.ref}",
|
|
142
|
-
chdir: project_config["repo_path"])
|
|
143
|
-
branch = JSON.parse(pr_data)["branch"]
|
|
144
|
-
|
|
145
|
-
result = find_work_item_by_branch(branch)
|
|
146
|
-
unless result
|
|
147
|
-
LOG.info "No card found for PR ##{pr_number} (branch: #{branch})"
|
|
148
|
-
return [200, { status: "ignored", reason: "no matching card" }.to_json]
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
_, card_info = result
|
|
152
|
-
card_number = card_info["number"]
|
|
153
|
-
worktree = card_info["worktree"]
|
|
154
|
-
|
|
155
|
-
unless worktree && File.directory?(worktree)
|
|
156
|
-
LOG.info "No active worktree for PR ##{pr_number}, ignoring comment"
|
|
157
|
-
return [200, { status: "ignored", reason: "no active worktree" }.to_json]
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
card_key = "card-#{card_number}"
|
|
161
|
-
if session_active?(card_key)
|
|
162
|
-
LOG.info "Skipping PR comment on card ##{card_number} — agent session already active"
|
|
163
|
-
return [200, { status: "ignored", reason: "session already active" }.to_json]
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
LOG.info "PR comment from #{comment_user} on PR ##{pr_number} for card ##{card_number} (project: #{project_key})"
|
|
167
|
-
dispatch_pr_comment(card_number, card_key, pr_number, comment_id, comment_user, comment_body,
|
|
168
|
-
repo_name, worktree, project_key, project_config)
|
|
169
|
-
|
|
170
|
-
[200, { status: "processed", card: card_number, pr: pr_number, comment_id: comment_id, project: project_key }.to_json]
|
|
171
|
-
rescue StandardError => e
|
|
172
|
-
LOG.error "Error handling PR comment: #{e.message}"
|
|
173
|
-
[500, { error: e.message }.to_json]
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def dispatch_pr_comment(card_number, card_key, pr_number, comment_id, comment_user, comment_body,
|
|
177
|
-
repo_name, worktree, project_key, project_config)
|
|
178
|
-
Thread.new do
|
|
179
|
-
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/issues/comments/#{comment_id}/reactions",
|
|
180
|
-
"-f", "content=eyes", "-H", "Accept: application/vnd.github+json", chdir: worktree)
|
|
181
|
-
rescue StandardError => e
|
|
182
|
-
LOG.warn "Could not add reaction to comment: #{e.message}"
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
agent_name = agent_name_for(project_config)
|
|
186
|
-
prompt = render_prompt(PROMPT_GITHUB_PR_COMMENT,
|
|
187
|
-
{ "CARD_NUMBER" => card_number, "CARD_ID" => card_number,
|
|
188
|
-
"COMMENT_CREATOR" => comment_user, "COMMENT_BODY" => comment_body,
|
|
189
|
-
"PR_NUMBER" => pr_number.to_s, "WORKTREE_PATH" => worktree },
|
|
190
|
-
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number,
|
|
191
|
-
project_key: project_key, comment_body: comment_body),
|
|
192
|
-
agent_name: agent_name, channel: :github)
|
|
193
|
-
|
|
194
|
-
pid, log_file = run_agent(prompt, project_config: project_config, chdir: worktree,
|
|
195
|
-
log_name: "pr-comment-#{pr_number}",
|
|
196
|
-
model: detect_model(project_config, text: comment_body),
|
|
197
|
-
effort: detect_effort(project_config, text: comment_body),
|
|
198
|
-
agent_name: agent_name, source: :github,
|
|
199
|
-
source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: worktree })
|
|
200
|
-
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def handle_github_workflow_run(payload)
|
|
204
|
-
workflow = payload["workflow_run"]
|
|
205
|
-
workflow_name = workflow["name"]
|
|
206
|
-
conclusion = workflow["conclusion"]
|
|
207
|
-
repo_full_name = payload.dig("repository", "full_name")
|
|
208
|
-
run_url = workflow["html_url"]
|
|
209
|
-
|
|
210
|
-
if workflow_name == "Deploy to Production" && conclusion == "failure"
|
|
211
|
-
project_key = identify_project_by_repo(repo_full_name)&.first || repo_full_name
|
|
212
|
-
send_workflow_failure_notification(project_key, workflow_name, run_url)
|
|
213
|
-
return [200, { status: "processed", action: "prod_deploy_failure_notified", project: project_key }.to_json]
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
if workflow_name == "Deploy to UAT" && conclusion == "success"
|
|
217
|
-
project_key = identify_project_by_repo(repo_full_name)&.first || repo_full_name
|
|
218
|
-
send_uat_deploy_notification(project_key)
|
|
219
|
-
return [200, { status: "processed", action: "uat_deploy_notified", project: project_key }.to_json]
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
return [200, { status: "ignored", reason: "conclusion: #{conclusion}" }.to_json] unless conclusion == "success"
|
|
223
|
-
|
|
224
|
-
return [200, { status: "ignored", reason: "workflow: #{workflow_name}" }.to_json] unless workflow_name == "Deploy to Production"
|
|
225
|
-
|
|
226
|
-
project_result = identify_project_by_repo(repo_full_name)
|
|
227
|
-
return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_result
|
|
228
|
-
|
|
229
|
-
project_key, project_config = project_result
|
|
230
|
-
close_uat_cards_after_deploy(project_key, project_config)
|
|
231
|
-
rescue StandardError => e
|
|
232
|
-
LOG.error "Error handling workflow run: #{e.message}"
|
|
233
|
-
[500, { error: e.message }.to_json]
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def close_uat_cards_after_deploy(project_key, project_config)
|
|
237
|
-
# Emit hook — work item plugins handle closing cards after production deploy
|
|
238
|
-
results = Brainiac.emit(:production_deployed, project_key: project_key, project_config: project_config)
|
|
239
|
-
closed_cards = results.flatten.compact
|
|
240
|
-
|
|
241
|
-
if closed_cards.any?
|
|
242
|
-
send_deploy_notification(project_key, closed_cards)
|
|
243
|
-
LOG.info "Prod deploy complete — closed #{closed_cards.size} cards"
|
|
244
|
-
else
|
|
245
|
-
LOG.info "Prod deploy processed — no cards closed (plugin may not be installed)"
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
[200, { status: "processed", action: "prod_deploy", closed_cards: closed_cards.map { |c| c[:number] }, project: project_key }.to_json]
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def send_deploy_notification(project_key, closed_cards)
|
|
252
|
-
card_lines = closed_cards.map { |c| "• [##{c[:number]} — #{c[:title]}](#{c[:url]})" }.join("\n")
|
|
253
|
-
message = "🚀 **#{project_key.capitalize}** deployed to production\nClosed UAT cards:\n#{card_lines}"
|
|
254
|
-
send_notification(:deploy, message, metadata_project: project_key)
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def send_uat_deploy_notification(project_key)
|
|
258
|
-
message = "✅ **#{project_key.capitalize}** deployed to UAT successfully"
|
|
259
|
-
send_notification(:deploy, message, metadata_project: project_key)
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def send_workflow_failure_notification(project_key, workflow_name, run_url)
|
|
263
|
-
message = "❌ **#{project_key.capitalize}** — #{workflow_name} failed\n[View run](#{run_url})"
|
|
264
|
-
send_notification(:ci_failure, message, metadata_project: project_key)
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def handle_github_issue_opened(payload)
|
|
268
|
-
issue = payload["issue"]
|
|
269
|
-
issue_url = issue["html_url"]
|
|
270
|
-
issue_title = issue["title"]
|
|
271
|
-
issue_number = issue["number"]
|
|
272
|
-
repo_name = payload.dig("repository", "full_name")
|
|
273
|
-
|
|
274
|
-
LOG.info "New GitHub issue ##{issue_number} on #{repo_name}: #{issue_title} (#{issue_url})"
|
|
275
|
-
|
|
276
|
-
[200, { status: "logged", issue: issue_number, title: issue_title, url: issue_url }.to_json]
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def handle_github_pr_review_submitted(payload)
|
|
280
|
-
pr = payload["pull_request"]
|
|
281
|
-
review = payload["review"]
|
|
282
|
-
branch = pr.dig("head", "ref")
|
|
283
|
-
pr_number = pr["number"]
|
|
284
|
-
repo_name = payload.dig("repository", "full_name")
|
|
285
|
-
review_state = review["state"]
|
|
286
|
-
reviewer = review.dig("user", "login")
|
|
287
|
-
|
|
288
|
-
return [200, { status: "ignored", reason: "review state: #{review_state}" }.to_json] unless %w[changes_requested commented].include?(review_state)
|
|
289
|
-
|
|
290
|
-
project_result = identify_project_by_repo(repo_name)
|
|
291
|
-
return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_result
|
|
292
|
-
|
|
293
|
-
project_key, project_config = project_result
|
|
294
|
-
repo_path = project_config["repo_path"]
|
|
295
|
-
|
|
296
|
-
result = find_work_item_by_branch(branch)
|
|
297
|
-
return [200, { status: "ignored", reason: "no matching card" }.to_json] unless result
|
|
298
|
-
|
|
299
|
-
internal_id, card_info = result
|
|
300
|
-
card_number = card_info["number"]
|
|
301
|
-
unless card_number
|
|
302
|
-
LOG.warn "Card #{internal_id} has no number — can't comment"
|
|
303
|
-
return [200, { status: "ignored", reason: "card has no number" }.to_json]
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
card_key = "card-#{card_number}"
|
|
307
|
-
return [200, { status: "ignored", reason: "session already active" }.to_json] if session_active?(card_key)
|
|
308
|
-
|
|
309
|
-
LOG.info "PR review submitted by #{reviewer} on PR ##{pr_number} for card ##{card_number} (project: #{project_key})"
|
|
310
|
-
dispatch_pr_review(card_number, card_key, card_info, pr_number, review, reviewer, repo_name, project_key, project_config, repo_path)
|
|
311
|
-
|
|
312
|
-
[200, { status: "processed", card: card_number, pr: pr_number, reviewer: reviewer, project: project_key }.to_json]
|
|
313
|
-
rescue StandardError => e
|
|
314
|
-
LOG.error "Error handling PR review: #{e.message}"
|
|
315
|
-
[500, { error: e.message }.to_json]
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def dispatch_pr_review(card_number, card_key, card_info, pr_number, review, reviewer, repo_name, project_key, project_config, repo_path)
|
|
319
|
-
review_id = review["id"]
|
|
320
|
-
Thread.new do
|
|
321
|
-
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/pulls/reviews/#{review_id}/reactions",
|
|
322
|
-
"-f", "content=eyes", "-H", "Accept: application/vnd.github+json", chdir: repo_path)
|
|
323
|
-
rescue StandardError => e
|
|
324
|
-
LOG.warn "Could not add reaction to review: #{e.message}"
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
agent_name = agent_name_for(project_config)
|
|
328
|
-
# Notify work item system that a review was received (plugin posts status comment)
|
|
329
|
-
Brainiac.emit(:pr_review_received, card_number: card_number, reviewer: reviewer,
|
|
330
|
-
agent_name: agent_name, project_config: project_config, repo_path: repo_path)
|
|
331
|
-
|
|
332
|
-
review_context = build_review_context(reviewer, review, pr_number, repo_name)
|
|
333
|
-
worktree = card_info["worktree"]
|
|
334
|
-
work_dir = worktree && File.directory?(worktree) ? worktree : repo_path
|
|
335
|
-
|
|
336
|
-
prompt = render_prompt(PROMPT_GITHUB_PR_REVIEW,
|
|
337
|
-
{ "CARD_NUMBER" => card_number, "CARD_ID" => card_number,
|
|
338
|
-
"COMMENT_CREATOR" => reviewer, "REVIEW_CONTEXT" => review_context,
|
|
339
|
-
"PR_NUMBER" => pr_number.to_s, "WORKTREE_PATH" => work_dir },
|
|
340
|
-
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number,
|
|
341
|
-
project_key: project_key),
|
|
342
|
-
agent_name: agent_name, channel: :github)
|
|
343
|
-
|
|
344
|
-
pid, log_file = run_agent(prompt, project_config: project_config, chdir: work_dir,
|
|
345
|
-
log_name: "review-#{card_number}", agent_name: agent_name,
|
|
346
|
-
source: :github,
|
|
347
|
-
source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: work_dir })
|
|
348
|
-
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
def build_review_context(reviewer, review, pr_number, repo_name)
|
|
352
|
-
context = "GitHub PR Review from @#{reviewer}:\n\n"
|
|
353
|
-
context += "Review body:\n#{review["body"]}\n\n" if review["body"] && !review["body"].empty?
|
|
354
|
-
|
|
355
|
-
review_comments = fetch_pr_review_comments(pr_number, repo_name)
|
|
356
|
-
if review_comments.any?
|
|
357
|
-
context += "Line-specific comments:\n"
|
|
358
|
-
review_comments.each do |comment|
|
|
359
|
-
context += "- #{comment["path"]}:#{comment["line"]} (@#{comment["user"]}): #{comment["body"]}\n"
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
context
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
def handle_github_pr_synchronized(payload)
|
|
366
|
-
pr = payload["pull_request"]
|
|
367
|
-
branch = pr.dig("head", "ref")
|
|
368
|
-
|
|
369
|
-
result = find_work_item_by_branch(branch)
|
|
370
|
-
return [200, { status: "ignored", reason: "no matching card" }.to_json] unless result
|
|
371
|
-
|
|
372
|
-
_internal_id, card_info = result
|
|
373
|
-
card_number = card_info["number"]
|
|
374
|
-
worktree = card_info["worktree"]
|
|
375
|
-
|
|
376
|
-
return [200, { status: "ignored", reason: "no worktree" }.to_json] unless worktree && File.directory?(worktree)
|
|
377
|
-
|
|
378
|
-
# Emit hook — deployment plugin handles sync-deploy logic
|
|
379
|
-
results = Brainiac.emit(:pr_synchronized, card_number: card_number, card_info: card_info,
|
|
380
|
-
worktree: worktree, pull_request: pr, branch: branch)
|
|
381
|
-
|
|
382
|
-
if results.any?
|
|
383
|
-
[200, { status: "processed", action: "pr_sync", card: card_number }.to_json]
|
|
384
|
-
else
|
|
385
|
-
[200, { status: "ignored", reason: "no deployment plugin" }.to_json]
|
|
386
|
-
end
|
|
387
|
-
rescue StandardError => e
|
|
388
|
-
LOG.error "[PR Sync] Error: #{e.message}"
|
|
389
|
-
[500, { error: e.message }.to_json]
|
|
390
|
-
end
|