brainiac 0.0.6 → 0.0.7
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 +136 -6
- data/bin/brainiac +382 -11
- data/bin/brainiac-completion.bash +1 -1
- data/lib/brainiac/agents.rb +15 -38
- data/lib/brainiac/config.rb +40 -11
- data/lib/brainiac/handlers/discord/api.rb +196 -0
- data/lib/brainiac/handlers/discord/config.rb +134 -0
- data/lib/brainiac/handlers/discord/delivery.rb +196 -0
- data/lib/brainiac/handlers/discord/gateway.rb +212 -0
- data/lib/brainiac/handlers/discord/message.rb +933 -0
- data/lib/brainiac/handlers/discord/reactions.rb +215 -0
- data/lib/brainiac/handlers/discord.rb +14 -1892
- data/lib/brainiac/handlers/fizzy/assignment.rb +125 -0
- data/lib/brainiac/handlers/fizzy/comments.rb +730 -0
- data/lib/brainiac/handlers/fizzy/dedup.rb +74 -0
- data/lib/brainiac/handlers/fizzy/deploy.rb +152 -0
- data/lib/brainiac/{deployments.rb → handlers/fizzy/deployments.rb} +4 -2
- data/lib/brainiac/handlers/fizzy.rb +12 -1290
- data/lib/brainiac/handlers/github.rb +149 -212
- data/lib/brainiac/handlers/shared/git.rb +203 -0
- data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
- data/lib/brainiac/handlers/zoho.rb +79 -76
- data/lib/brainiac/helpers.rb +2 -121
- data/lib/brainiac/planning.rb +2 -2
- data/lib/brainiac/plugins.rb +129 -0
- data/lib/brainiac/restart.rb +94 -0
- data/lib/brainiac/routes/api.rb +427 -0
- data/lib/brainiac/version.rb +1 -1
- data/lib/brainiac/zoho_mail_api.rb +2 -1
- data/lib/brainiac.rb +7 -0
- data/monitor/daemon.rb +4 -27
- data/monitor/shared.rb +247 -0
- data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +14 -86
- data/monitor/waybar/setup.rb +232 -0
- data/monitor/waybar/status.rb +51 -0
- data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
- data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
- data/monitor/xbar/plugin.rb +155 -0
- data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
- data/receiver.rb +91 -450
- data/templates/brainiac.json.example +9 -0
- data/templates/cli-providers/kiro.json.example +8 -2
- data/templates/plugins.json.example +3 -0
- metadata +30 -20
- data/lib/user_registry.rb +0 -159
- data/monitor/menubar.rb +0 -295
- data/monitor/setup-waybar-deploy-envs.rb +0 -121
- data/monitor/setup-waybar-deployments.rb +0 -96
- data/monitor/setup-waybar-module.rb +0 -113
- data/monitor/setup-xbar-plugin.rb +0 -35
- data/monitor/view-logs.rb +0 -119
- data/monitor/waybar-config-updater.rb +0 -56
- data/monitor/waybar-deployments.rb +0 -239
- data/monitor/waybar.rb +0 -146
- data/monitor/xbar.3s.rb +0 -179
- /data/lib/brainiac/{card_index.rb → handlers/fizzy/card_index.rb} +0 -0
- /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
- /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
|
@@ -75,14 +75,12 @@ def handle_github_pr_merged(payload)
|
|
|
75
75
|
pr_title = pr["title"]
|
|
76
76
|
repo_full_name = payload.dig("repository", "full_name")
|
|
77
77
|
|
|
78
|
-
# Only act on merges into the repo's default branch
|
|
79
78
|
default_branch = payload.dig("repository", "default_branch") || "main"
|
|
80
79
|
unless base == default_branch
|
|
81
80
|
LOG.info "PR merged into #{base}, not #{default_branch} — ignoring"
|
|
82
81
|
return [200, { status: "ignored", reason: "not merged into #{default_branch}" }.to_json]
|
|
83
82
|
end
|
|
84
83
|
|
|
85
|
-
# Identify project by GitHub repo
|
|
86
84
|
project_result = identify_project_by_repo(repo_full_name)
|
|
87
85
|
unless project_result
|
|
88
86
|
LOG.info "No project found for GitHub repo #{repo_full_name}"
|
|
@@ -100,62 +98,53 @@ def handle_github_pr_merged(payload)
|
|
|
100
98
|
|
|
101
99
|
internal_id, card_info = result
|
|
102
100
|
card_number = card_info["number"]
|
|
103
|
-
|
|
104
101
|
unless card_number
|
|
105
102
|
LOG.warn "Card #{internal_id} has no number — can't comment or move"
|
|
106
103
|
return [200, { status: "ignored", reason: "card has no number" }.to_json]
|
|
107
104
|
end
|
|
108
105
|
|
|
109
106
|
LOG.info "PR merged into main for card ##{card_number} (project: #{project_key}): #{pr_url}"
|
|
107
|
+
process_merged_pr(card_info, card_number, branch, pr, pr_url, pr_title, project_key, project_config, repo_path)
|
|
108
|
+
|
|
109
|
+
[200, { status: "processed", card: card_number, pr: pr_url, action: "merged_to_uat", project: project_key }.to_json]
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
LOG.error "Error handling merged PR: #{e.message}"
|
|
112
|
+
[500, { error: e.message }.to_json]
|
|
113
|
+
end
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
def process_merged_pr(card_info, card_number, branch, pull_request, pr_url, pr_title, project_key, project_config, repo_path)
|
|
112
116
|
card_agent = card_info["agent"]
|
|
113
117
|
card_fizzy_env = fizzy_env_for(card_agent)
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
if pr_link_already_commented?(card_number, pr_url, chdir: repo_path, env: card_fizzy_env)
|
|
117
|
-
LOG.info "PR link already on card ##{card_number}, skipping comment"
|
|
118
|
-
else
|
|
119
|
+
unless pr_link_already_commented?(card_number, pr_url, chdir: repo_path, env: card_fizzy_env)
|
|
119
120
|
comment_body = "<p>PR merged into main: <a href=\"#{pr_url}\">#{pr_title}</a></p><p>Branch: <code>#{branch}</code></p>"
|
|
120
121
|
run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", comment_body, chdir: repo_path, env: card_fizzy_env)
|
|
121
|
-
LOG.info "Commented PR link on card ##{card_number} as #{card_agent || "default"}"
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
-
# Move card to UAT column — merged into main means it's deployed to UAT
|
|
125
124
|
mark_card_merged(card_number)
|
|
126
125
|
run_cmd("fizzy", "card", "column", card_number.to_s, "--column", uat_column_id(project_config), chdir: repo_path, env: card_fizzy_env)
|
|
127
126
|
record_self_move(card_number)
|
|
128
|
-
LOG.info "Moved card ##{card_number} to UAT column as #{card_agent || "default"}"
|
|
129
|
-
|
|
130
|
-
# Clean up the primary worktree and any cross-agent review worktrees
|
|
131
127
|
cleanup_card_worktrees(card_number, repo_path: repo_path, primary_worktree: card_info["worktree"], primary_branch: branch)
|
|
132
|
-
|
|
133
|
-
# Clear any deployment environments occupied by this card
|
|
134
128
|
clear_deployment_for_card(card_number)
|
|
135
129
|
|
|
136
|
-
# Dispatch agent to comment UAT testing steps on the Fizzy card
|
|
137
130
|
agent_name = card_agent || agent_name_for(project_config)
|
|
138
131
|
card_title = card_info["title"] || pr_title
|
|
132
|
+
dispatch_uat_agent(card_number, card_title, pull_request["number"].to_s, agent_name, project_key, project_config, repo_path)
|
|
133
|
+
end
|
|
139
134
|
|
|
135
|
+
def dispatch_uat_agent(card_number, card_title, pr_number, agent_name, project_key, project_config, repo_path)
|
|
140
136
|
prompt = render_prompt(PROMPT_GITHUB_UAT,
|
|
141
|
-
{ "CARD_NUMBER" => card_number,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
project_key: project_key),
|
|
146
|
-
agent_name: agent_name,
|
|
147
|
-
channel: :fizzy,
|
|
137
|
+
{ "CARD_NUMBER" => card_number, "CARD_TITLE" => card_title, "PR_NUMBER" => pr_number },
|
|
138
|
+
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number,
|
|
139
|
+
card_title: card_title, project_key: project_key),
|
|
140
|
+
agent_name: agent_name, channel: :fizzy,
|
|
148
141
|
board_key: board_key_for_project(project_config))
|
|
149
142
|
|
|
150
|
-
pid, log_file = run_agent(prompt, project_config: project_config, chdir: repo_path,
|
|
143
|
+
pid, log_file = run_agent(prompt, project_config: project_config, chdir: repo_path,
|
|
144
|
+
log_name: "uat-#{card_number}", agent_name: agent_name,
|
|
151
145
|
source: :fizzy, source_context: { card_number: card_number }, skip_column_move: true)
|
|
152
146
|
register_session("card-#{card_number}", pid, log_file: log_file, agent_name: agent_name)
|
|
153
147
|
LOG.info "Dispatched #{agent_name} for UAT testing steps on card ##{card_number}"
|
|
154
|
-
|
|
155
|
-
[200, { status: "processed", card: card_number, pr: pr_url, action: "merged_to_uat", project: project_key }.to_json]
|
|
156
|
-
rescue StandardError => e
|
|
157
|
-
LOG.error "Error handling merged PR: #{e.message}"
|
|
158
|
-
[500, { error: e.message }.to_json]
|
|
159
148
|
end
|
|
160
149
|
|
|
161
150
|
def handle_github_issue_comment(payload)
|
|
@@ -166,13 +155,11 @@ def handle_github_issue_comment(payload)
|
|
|
166
155
|
comment_user = comment.dig("user", "login")
|
|
167
156
|
repo_name = payload.dig("repository", "full_name")
|
|
168
157
|
|
|
169
|
-
# Only process if this is a PR (issues have pull_request key when they're PRs)
|
|
170
158
|
unless issue["pull_request"]
|
|
171
159
|
LOG.info "Issue comment on non-PR issue ##{issue["number"]}, ignoring"
|
|
172
160
|
return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
|
|
173
161
|
end
|
|
174
162
|
|
|
175
|
-
# Identify project by GitHub repo
|
|
176
163
|
project_result = identify_project_by_repo(repo_name)
|
|
177
164
|
unless project_result
|
|
178
165
|
LOG.info "No project found for GitHub repo #{repo_name}"
|
|
@@ -180,12 +167,10 @@ def handle_github_issue_comment(payload)
|
|
|
180
167
|
end
|
|
181
168
|
|
|
182
169
|
project_key, project_config = project_result
|
|
183
|
-
|
|
184
170
|
pr_number = issue["number"]
|
|
185
|
-
issue["html_url"]
|
|
186
171
|
|
|
187
|
-
|
|
188
|
-
|
|
172
|
+
pr_data = run_cmd("gh", "api", "/repos/#{repo_name}/pulls/#{pr_number}", "--jq", "{branch: .head.ref}",
|
|
173
|
+
chdir: project_config["repo_path"])
|
|
189
174
|
branch = JSON.parse(pr_data)["branch"]
|
|
190
175
|
|
|
191
176
|
result = find_card_by_branch(branch)
|
|
@@ -198,54 +183,52 @@ def handle_github_issue_comment(payload)
|
|
|
198
183
|
card_number = card_info["number"]
|
|
199
184
|
worktree = card_info["worktree"]
|
|
200
185
|
|
|
201
|
-
# Only process if worktree exists
|
|
202
186
|
unless worktree && File.directory?(worktree)
|
|
203
187
|
LOG.info "No active worktree for PR ##{pr_number}, ignoring comment"
|
|
204
188
|
return [200, { status: "ignored", reason: "no active worktree" }.to_json]
|
|
205
189
|
end
|
|
206
190
|
|
|
207
|
-
LOG.info "PR comment from #{comment_user} on PR ##{pr_number} for card ##{card_number} (project: #{project_key})"
|
|
208
|
-
|
|
209
191
|
card_key = "card-#{card_number}"
|
|
210
192
|
if session_active?(card_key)
|
|
211
193
|
LOG.info "Skipping PR comment on card ##{card_number} — agent session already active"
|
|
212
194
|
return [200, { status: "ignored", reason: "session already active" }.to_json]
|
|
213
195
|
end
|
|
214
196
|
|
|
215
|
-
#
|
|
197
|
+
LOG.info "PR comment from #{comment_user} on PR ##{pr_number} for card ##{card_number} (project: #{project_key})"
|
|
198
|
+
dispatch_pr_comment(card_number, card_key, pr_number, comment_id, comment_user, comment_body,
|
|
199
|
+
repo_name, worktree, project_key, project_config)
|
|
200
|
+
|
|
201
|
+
[200, { status: "processed", card: card_number, pr: pr_number, comment_id: comment_id, project: project_key }.to_json]
|
|
202
|
+
rescue StandardError => e
|
|
203
|
+
LOG.error "Error handling PR comment: #{e.message}"
|
|
204
|
+
[500, { error: e.message }.to_json]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def dispatch_pr_comment(card_number, card_key, pr_number, comment_id, comment_user, comment_body,
|
|
208
|
+
repo_name, worktree, project_key, project_config)
|
|
216
209
|
Thread.new do
|
|
217
|
-
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/issues/comments/#{comment_id}/reactions",
|
|
218
|
-
"Accept: application/vnd.github+json", chdir: worktree)
|
|
219
|
-
LOG.info "Added 👀 reaction to comment ##{comment_id}"
|
|
210
|
+
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/issues/comments/#{comment_id}/reactions",
|
|
211
|
+
"-f", "content=eyes", "-H", "Accept: application/vnd.github+json", chdir: worktree)
|
|
220
212
|
rescue StandardError => e
|
|
221
213
|
LOG.warn "Could not add reaction to comment: #{e.message}"
|
|
222
214
|
end
|
|
223
215
|
|
|
224
|
-
# Detect model override
|
|
225
|
-
model = detect_model(project_config, text: comment_body)
|
|
226
|
-
effort = detect_effort(project_config, text: comment_body)
|
|
227
216
|
agent_name = agent_name_for(project_config)
|
|
228
|
-
|
|
229
217
|
prompt = render_prompt(PROMPT_GITHUB_PR_COMMENT,
|
|
230
|
-
{ "CARD_NUMBER" => card_number,
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
218
|
+
{ "CARD_NUMBER" => card_number, "CARD_ID" => card_number,
|
|
219
|
+
"COMMENT_CREATOR" => comment_user, "COMMENT_BODY" => comment_body,
|
|
220
|
+
"PR_NUMBER" => pr_number.to_s, "WORKTREE_PATH" => worktree },
|
|
221
|
+
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number,
|
|
222
|
+
project_key: project_key, comment_body: comment_body),
|
|
223
|
+
agent_name: agent_name, channel: :github)
|
|
224
|
+
|
|
225
|
+
pid, log_file = run_agent(prompt, project_config: project_config, chdir: worktree,
|
|
226
|
+
log_name: "pr-comment-#{pr_number}",
|
|
227
|
+
model: detect_model(project_config, text: comment_body),
|
|
228
|
+
effort: detect_effort(project_config, text: comment_body),
|
|
229
|
+
agent_name: agent_name, source: :github,
|
|
230
|
+
source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: worktree })
|
|
243
231
|
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
244
|
-
|
|
245
|
-
[200, { status: "processed", card: card_number, pr: pr_number, comment_id: comment_id, project: project_key }.to_json]
|
|
246
|
-
rescue StandardError => e
|
|
247
|
-
LOG.error "Error handling PR comment: #{e.message}"
|
|
248
|
-
[500, { error: e.message }.to_json]
|
|
249
232
|
end
|
|
250
233
|
|
|
251
234
|
def handle_github_workflow_run(payload)
|
|
@@ -255,75 +238,70 @@ def handle_github_workflow_run(payload)
|
|
|
255
238
|
repo_full_name = payload.dig("repository", "full_name")
|
|
256
239
|
run_url = workflow["html_url"]
|
|
257
240
|
|
|
258
|
-
# Handle Deploy to Production failure
|
|
259
241
|
if workflow_name == "Deploy to Production" && conclusion == "failure"
|
|
260
|
-
|
|
261
|
-
project_key = project_result ? project_result[0] : repo_full_name
|
|
242
|
+
project_key = identify_project_by_repo(repo_full_name)&.first || repo_full_name
|
|
262
243
|
send_workflow_failure_notification(project_key, workflow_name, run_url)
|
|
263
|
-
LOG.info "Deploy to Production failed for #{project_key} — notified Discord"
|
|
264
244
|
return [200, { status: "processed", action: "prod_deploy_failure_notified", project: project_key }.to_json]
|
|
265
245
|
end
|
|
266
246
|
|
|
267
|
-
# Handle Deploy to UAT success
|
|
268
247
|
if workflow_name == "Deploy to UAT" && conclusion == "success"
|
|
269
|
-
|
|
270
|
-
project_key = project_result ? project_result[0] : repo_full_name
|
|
248
|
+
project_key = identify_project_by_repo(repo_full_name)&.first || repo_full_name
|
|
271
249
|
send_uat_deploy_notification(project_key)
|
|
272
|
-
LOG.info "Deploy to UAT succeeded for #{project_key} — notified Discord"
|
|
273
250
|
return [200, { status: "processed", action: "uat_deploy_notified", project: project_key }.to_json]
|
|
274
251
|
end
|
|
275
252
|
|
|
276
|
-
unless conclusion == "success"
|
|
277
|
-
LOG.info "Workflow '#{workflow_name}' concluded with '#{conclusion}' — ignoring"
|
|
278
|
-
return [200, { status: "ignored", reason: "conclusion: #{conclusion}" }.to_json]
|
|
279
|
-
end
|
|
253
|
+
return [200, { status: "ignored", reason: "conclusion: #{conclusion}" }.to_json] unless conclusion == "success"
|
|
280
254
|
|
|
281
|
-
unless workflow_name == "Deploy to Production"
|
|
282
|
-
LOG.info "Workflow '#{workflow_name}' is not a prod deploy — ignoring"
|
|
283
|
-
return [200, { status: "ignored", reason: "workflow: #{workflow_name}" }.to_json]
|
|
284
|
-
end
|
|
255
|
+
return [200, { status: "ignored", reason: "workflow: #{workflow_name}" }.to_json] unless workflow_name == "Deploy to Production"
|
|
285
256
|
|
|
286
257
|
project_result = identify_project_by_repo(repo_full_name)
|
|
287
|
-
unless project_result
|
|
288
|
-
LOG.info "No project found for GitHub repo #{repo_full_name}"
|
|
289
|
-
return [200, { status: "ignored", reason: "no matching project" }.to_json]
|
|
290
|
-
end
|
|
258
|
+
return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_result
|
|
291
259
|
|
|
292
260
|
project_key, project_config = project_result
|
|
293
|
-
|
|
261
|
+
close_uat_cards_after_deploy(project_key, project_config)
|
|
262
|
+
rescue StandardError => e
|
|
263
|
+
LOG.error "Error handling workflow run: #{e.message}"
|
|
264
|
+
[500, { error: e.message }.to_json]
|
|
265
|
+
end
|
|
294
266
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
267
|
+
def close_uat_cards_after_deploy(project_key, project_config)
|
|
268
|
+
repo_path = project_config["repo_path"]
|
|
269
|
+
output = run_cmd("fizzy", "card", "list", "--column", uat_column_id(project_config), "--all",
|
|
270
|
+
chdir: repo_path, env: default_fizzy_env)
|
|
271
|
+
card_list = JSON.parse(output)["data"] || []
|
|
299
272
|
|
|
300
273
|
if card_list.empty?
|
|
301
274
|
LOG.info "No cards in UAT column — nothing to close"
|
|
302
275
|
return [200, { status: "processed", action: "no_uat_cards", project: project_key }.to_json]
|
|
303
276
|
end
|
|
304
277
|
|
|
278
|
+
closed_cards = close_and_cleanup_uat_cards(card_list, repo_path)
|
|
279
|
+
send_deploy_notification(project_key, closed_cards) if closed_cards.any?
|
|
280
|
+
|
|
281
|
+
LOG.info "Prod deploy complete — closed #{closed_cards.size} UAT cards: #{closed_cards.map { |c| c[:number] }.join(", ")}"
|
|
282
|
+
[200, { status: "processed", action: "prod_deploy_closed_uat",
|
|
283
|
+
closed_cards: closed_cards.map { |c| c[:number] }, project: project_key }.to_json]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def close_and_cleanup_uat_cards(card_list, repo_path)
|
|
305
287
|
closed_cards = []
|
|
306
288
|
map = load_card_map
|
|
289
|
+
|
|
307
290
|
card_list.each do |card|
|
|
308
291
|
card_number = card["number"]
|
|
309
292
|
next unless card_number
|
|
310
293
|
|
|
311
|
-
# Try to find the card in our map to get the assigned agent
|
|
312
294
|
map_entry = map.values.find { |info| info["number"] == card_number }
|
|
313
295
|
agent_name = map_entry["agent"] if map_entry
|
|
314
|
-
|
|
315
296
|
env = agent_name ? fizzy_env_for(agent_name) : default_fizzy_env
|
|
316
297
|
|
|
317
|
-
|
|
318
|
-
|
|
298
|
+
run_cmd("fizzy", "comment", "create", "--card", card_number.to_s,
|
|
299
|
+
"--body", "<p>✅ Deployed to production. Closing card.</p>", chdir: repo_path, env: env)
|
|
319
300
|
run_cmd("fizzy", "card", "close", card_number.to_s, chdir: repo_path, env: env)
|
|
320
301
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
primary_branch = map_entry&.dig("branch")
|
|
324
|
-
cleanup_card_worktrees(card_number, repo_path: repo_path, primary_worktree: primary_worktree, primary_branch: primary_branch)
|
|
302
|
+
cleanup_card_worktrees(card_number, repo_path: repo_path,
|
|
303
|
+
primary_worktree: map_entry&.dig("worktree"), primary_branch: map_entry&.dig("branch"))
|
|
325
304
|
|
|
326
|
-
# Clean up card map entry
|
|
327
305
|
if map_entry
|
|
328
306
|
internal_id = map.key(map_entry)
|
|
329
307
|
map.delete(internal_id)
|
|
@@ -334,14 +312,7 @@ def handle_github_workflow_run(payload)
|
|
|
334
312
|
end
|
|
335
313
|
|
|
336
314
|
save_card_map(map) if closed_cards.any?
|
|
337
|
-
|
|
338
|
-
send_deploy_notification(project_key, closed_cards) if closed_cards.any?
|
|
339
|
-
|
|
340
|
-
LOG.info "Prod deploy complete — closed #{closed_cards.size} UAT cards: #{closed_cards.map { |c| c[:number] }.join(", ")}"
|
|
341
|
-
[200, { status: "processed", action: "prod_deploy_closed_uat", closed_cards: closed_cards.map { |c| c[:number] }, project: project_key }.to_json]
|
|
342
|
-
rescue StandardError => e
|
|
343
|
-
LOG.error "Error handling workflow run: #{e.message}"
|
|
344
|
-
[500, { error: e.message }.to_json]
|
|
315
|
+
closed_cards
|
|
345
316
|
end
|
|
346
317
|
|
|
347
318
|
def send_deploy_notification(project_key, closed_cards)
|
|
@@ -402,171 +373,137 @@ def handle_github_pr_review_submitted(payload)
|
|
|
402
373
|
review = payload["review"]
|
|
403
374
|
branch = pr.dig("head", "ref")
|
|
404
375
|
pr_number = pr["number"]
|
|
405
|
-
pr["html_url"]
|
|
406
376
|
repo_name = payload.dig("repository", "full_name")
|
|
407
377
|
review_state = review["state"]
|
|
408
378
|
reviewer = review.dig("user", "login")
|
|
409
379
|
|
|
410
|
-
|
|
411
|
-
unless %w[changes_requested commented].include?(review_state)
|
|
412
|
-
LOG.info "PR review state is '#{review_state}', ignoring"
|
|
413
|
-
return [200, { status: "ignored", reason: "review state: #{review_state}" }.to_json]
|
|
414
|
-
end
|
|
380
|
+
return [200, { status: "ignored", reason: "review state: #{review_state}" }.to_json] unless %w[changes_requested commented].include?(review_state)
|
|
415
381
|
|
|
416
|
-
# Identify project by GitHub repo
|
|
417
382
|
project_result = identify_project_by_repo(repo_name)
|
|
418
|
-
unless project_result
|
|
419
|
-
LOG.info "No project found for GitHub repo #{repo_name}"
|
|
420
|
-
return [200, { status: "ignored", reason: "no matching project" }.to_json]
|
|
421
|
-
end
|
|
383
|
+
return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_result
|
|
422
384
|
|
|
423
385
|
project_key, project_config = project_result
|
|
424
386
|
repo_path = project_config["repo_path"]
|
|
425
387
|
|
|
426
388
|
result = find_card_by_branch(branch)
|
|
427
|
-
unless result
|
|
428
|
-
LOG.info "No Fizzy card found for PR branch #{branch}"
|
|
429
|
-
return [200, { status: "ignored", reason: "no matching card" }.to_json]
|
|
430
|
-
end
|
|
389
|
+
return [200, { status: "ignored", reason: "no matching card" }.to_json] unless result
|
|
431
390
|
|
|
432
391
|
internal_id, card_info = result
|
|
433
392
|
card_number = card_info["number"]
|
|
434
|
-
worktree = card_info["worktree"]
|
|
435
|
-
|
|
436
393
|
unless card_number
|
|
437
394
|
LOG.warn "Card #{internal_id} has no number — can't comment"
|
|
438
395
|
return [200, { status: "ignored", reason: "card has no number" }.to_json]
|
|
439
396
|
end
|
|
440
397
|
|
|
398
|
+
card_key = "card-#{card_number}"
|
|
399
|
+
return [200, { status: "ignored", reason: "session already active" }.to_json] if session_active?(card_key)
|
|
400
|
+
|
|
441
401
|
LOG.info "PR review submitted by #{reviewer} on PR ##{pr_number} for card ##{card_number} (project: #{project_key})"
|
|
402
|
+
dispatch_pr_review(card_number, card_key, card_info, pr_number, review, reviewer, repo_name, project_key, project_config, repo_path)
|
|
442
403
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
404
|
+
[200, { status: "processed", card: card_number, pr: pr_number, reviewer: reviewer, project: project_key }.to_json]
|
|
405
|
+
rescue StandardError => e
|
|
406
|
+
LOG.error "Error handling PR review: #{e.message}"
|
|
407
|
+
[500, { error: e.message }.to_json]
|
|
408
|
+
end
|
|
448
409
|
|
|
449
|
-
|
|
410
|
+
def dispatch_pr_review(card_number, card_key, card_info, pr_number, review, reviewer, repo_name, project_key, project_config, repo_path)
|
|
450
411
|
review_id = review["id"]
|
|
451
|
-
# React in background — don't block the dispatch path
|
|
452
412
|
Thread.new do
|
|
453
|
-
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/pulls/reviews/#{review_id}/reactions",
|
|
454
|
-
"Accept: application/vnd.github+json", chdir: repo_path)
|
|
455
|
-
LOG.info "Added 👀 reaction to review ##{review_id}"
|
|
413
|
+
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/pulls/reviews/#{review_id}/reactions",
|
|
414
|
+
"-f", "content=eyes", "-H", "Accept: application/vnd.github+json", chdir: repo_path)
|
|
456
415
|
rescue StandardError => e
|
|
457
416
|
LOG.warn "Could not add reaction to review: #{e.message}"
|
|
458
417
|
end
|
|
459
418
|
|
|
460
|
-
# Post status to Fizzy in background — don't block the dispatch path
|
|
461
419
|
agent_name = agent_name_for(project_config)
|
|
462
420
|
Thread.new do
|
|
463
421
|
status_comment = "<p>🔄 Code review received from @#{reviewer}. Updates in progress...</p>"
|
|
464
|
-
run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", status_comment,
|
|
465
|
-
|
|
422
|
+
run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", status_comment,
|
|
423
|
+
chdir: repo_path, env: fizzy_env_for(agent_name))
|
|
466
424
|
rescue StandardError => e
|
|
467
425
|
LOG.warn "Could not post status update to card ##{card_number}: #{e.message}"
|
|
468
426
|
end
|
|
469
427
|
|
|
470
|
-
|
|
471
|
-
|
|
428
|
+
review_context = build_review_context(reviewer, review, pr_number, repo_name)
|
|
429
|
+
worktree = card_info["worktree"]
|
|
430
|
+
work_dir = worktree && File.directory?(worktree) ? worktree : repo_path
|
|
472
431
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
432
|
+
prompt = render_prompt(PROMPT_GITHUB_PR_REVIEW,
|
|
433
|
+
{ "CARD_NUMBER" => card_number, "CARD_ID" => card_number,
|
|
434
|
+
"COMMENT_CREATOR" => reviewer, "REVIEW_CONTEXT" => review_context,
|
|
435
|
+
"PR_NUMBER" => pr_number.to_s, "WORKTREE_PATH" => work_dir },
|
|
436
|
+
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number,
|
|
437
|
+
project_key: project_key),
|
|
438
|
+
agent_name: agent_name, channel: :github)
|
|
476
439
|
|
|
440
|
+
pid, log_file = run_agent(prompt, project_config: project_config, chdir: work_dir,
|
|
441
|
+
log_name: "review-#{card_number}", agent_name: agent_name,
|
|
442
|
+
source: :github,
|
|
443
|
+
source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: work_dir })
|
|
444
|
+
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def build_review_context(reviewer, review, pr_number, repo_name)
|
|
448
|
+
context = "GitHub PR Review from @#{reviewer}:\n\n"
|
|
449
|
+
context += "Review body:\n#{review["body"]}\n\n" if review["body"] && !review["body"].empty?
|
|
450
|
+
|
|
451
|
+
review_comments = fetch_pr_review_comments(pr_number, repo_name)
|
|
477
452
|
if review_comments.any?
|
|
478
|
-
|
|
453
|
+
context += "Line-specific comments:\n"
|
|
479
454
|
review_comments.each do |comment|
|
|
480
|
-
|
|
455
|
+
context += "- #{comment["path"]}:#{comment["line"]} (@#{comment["user"]}): #{comment["body"]}\n"
|
|
481
456
|
end
|
|
482
457
|
end
|
|
483
|
-
|
|
484
|
-
# Determine working directory
|
|
485
|
-
work_dir = worktree && File.directory?(worktree) ? worktree : repo_path
|
|
486
|
-
|
|
487
|
-
prompt = render_prompt(PROMPT_GITHUB_PR_REVIEW,
|
|
488
|
-
{ "CARD_NUMBER" => card_number,
|
|
489
|
-
"CARD_ID" => card_number,
|
|
490
|
-
"COMMENT_CREATOR" => reviewer,
|
|
491
|
-
"REVIEW_CONTEXT" => review_context,
|
|
492
|
-
"PR_NUMBER" => pr_number.to_s,
|
|
493
|
-
"WORKTREE_PATH" => work_dir },
|
|
494
|
-
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number, project_key: project_key),
|
|
495
|
-
agent_name: agent_name,
|
|
496
|
-
channel: :github)
|
|
497
|
-
|
|
498
|
-
pid, log_file = run_agent(prompt, project_config: project_config, chdir: work_dir, log_name: "review-#{card_number}", agent_name: agent_name,
|
|
499
|
-
source: :github, source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: work_dir })
|
|
500
|
-
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
501
|
-
|
|
502
|
-
[200, { status: "processed", card: card_number, pr: pr_number, reviewer: reviewer, project: project_key }.to_json]
|
|
503
|
-
rescue StandardError => e
|
|
504
|
-
LOG.error "Error handling PR review: #{e.message}"
|
|
505
|
-
[500, { error: e.message }.to_json]
|
|
458
|
+
context
|
|
506
459
|
end
|
|
507
460
|
|
|
508
|
-
# Auto-deploy when a PR gets new commits (synchronize event) if the card is already on a dev env.
|
|
509
461
|
def handle_github_pr_synchronized(payload)
|
|
510
462
|
pr = payload["pull_request"]
|
|
511
463
|
branch = pr.dig("head", "ref")
|
|
512
|
-
payload.dig("repository", "full_name")
|
|
513
464
|
|
|
514
465
|
result = find_card_by_branch(branch)
|
|
515
|
-
unless result
|
|
516
|
-
LOG.info "[PR Sync] No card found for branch #{branch}"
|
|
517
|
-
return [200, { status: "ignored", reason: "no matching card" }.to_json]
|
|
518
|
-
end
|
|
466
|
+
return [200, { status: "ignored", reason: "no matching card" }.to_json] unless result
|
|
519
467
|
|
|
520
468
|
_internal_id, card_info = result
|
|
521
469
|
card_number = card_info["number"]
|
|
522
470
|
worktree = card_info["worktree"]
|
|
523
471
|
|
|
524
|
-
unless worktree && File.directory?(worktree)
|
|
525
|
-
LOG.info "[PR Sync] No worktree for card ##{card_number}"
|
|
526
|
-
return [200, { status: "ignored", reason: "no worktree" }.to_json]
|
|
527
|
-
end
|
|
472
|
+
return [200, { status: "ignored", reason: "no worktree" }.to_json] unless worktree && File.directory?(worktree)
|
|
528
473
|
|
|
529
|
-
# Check if this card is deployed to any environment
|
|
530
474
|
state = load_deployment_state
|
|
531
475
|
config = DEPLOYMENTS_CONFIG["environments"] || {}
|
|
532
476
|
env_key = state.find { |_k, v| v["card_number"] == card_number && v["status"] == "occupied" }&.first
|
|
533
477
|
|
|
534
|
-
unless env_key
|
|
535
|
-
LOG.info "[PR Sync] Card ##{card_number} not deployed to any environment — skipping"
|
|
536
|
-
return [200, { status: "ignored", reason: "card not deployed" }.to_json]
|
|
537
|
-
end
|
|
478
|
+
return [200, { status: "ignored", reason: "card not deployed" }.to_json] unless env_key
|
|
538
479
|
|
|
539
|
-
# Only deploy if this machine owns the environment
|
|
540
480
|
env_owner = config.dig(env_key, "owner")
|
|
541
|
-
unless env_owner && env_owner.downcase == AI_AGENT_NAME.downcase
|
|
542
|
-
LOG.info "[PR Sync] Skipping #{env_key} — owner is #{env_owner.inspect}, this machine is #{AI_AGENT_NAME}"
|
|
543
|
-
return [200, { status: "ignored", reason: "not env owner" }.to_json]
|
|
544
|
-
end
|
|
481
|
+
return [200, { status: "ignored", reason: "not env owner" }.to_json] unless env_owner && env_owner.downcase == AI_AGENT_NAME.downcase
|
|
545
482
|
|
|
546
|
-
|
|
547
|
-
if on_deploy_cooldown?(env_key)
|
|
548
|
-
LOG.info "[PR Sync] Skipping deploy to #{env_key} — within #{DEPLOY_COOLDOWN}s cooldown"
|
|
549
|
-
return [200, { status: "ignored", reason: "deploy cooldown" }.to_json]
|
|
550
|
-
end
|
|
483
|
+
return [200, { status: "ignored", reason: "deploy cooldown" }.to_json] if on_deploy_cooldown?(env_key)
|
|
551
484
|
|
|
552
485
|
touch_deploy_cooldown(env_key)
|
|
553
486
|
|
|
554
|
-
# Pull latest commits into the worktree
|
|
555
487
|
system("git", "pull", "--ff-only", chdir: worktree)
|
|
556
|
-
|
|
557
488
|
deploy_script = File.join(worktree, "scripts", "deploy.sh")
|
|
558
|
-
unless File.exist?(deploy_script)
|
|
559
|
-
LOG.warn "[PR Sync] No deploy script at #{deploy_script}"
|
|
560
|
-
return [200, { status: "ignored", reason: "no deploy script" }.to_json]
|
|
561
|
-
end
|
|
489
|
+
return [200, { status: "ignored", reason: "no deploy script" }.to_json] unless File.exist?(deploy_script)
|
|
562
490
|
|
|
563
491
|
LOG.info "[PR Sync] Auto-deploying card ##{card_number} to #{env_key} (PR updated)"
|
|
564
492
|
mark_deploying(env_key, worktree_path: worktree)
|
|
493
|
+
run_pr_sync_deploy(env_key, card_number, worktree, config)
|
|
494
|
+
|
|
495
|
+
[200, { status: "processed", action: "pr_sync_deploy", card: card_number, env: env_key }.to_json]
|
|
496
|
+
rescue StandardError => e
|
|
497
|
+
LOG.error "[PR Sync] Error: #{e.message}"
|
|
498
|
+
[500, { error: e.message }.to_json]
|
|
499
|
+
end
|
|
565
500
|
|
|
501
|
+
def run_pr_sync_deploy(env_key, card_number, worktree, config)
|
|
566
502
|
Thread.new do
|
|
567
503
|
deploy_env = {}
|
|
568
504
|
aws_profile = config.dig(env_key, "aws_profile")
|
|
569
505
|
deploy_env["AWS_PROFILE"] = aws_profile if aws_profile
|
|
506
|
+
deploy_script = File.join(worktree, "scripts", "deploy.sh")
|
|
570
507
|
|
|
571
508
|
stdout, stderr, status = Open3.capture3(deploy_env, deploy_script, env_key, chdir: worktree)
|
|
572
509
|
|
|
@@ -574,25 +511,25 @@ def handle_github_pr_synchronized(payload)
|
|
|
574
511
|
deploy_to_environment(env_key, worktree_path: worktree, deployed_by: "pr-sync")
|
|
575
512
|
LOG.info "[PR Sync] Deploy to #{env_key} succeeded for card ##{card_number}"
|
|
576
513
|
elsif terraform_lock_error?(stdout, stderr)
|
|
577
|
-
|
|
578
|
-
FileUtils.rm_f(lock_file)
|
|
579
|
-
Open3.capture3("terraform", "init", "-upgrade", chdir: File.join(worktree, "infrastructure/#{env_key}"))
|
|
580
|
-
stdout2, stderr2, status2 = Open3.capture3(deploy_env, deploy_script, env_key, chdir: worktree)
|
|
581
|
-
if status2.success?
|
|
582
|
-
deploy_to_environment(env_key, worktree_path: worktree, deployed_by: "pr-sync")
|
|
583
|
-
LOG.info "[PR Sync] Deploy to #{env_key} succeeded (after terraform lock fix) for card ##{card_number}"
|
|
584
|
-
else
|
|
585
|
-
record_deploy_failure(env_key, worktree_path: worktree, stdout: stdout2, stderr: stderr2)
|
|
586
|
-
LOG.error "[PR Sync] Deploy to #{env_key} failed (after retry) for card ##{card_number}"
|
|
587
|
-
end
|
|
514
|
+
retry_pr_sync_deploy(deploy_env, deploy_script, env_key, card_number, worktree)
|
|
588
515
|
else
|
|
589
516
|
record_deploy_failure(env_key, worktree_path: worktree, stdout: stdout, stderr: stderr)
|
|
590
517
|
LOG.error "[PR Sync] Deploy to #{env_key} failed for card ##{card_number}"
|
|
591
518
|
end
|
|
592
519
|
end
|
|
520
|
+
end
|
|
593
521
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
522
|
+
def retry_pr_sync_deploy(deploy_env, deploy_script, env_key, card_number, worktree)
|
|
523
|
+
infra_dir = File.join(worktree, "infrastructure/#{env_key}")
|
|
524
|
+
FileUtils.rm_f(File.join(infra_dir, ".terraform.lock.hcl"))
|
|
525
|
+
Open3.capture3("terraform", "init", "-upgrade", chdir: infra_dir)
|
|
526
|
+
stdout, stderr, status = Open3.capture3(deploy_env, deploy_script, env_key, chdir: worktree)
|
|
527
|
+
|
|
528
|
+
if status.success?
|
|
529
|
+
deploy_to_environment(env_key, worktree_path: worktree, deployed_by: "pr-sync")
|
|
530
|
+
LOG.info "[PR Sync] Deploy to #{env_key} succeeded (after terraform lock fix) for card ##{card_number}"
|
|
531
|
+
else
|
|
532
|
+
record_deploy_failure(env_key, worktree_path: worktree, stdout: stdout, stderr: stderr)
|
|
533
|
+
LOG.error "[PR Sync] Deploy to #{env_key} failed (after retry) for card ##{card_number}"
|
|
534
|
+
end
|
|
598
535
|
end
|