brainiac-fizzy 0.0.25 → 0.0.27
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/lib/brainiac/plugins/fizzy/cli.rb +67 -2
- data/lib/brainiac/plugins/fizzy/delegators.rb +12 -0
- data/lib/brainiac/plugins/fizzy/env_url.rb +73 -0
- data/lib/brainiac/plugins/fizzy/handlers/assignment.rb +137 -0
- data/lib/brainiac/plugins/fizzy/handlers/comments.rb +6 -0
- data/lib/brainiac/plugins/fizzy/helpers.rb +76 -6
- data/lib/brainiac/plugins/fizzy/version.rb +1 -1
- data/lib/brainiac/plugins/fizzy.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dabc51c154f2caad4353e83a1c837a961bf1df27139ede86181929c8fa28290d
|
|
4
|
+
data.tar.gz: 6edde302d45dfe3628606cd005ca0ea6e3964d004bc6fe0c4c93be2d1590800d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac4ce9b1192051ce0d748d13c1ecdc834935bc503513fb101486460cfc478046749475d5eaa7cfff32252ba34e2be65efad10f3a9aecd6d8b1bdf2aa0c10cd33
|
|
7
|
+
data.tar.gz: 24afb9c9f9af9f481be0df59afeb94546e49546033d40190cee639fb17989b90dc5011f1c7d911daea9d21be1d7c3707e4180cb4be719220aad029a91c4268d1
|
|
@@ -658,8 +658,73 @@ module Brainiac
|
|
|
658
658
|
end
|
|
659
659
|
|
|
660
660
|
# Subcommand names for bash completion.
|
|
661
|
-
|
|
662
|
-
|
|
661
|
+
# When called with no args, returns top-level subcommands.
|
|
662
|
+
# When called with args (the words typed so far after the plugin name),
|
|
663
|
+
# returns context-sensitive completions for nested subcommands.
|
|
664
|
+
def self.completions(args = [])
|
|
665
|
+
return %w[board config setup status] if args.empty?
|
|
666
|
+
|
|
667
|
+
case args[0]
|
|
668
|
+
when "board"
|
|
669
|
+
board_completions(args[1..])
|
|
670
|
+
else
|
|
671
|
+
[]
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Nested completions for `brainiac fizzy board ...`
|
|
676
|
+
def self.board_completions(args)
|
|
677
|
+
board_subcommands = %w[assign columns list ls setup webhook]
|
|
678
|
+
return board_subcommands if args.empty?
|
|
679
|
+
|
|
680
|
+
case args[0]
|
|
681
|
+
when "assign"
|
|
682
|
+
board_assign_completions(args[1..])
|
|
683
|
+
when "columns", "webhook"
|
|
684
|
+
board_key_completions(args[1..])
|
|
685
|
+
else
|
|
686
|
+
[]
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Completions for `brainiac fizzy board assign <project> <board>`
|
|
691
|
+
def self.board_assign_completions(args)
|
|
692
|
+
case args.length
|
|
693
|
+
when 0
|
|
694
|
+
load_project_keys
|
|
695
|
+
when 1
|
|
696
|
+
load_board_keys
|
|
697
|
+
else
|
|
698
|
+
[]
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Completions for commands that take a board key as next arg
|
|
703
|
+
def self.board_key_completions(args)
|
|
704
|
+
return load_board_keys if args.empty?
|
|
705
|
+
|
|
706
|
+
[]
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# Load board keys from fizzy.json for completion
|
|
710
|
+
def self.load_board_keys
|
|
711
|
+
config_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "fizzy.json")
|
|
712
|
+
return [] unless File.exist?(config_file)
|
|
713
|
+
|
|
714
|
+
config = JSON.parse(File.read(config_file))
|
|
715
|
+
(config["boards"] || {}).keys
|
|
716
|
+
rescue StandardError
|
|
717
|
+
[]
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Load project keys from projects.json for completion
|
|
721
|
+
def self.load_project_keys
|
|
722
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
723
|
+
return [] unless File.exist?(projects_file)
|
|
724
|
+
|
|
725
|
+
JSON.parse(File.read(projects_file)).keys
|
|
726
|
+
rescue StandardError
|
|
727
|
+
[]
|
|
663
728
|
end
|
|
664
729
|
|
|
665
730
|
# Called by brainiac CLI after `agent create` — prompts for Fizzy user ID.
|
|
@@ -25,6 +25,18 @@ def prefetch_card_context(card_number, repo_path:, agent_name: nil)
|
|
|
25
25
|
Brainiac::Plugins::Fizzy::Helpers.prefetch_card_context(card_number, repo_path: repo_path, agent_name: agent_name)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def tag_names(tags)
|
|
29
|
+
Brainiac::Plugins::Fizzy::Helpers.tag_names(tags)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def card_has_tag?(tags, name)
|
|
33
|
+
Brainiac::Plugins::Fizzy::Helpers.card_has_tag?(tags, name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_card_tags(card_number, repo_path:, env: nil)
|
|
37
|
+
Brainiac::Plugins::Fizzy::Helpers.fetch_card_tags(card_number, repo_path: repo_path, env: env)
|
|
38
|
+
end
|
|
39
|
+
|
|
28
40
|
def resolve_github_agent_env(agent_name, github_repo)
|
|
29
41
|
Brainiac::Plugins::Fizzy::Helpers.resolve_github_agent_env(agent_name, github_repo)
|
|
30
42
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brainiac
|
|
4
|
+
module Plugins
|
|
5
|
+
module Fizzy
|
|
6
|
+
# Derives the public URL of an ephemeral Belt environment from its
|
|
7
|
+
# terraform.tfvars. Kept separate from Helpers so the pure URL/HCL logic
|
|
8
|
+
# stays small and independently testable.
|
|
9
|
+
module EnvUrl
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# URL of the ephemeral Belt environment for a card, if one is active.
|
|
13
|
+
# The env name is `fizzy-<card_number>`; the public URL is derived from the
|
|
14
|
+
# env's terraform.tfvars, matching the frontend_urls convention in the Belt
|
|
15
|
+
# infra module:
|
|
16
|
+
# parent set -> https://<env>.<parent>.<domain>
|
|
17
|
+
# prod -> https://<domain>
|
|
18
|
+
# otherwise -> https://<env>.<domain>
|
|
19
|
+
def for_card(card_number)
|
|
20
|
+
return nil unless defined?(BeltConfig)
|
|
21
|
+
|
|
22
|
+
env_name = BeltConfig.ephemeral_env_for_card(card_number)
|
|
23
|
+
return nil unless BeltConfig.ephemeral_env?(env_name)
|
|
24
|
+
|
|
25
|
+
info = BeltConfig.respond_to?(:ephemeral_env_info) ? BeltConfig.ephemeral_env_info(env_name) : nil
|
|
26
|
+
worktree = info && info["worktree"]
|
|
27
|
+
return nil unless worktree
|
|
28
|
+
|
|
29
|
+
tfvars = File.join(worktree, "infrastructure", env_name.to_s, "terraform.tfvars")
|
|
30
|
+
return nil unless File.exist?(tfvars)
|
|
31
|
+
|
|
32
|
+
from_tfvars(tfvars)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
LOG.warn "[Fizzy] Could not detect env URL for card ##{card_number}: #{e.message}" if defined?(LOG)
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def from_tfvars(tfvars_path)
|
|
39
|
+
vars = parse(File.read(tfvars_path))
|
|
40
|
+
domain = vars["domain"].to_s
|
|
41
|
+
return nil if domain.empty?
|
|
42
|
+
|
|
43
|
+
environment = vars["environment"].to_s
|
|
44
|
+
parent = vars["parent_environment"].to_s
|
|
45
|
+
|
|
46
|
+
host =
|
|
47
|
+
if !parent.empty?
|
|
48
|
+
"#{environment}.#{parent}.#{domain}"
|
|
49
|
+
elsif environment == "prod"
|
|
50
|
+
domain
|
|
51
|
+
else
|
|
52
|
+
"#{environment}.#{domain}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
"https://#{host}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Minimal HCL parser for `key = "value"` lines in terraform.tfvars.
|
|
59
|
+
def parse(contents)
|
|
60
|
+
vars = {}
|
|
61
|
+
contents.each_line do |line|
|
|
62
|
+
stripped = line.strip
|
|
63
|
+
next if stripped.empty? || stripped.start_with?("#")
|
|
64
|
+
|
|
65
|
+
match = stripped.match(/\A(\w+)\s*=\s*"([^"]*)"/)
|
|
66
|
+
vars[match[1]] = match[2] if match
|
|
67
|
+
end
|
|
68
|
+
vars
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -58,6 +58,10 @@ def handle_card_assigned(payload, board_key: nil)
|
|
|
58
58
|
inline_effort: initial_effort
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
+
maybe_create_ephemeral_belt_env(
|
|
62
|
+
worktree_path: worktree_path, card_number: card_number, project_key: project_key, tags: tags
|
|
63
|
+
)
|
|
64
|
+
|
|
61
65
|
dispatch_assigned_card(
|
|
62
66
|
card_number: card_number, card_internal_id: card_internal_id, title: title, tags: tags,
|
|
63
67
|
branch: branch, worktree_path: worktree_path, project_config: project_config, project_key: project_key,
|
|
@@ -183,3 +187,136 @@ def dispatch_assigned_card(card_number:, card_internal_id:, title:, tags:, branc
|
|
|
183
187
|
|
|
184
188
|
[200, { status: "processed", card: card_number, branch: branch, project: project_key, agent: agent_name }.to_json]
|
|
185
189
|
end
|
|
190
|
+
|
|
191
|
+
# Ensure an ephemeral Belt environment is configured in the worktree when the
|
|
192
|
+
# card has a `deploy` tag. Fizzy has no tag-added webhook, so comment handlers
|
|
193
|
+
# pass fetch_live_tags: true to read current tags from `fizzy card show`.
|
|
194
|
+
#
|
|
195
|
+
# `belt g environment` is synchronous so the worktree is configured before the
|
|
196
|
+
# agent starts. The actual `belt deploy` runs in the background.
|
|
197
|
+
def maybe_create_ephemeral_belt_env(worktree_path:, card_number:, project_key:, tags: nil,
|
|
198
|
+
repo_path: nil, agent_name: nil, fetch_live_tags: false)
|
|
199
|
+
unless defined?(BeltConfig) && defined?(BeltEnvironment)
|
|
200
|
+
LOG.debug "[EphemeralEnv] Belt utilities not available — skipping"
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
unless worktree_path && File.directory?(worktree_path)
|
|
205
|
+
LOG.debug "[EphemeralEnv] Worktree missing at #{worktree_path.inspect} — skipping"
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
unless belt_app_for_ephemeral?(worktree_path)
|
|
210
|
+
LOG.debug "[EphemeralEnv] #{project_key} is not a Belt app — skipping"
|
|
211
|
+
return
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
unless BeltConfig.ephemeral_deploys_enabled?
|
|
215
|
+
LOG.info "[EphemeralEnv] Ephemeral deploys disabled in basecamp.json"
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
resolved_tags = resolve_ephemeral_env_tags(
|
|
220
|
+
tags, card_number: card_number, repo_path: repo_path || worktree_path,
|
|
221
|
+
agent_name: agent_name, fetch_live_tags: fetch_live_tags
|
|
222
|
+
)
|
|
223
|
+
tag_list = tag_names(resolved_tags)
|
|
224
|
+
unless tag_list.include?("deploy")
|
|
225
|
+
LOG.debug "[EphemeralEnv] Card ##{card_number} tags=#{tag_list.inspect} — no deploy tag, skipping"
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
parent_env = BeltConfig.parent_env_for(project_key)
|
|
230
|
+
unless parent_env
|
|
231
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag but no parent env configured for #{project_key}"
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
env_name = BeltConfig.ephemeral_env_for_card(card_number)
|
|
236
|
+
env_dir = File.join(worktree_path, "infrastructure", env_name.to_s)
|
|
237
|
+
|
|
238
|
+
if File.directory?(env_dir)
|
|
239
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag; #{env_name} already configured at #{env_dir}"
|
|
240
|
+
track_ephemeral_env_if_needed(env_name, card_number, project_key, parent_env, worktree_path)
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
parent_dir = File.join(worktree_path, "infrastructure", parent_env.to_s)
|
|
245
|
+
unless File.directory?(parent_dir)
|
|
246
|
+
LOG.error "[EphemeralEnv] Parent env '#{parent_env}' not found at #{parent_dir}"
|
|
247
|
+
return
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag; #{env_name} not in worktree — creating from '#{parent_env}'"
|
|
251
|
+
create_and_deploy_ephemeral_env(
|
|
252
|
+
worktree_path: worktree_path, env_name: env_name, parent_env: parent_env,
|
|
253
|
+
card_number: card_number, project_key: project_key
|
|
254
|
+
)
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
LOG.error "[EphemeralEnv] Error creating ephemeral env: #{e.message}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def ensure_ephemeral_env_for_comment(ctx, card_number, worktree)
|
|
260
|
+
return unless card_number && worktree && File.directory?(worktree)
|
|
261
|
+
|
|
262
|
+
maybe_create_ephemeral_belt_env(
|
|
263
|
+
worktree_path: worktree, card_number: card_number, project_key: ctx.project_key,
|
|
264
|
+
tags: ctx.card_tags, repo_path: ctx.project_config["repo_path"],
|
|
265
|
+
agent_name: ctx.agent_name, fetch_live_tags: true
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def belt_app_for_ephemeral?(worktree_path)
|
|
270
|
+
return BeltEnvironment.belt_app?(worktree_path) if defined?(BeltEnvironment) && BeltEnvironment.respond_to?(:belt_app?)
|
|
271
|
+
|
|
272
|
+
%w[config/routes.rb config/routes.tf.rb infrastructure/routes.tf.rb].any? do |rel|
|
|
273
|
+
File.exist?(File.join(worktree_path, rel))
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def resolve_ephemeral_env_tags(tags, card_number:, repo_path:, agent_name:, fetch_live_tags:)
|
|
278
|
+
return tags unless fetch_live_tags && card_number
|
|
279
|
+
|
|
280
|
+
live = fetch_card_tags(card_number, repo_path: repo_path, env: fizzy_env_for(agent_name || AI_AGENT_NAME))
|
|
281
|
+
if live
|
|
282
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} live tags: #{tag_names(live).inspect}"
|
|
283
|
+
live
|
|
284
|
+
else
|
|
285
|
+
LOG.warn "[EphemeralEnv] Could not fetch live tags for card ##{card_number} — falling back to webhook tags #{tag_names(tags).inspect}"
|
|
286
|
+
tags
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def track_ephemeral_env_if_needed(env_name, card_number, project_key, parent_env, worktree_path)
|
|
291
|
+
return if BeltConfig.ephemeral_env?(env_name)
|
|
292
|
+
|
|
293
|
+
BeltConfig.track_ephemeral_env(env_name,
|
|
294
|
+
"card_number" => card_number,
|
|
295
|
+
"project" => project_key,
|
|
296
|
+
"parent_env" => parent_env,
|
|
297
|
+
"worktree" => worktree_path)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def create_and_deploy_ephemeral_env(worktree_path:, env_name:, parent_env:, card_number:, project_key:)
|
|
301
|
+
success = BeltEnvironment.create_environment(
|
|
302
|
+
worktree: worktree_path, env_name: env_name, parent_env: parent_env
|
|
303
|
+
)
|
|
304
|
+
unless success
|
|
305
|
+
LOG.error "[EphemeralEnv] Failed to create environment '#{env_name}'"
|
|
306
|
+
return
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
BeltConfig.track_ephemeral_env(env_name,
|
|
310
|
+
"card_number" => card_number,
|
|
311
|
+
"project" => project_key,
|
|
312
|
+
"parent_env" => parent_env,
|
|
313
|
+
"worktree" => worktree_path)
|
|
314
|
+
LOG.info "[EphemeralEnv] Configured #{env_name} in worktree, deploying in background"
|
|
315
|
+
|
|
316
|
+
Thread.new do
|
|
317
|
+
frontend_only = BeltEnvironment.frontend_only_changes?(worktree: worktree_path)
|
|
318
|
+
BeltEnvironment.deploy(worktree: worktree_path, env_name: env_name, frontend_only: frontend_only)
|
|
319
|
+
rescue StandardError => e
|
|
320
|
+
LOG.error "[EphemeralEnv] Error deploying '#{env_name}': #{e.message}"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -453,6 +453,10 @@ def handle_existing_card_comment(ctx)
|
|
|
453
453
|
work_dir = worktree && File.directory?(worktree) ? worktree : ctx.project_config["repo_path"]
|
|
454
454
|
card_key = "card-#{card_number || ctx.card_internal_id}"
|
|
455
455
|
|
|
456
|
+
# Fizzy has no tag-added webhook. On comment, fetch live tags and configure
|
|
457
|
+
# the Belt env in this worktree before dispatching (or queueing) the agent.
|
|
458
|
+
ensure_ephemeral_env_for_comment(ctx, card_number, worktree)
|
|
459
|
+
|
|
456
460
|
# Session management (wait, supersede, or queue)
|
|
457
461
|
queued = handle_session_conflict(ctx, card_key, card_number, work_dir)
|
|
458
462
|
return queued if queued
|
|
@@ -491,6 +495,8 @@ def handle_new_mention(ctx)
|
|
|
491
495
|
react_to_comment(card_number, ctx.comment_id, ctx.project_config, ctx.agent_name, "👀")
|
|
492
496
|
|
|
493
497
|
worktree_path, branch = setup_new_mention_worktree(ctx, card_number, card_title)
|
|
498
|
+
ensure_ephemeral_env_for_comment(ctx, card_number, worktree_path)
|
|
499
|
+
|
|
494
500
|
dispatch_new_mention(ctx, card_key: card_key, card_number: card_number,
|
|
495
501
|
card_title: card_title, branch: branch, worktree_path: worktree_path)
|
|
496
502
|
end
|
|
@@ -60,6 +60,37 @@ module Brainiac
|
|
|
60
60
|
context
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# Normalize Fizzy tags from webhook hashes ({ "name" => "deploy" })
|
|
64
|
+
# or API strings ("deploy") into a lowercase name list.
|
|
65
|
+
def tag_names(tags)
|
|
66
|
+
Array(tags).filter_map do |tag|
|
|
67
|
+
name = tag.is_a?(Hash) ? (tag["name"] || tag[:name]) : tag
|
|
68
|
+
normalized = name.to_s.downcase
|
|
69
|
+
normalized unless normalized.empty?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def card_has_tag?(tags, name)
|
|
74
|
+
tag_names(tags).include?(name.to_s.downcase)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Live tags from `fizzy card show`. Returns nil on failure so callers
|
|
78
|
+
# can fall back to webhook payload tags. Fizzy has no tag-added webhook;
|
|
79
|
+
# comment payloads may omit or stale-cache tags, so this is the source of truth.
|
|
80
|
+
def fetch_card_tags(card_number, repo_path:, env: nil)
|
|
81
|
+
return nil unless card_number && repo_path
|
|
82
|
+
|
|
83
|
+
env ||= default_fizzy_env
|
|
84
|
+
output = run_cmd("fizzy", "card", "show", card_number.to_s, chdir: repo_path, env: env)
|
|
85
|
+
card = JSON.parse(output)["data"]
|
|
86
|
+
return nil unless card
|
|
87
|
+
|
|
88
|
+
Array(card["tags"])
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
LOG.warn "[Fizzy] Could not fetch tags for card ##{card_number}: #{e.message}" if defined?(LOG)
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
63
94
|
def fetch_card_details(card_number, repo_path:, env:)
|
|
64
95
|
output = run_cmd("fizzy", "card", "show", card_number.to_s, chdir: repo_path, env: env)
|
|
65
96
|
card = JSON.parse(output)["data"]
|
|
@@ -142,14 +173,10 @@ module Brainiac
|
|
|
142
173
|
|
|
143
174
|
# Append footer if applicable
|
|
144
175
|
body = last_agent_comment.dig("body", "html") || ""
|
|
145
|
-
unless
|
|
176
|
+
unless footer_already_present?(body)
|
|
146
177
|
branch = detect_branch_from_comment(body, card_number)
|
|
147
178
|
if branch
|
|
148
|
-
|
|
149
|
-
footer = "<p><em>Branch: <code>#{branch}</code>"
|
|
150
|
-
footer += " | <a href=\"#{pr_url}\">PR</a>" if pr_url
|
|
151
|
-
footer += "</em></p>"
|
|
152
|
-
|
|
179
|
+
footer = build_comment_footer(branch, card_number, project_config)
|
|
153
180
|
updated_body = body + footer
|
|
154
181
|
run_cmd("fizzy", "comment", "update", last_agent_comment["id"], "--card", card_number.to_s,
|
|
155
182
|
"--body", updated_body, chdir: repo_path, env: env)
|
|
@@ -300,8 +327,51 @@ module Brainiac
|
|
|
300
327
|
repo = project_config["github_repo"]
|
|
301
328
|
return nil unless repo
|
|
302
329
|
|
|
330
|
+
# Prefer the existing open PR for this branch. `pull/new/<branch>` only
|
|
331
|
+
# opens the "create PR" page, which is wrong once a PR already exists.
|
|
332
|
+
existing = existing_pr_url(branch, repo, project_config["repo_path"])
|
|
333
|
+
return existing if existing
|
|
334
|
+
|
|
303
335
|
"https://github.com/#{repo}/pull/new/#{branch}"
|
|
304
336
|
end
|
|
337
|
+
|
|
338
|
+
# Look up the URL of an already-open PR for `branch` via the gh CLI.
|
|
339
|
+
# Returns nil if gh fails or no PR exists (caller falls back to new-PR link).
|
|
340
|
+
def existing_pr_url(branch, repo, repo_path)
|
|
341
|
+
output = run_cmd("gh", "pr", "view", branch, "--repo", repo, "--json", "url",
|
|
342
|
+
"--jq", ".url", chdir: repo_path)
|
|
343
|
+
url = output.to_s.strip
|
|
344
|
+
url.empty? ? nil : url
|
|
345
|
+
rescue StandardError
|
|
346
|
+
nil
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# True if the comment body already carries a "Branch:" footer/label, in
|
|
350
|
+
# either the footer format (`<em>Branch:`) or the agent-authored format
|
|
351
|
+
# (`<strong>Branch:</strong>`). Prevents appending a duplicate footer.
|
|
352
|
+
def footer_already_present?(body)
|
|
353
|
+
body.include?("<em>Branch:") ||
|
|
354
|
+
body.match?(%r{<strong>\s*Branch:\s*</strong>}i) ||
|
|
355
|
+
body.match?(/(^|>)\s*Branch:\s*</)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Build the "<em>Branch: <code>...</code> | PR | Env</em>" footer.
|
|
359
|
+
# Includes the ephemeral env link when the card has an active env.
|
|
360
|
+
def build_comment_footer(branch, card_number, project_config)
|
|
361
|
+
pr_url = detect_pr_url(branch, project_config)
|
|
362
|
+
env_url = detect_env_url(card_number)
|
|
363
|
+
|
|
364
|
+
footer = "<p><em>Branch: <code>#{branch}</code>"
|
|
365
|
+
footer += " | <a href=\"#{pr_url}\">PR</a>" if pr_url
|
|
366
|
+
footer += " | <a href=\"#{env_url}\">Env</a>" if env_url
|
|
367
|
+
footer += "</em></p>"
|
|
368
|
+
footer
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# URL of the ephemeral Belt environment for this card, if one is active.
|
|
372
|
+
def detect_env_url(card_number)
|
|
373
|
+
EnvUrl.for_card(card_number)
|
|
374
|
+
end
|
|
305
375
|
end
|
|
306
376
|
end
|
|
307
377
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brainiac-fizzy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.27
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- lib/brainiac/plugins/fizzy/cli.rb
|
|
93
93
|
- lib/brainiac/plugins/fizzy/config.rb
|
|
94
94
|
- lib/brainiac/plugins/fizzy/delegators.rb
|
|
95
|
+
- lib/brainiac/plugins/fizzy/env_url.rb
|
|
95
96
|
- lib/brainiac/plugins/fizzy/handlers/assignment.rb
|
|
96
97
|
- lib/brainiac/plugins/fizzy/handlers/card_index.rb
|
|
97
98
|
- lib/brainiac/plugins/fizzy/handlers/comments.rb
|