brainiac-basecamp 0.0.11 → 0.0.13
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8adcfd4dedeee9ee8fee1e9439e80f7f3917d81ece1c5420a3777619df8d7ab5
|
|
4
|
+
data.tar.gz: d88eb5a5e18d8bea39522ccfa6c8fbac13b2b50f87a28a29c6c1b8958e4f3eea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 311711ad321fe576e24aeb51d82d9dffc4b9d2b6199b8f6b6242b7405583f224d1686d4b91cc395d3e16985dd73834324cbe539fe216f2eb22962a4f34d05d2d
|
|
7
|
+
data.tar.gz: 7ccb5d585757a9b87b5dfa88512e44528048c153d4d5e159c4e62baa9c78f904dec5333899abdbaeec46b0a8da0ac6bfbd1cd39ca2530030f1d6e05e3a1d096c
|
|
@@ -156,10 +156,75 @@ module Brainiac
|
|
|
156
156
|
# If project is set, look up directly
|
|
157
157
|
return epic_branches[project_key] if project_key && epic_branches[project_key]
|
|
158
158
|
|
|
159
|
-
# Fallback: if there's only one epic branch
|
|
160
|
-
|
|
159
|
+
# Fallback: if there's only one epic branch AND the task has no specific project set,
|
|
160
|
+
# use it (assumed single-project epic).
|
|
161
|
+
# Do NOT fallback if the task has a specific project that isn't in epic_branches —
|
|
162
|
+
# that means the branch wasn't created for this repo and we should return nil
|
|
163
|
+
# (letting the default branch be used) rather than returning a branch from a different repo.
|
|
164
|
+
return epic_branches.values.first if epic_branches.size == 1 && !project_key
|
|
165
|
+
|
|
166
|
+
# Multi-project epic but no matching branch for this task's project — return nil
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Lazily ensure an epic branch exists for a card's project.
|
|
171
|
+
# Called when epic_branch_for_card returns nil but the card IS in an active epic.
|
|
172
|
+
# Creates the branch in the task's repo and registers it in epic state.
|
|
173
|
+
#
|
|
174
|
+
# @param card_number [Integer, String] Fizzy card number
|
|
175
|
+
# @return [String, nil] Epic branch name or nil
|
|
176
|
+
def ensure_epic_branch_for_card(card_number)
|
|
177
|
+
epic = Orchestrator.find_epic_for_card(card_number.to_i)
|
|
178
|
+
return nil unless epic
|
|
179
|
+
return nil unless epic["review_gate"] == "epic_branch"
|
|
180
|
+
|
|
181
|
+
task = epic["tasks"]&.find { |t| t["fizzy_card"] == card_number.to_i }
|
|
182
|
+
return nil unless task
|
|
183
|
+
|
|
184
|
+
project_key = task["project"]
|
|
185
|
+
return nil unless project_key
|
|
161
186
|
|
|
162
|
-
#
|
|
187
|
+
# Already have a branch for this project
|
|
188
|
+
epic_branches = epic["epic_branches"] || {}
|
|
189
|
+
return epic_branches[project_key] if epic_branches[project_key]
|
|
190
|
+
|
|
191
|
+
# Resolve repo path for this project
|
|
192
|
+
projects_file = File.join(BRAINIAC_DIR, "projects.json")
|
|
193
|
+
return nil unless File.exist?(projects_file)
|
|
194
|
+
|
|
195
|
+
all_projects = JSON.parse(File.read(projects_file))
|
|
196
|
+
repo_path = all_projects.dig(project_key, "repo_path")
|
|
197
|
+
return nil unless repo_path
|
|
198
|
+
|
|
199
|
+
# Create the branch
|
|
200
|
+
slug = branch_slug(epic["title"])
|
|
201
|
+
branch_name = "epic/#{slug}"
|
|
202
|
+
default_branch = detect_default_branch(repo_path)
|
|
203
|
+
|
|
204
|
+
run_git("fetch", "origin", chdir: repo_path)
|
|
205
|
+
|
|
206
|
+
# Check if branch already exists remotely
|
|
207
|
+
remote_exists = system("git", "ls-remote", "--exit-code", "--heads", "origin", branch_name,
|
|
208
|
+
chdir: repo_path, out: File::NULL, err: File::NULL)
|
|
209
|
+
|
|
210
|
+
if remote_exists
|
|
211
|
+
run_git("fetch", "origin", "#{branch_name}:#{branch_name}", chdir: repo_path)
|
|
212
|
+
LOG.info "[Basecamp:EpicBranch] Lazy-created: reusing existing '#{branch_name}' in #{project_key}" if defined?(LOG)
|
|
213
|
+
else
|
|
214
|
+
run_git("branch", branch_name, "origin/#{default_branch}", chdir: repo_path)
|
|
215
|
+
run_git("push", "-u", "origin", branch_name, chdir: repo_path)
|
|
216
|
+
LOG.info "[Basecamp:EpicBranch] Lazy-created: new '#{branch_name}' in #{project_key}" if defined?(LOG)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Register in epic state
|
|
220
|
+
epic["epic_branches"] ||= {}
|
|
221
|
+
epic["epic_branches"][project_key] = branch_name
|
|
222
|
+
epic["updated_at"] = Time.now.iso8601
|
|
223
|
+
Orchestrator.send(:save_epic, epic)
|
|
224
|
+
|
|
225
|
+
branch_name
|
|
226
|
+
rescue StandardError => e
|
|
227
|
+
LOG.error "[Basecamp:EpicBranch] Lazy branch creation failed for #{project_key}: #{e.message}" if defined?(LOG)
|
|
163
228
|
nil
|
|
164
229
|
end
|
|
165
230
|
|
|
@@ -123,6 +123,18 @@ module Brainiac
|
|
|
123
123
|
if epic_branches.include?(base_branch)
|
|
124
124
|
LOG.info "[Basecamp:Hooks] PR merged to epic branch #{base_branch} for card ##{card_number} — advancing" if defined?(LOG)
|
|
125
125
|
Orchestrator.on_card_completed(card_number)
|
|
126
|
+
elsif epic_branches.empty?
|
|
127
|
+
# Fallback: epic_branches was never populated (branch creation failed).
|
|
128
|
+
# If the task is in final_decision or in_review, the merge still represents
|
|
129
|
+
# completion — advance the epic regardless of which branch was targeted.
|
|
130
|
+
task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
|
|
131
|
+
if task && %w[final_decision in_review in_flight].include?(task["status"])
|
|
132
|
+
if defined?(LOG)
|
|
133
|
+
LOG.warn "[Basecamp:Hooks] epic_branches is empty but PR merged to '#{base_branch}' " \
|
|
134
|
+
"for card ##{card_number} (status: #{task['status']}) — advancing anyway (branch creation likely failed)"
|
|
135
|
+
end
|
|
136
|
+
Orchestrator.on_card_completed(card_number)
|
|
137
|
+
end
|
|
126
138
|
end
|
|
127
139
|
end
|
|
128
140
|
end
|
|
@@ -470,12 +482,17 @@ module Brainiac
|
|
|
470
482
|
end
|
|
471
483
|
|
|
472
484
|
# Return the epic branch as the worktree base for cards in an active epic.
|
|
485
|
+
# If the epic branch doesn't exist for this task's project yet, create it lazily.
|
|
473
486
|
def register_resolve_base_branch
|
|
474
487
|
Brainiac.on(:resolve_base_branch) do |ctx|
|
|
475
488
|
card_number = ctx[:card_number]
|
|
476
489
|
next unless card_number
|
|
477
490
|
|
|
478
491
|
branch = EpicBranch.epic_branch_for_card(card_number)
|
|
492
|
+
|
|
493
|
+
# Lazy creation: if no branch exists for this task's project, create one now
|
|
494
|
+
branch ||= EpicBranch.ensure_epic_branch_for_card(card_number)
|
|
495
|
+
|
|
479
496
|
next unless branch
|
|
480
497
|
|
|
481
498
|
"origin/#{branch}"
|
|
@@ -483,12 +500,15 @@ module Brainiac
|
|
|
483
500
|
end
|
|
484
501
|
|
|
485
502
|
# Return the epic branch as the PR target for cards in an active epic.
|
|
503
|
+
# Same lazy-creation logic as resolve_base_branch.
|
|
486
504
|
def register_resolve_pr_target
|
|
487
505
|
Brainiac.on(:resolve_pr_target) do |ctx|
|
|
488
506
|
card_number = ctx[:card_number]
|
|
489
507
|
next unless card_number
|
|
490
508
|
|
|
491
|
-
EpicBranch.epic_branch_for_card(card_number)
|
|
509
|
+
branch = EpicBranch.epic_branch_for_card(card_number)
|
|
510
|
+
branch ||= EpicBranch.ensure_epic_branch_for_card(card_number)
|
|
511
|
+
branch
|
|
492
512
|
end
|
|
493
513
|
end
|
|
494
514
|
|
|
@@ -613,13 +633,32 @@ module Brainiac
|
|
|
613
633
|
github_repo = project_config&.dig("github_repo")
|
|
614
634
|
|
|
615
635
|
# Check if PR is already merged (handles manual merges, webhook misses, etc.)
|
|
616
|
-
if github_repo && pr_number
|
|
636
|
+
if github_repo && pr_number&.to_i&.positive?
|
|
617
637
|
pr_state, = Open3.capture2("gh", "pr", "view", pr_number.to_s, "--repo", github_repo, "--json", "state", "-q", ".state")
|
|
618
638
|
if pr_state.strip == "MERGED"
|
|
619
639
|
LOG.info "[Basecamp:Hooks] PR ##{pr_number} already merged — marking card ##{card_number} complete" if defined?(LOG)
|
|
620
640
|
Orchestrator.on_card_completed(card_number)
|
|
621
641
|
return
|
|
622
642
|
end
|
|
643
|
+
|
|
644
|
+
# SAFEGUARD: Verify PR targets the epic branch (not main/master).
|
|
645
|
+
# If epic_branches is populated and the PR targets the wrong base, retarget it.
|
|
646
|
+
epic_branches = epic["epic_branches"] || {}
|
|
647
|
+
expected_base = epic_branches[project_key] || epic_branches.values.first
|
|
648
|
+
if expected_base
|
|
649
|
+
actual_base, = Open3.capture2("gh", "pr", "view", pr_number.to_s, "--repo", github_repo, "--json", "baseRefName", "-q",
|
|
650
|
+
".baseRefName")
|
|
651
|
+
actual_base = actual_base.strip
|
|
652
|
+
if !actual_base.empty? && actual_base != expected_base
|
|
653
|
+
LOG.warn "[Basecamp:Hooks] PR ##{pr_number} targets '#{actual_base}' but expected '#{expected_base}' — retargeting" if defined?(LOG)
|
|
654
|
+
_, stderr, status = Open3.capture3("gh", "pr", "edit", pr_number.to_s, "--repo", github_repo, "--base", expected_base)
|
|
655
|
+
if status.success?
|
|
656
|
+
LOG.info "[Basecamp:Hooks] Retargeted PR ##{pr_number} to '#{expected_base}'" if defined?(LOG)
|
|
657
|
+
elsif defined?(LOG)
|
|
658
|
+
LOG.error "[Basecamp:Hooks] Failed to retarget PR ##{pr_number}: #{stderr.strip}"
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
end
|
|
623
662
|
end
|
|
624
663
|
|
|
625
664
|
# Mark task as awaiting final decision
|
|
@@ -659,6 +698,17 @@ module Brainiac
|
|
|
659
698
|
gate_approvals = task["gate_approvals"] || []
|
|
660
699
|
gate_agents = gate_approvals.map { |a| a["agent"] }.join(", ")
|
|
661
700
|
|
|
701
|
+
# Determine expected target branch for the prompt
|
|
702
|
+
epic_branches = epic["epic_branches"] || {}
|
|
703
|
+
expected_base = epic_branches[project_key] || epic_branches.values.first
|
|
704
|
+
target_note = if expected_base
|
|
705
|
+
"\n**IMPORTANT:** This PR should target `#{expected_base}` (the epic branch). " \
|
|
706
|
+
"If `gh pr view #{pr_number} --json baseRefName` shows a different base, " \
|
|
707
|
+
"do NOT merge — report the mismatch instead.\n"
|
|
708
|
+
else
|
|
709
|
+
""
|
|
710
|
+
end
|
|
711
|
+
|
|
662
712
|
prompt = <<~PROMPT
|
|
663
713
|
## Final Decision Required — Fizzy Card ##{card_number}
|
|
664
714
|
|
|
@@ -670,7 +720,7 @@ module Brainiac
|
|
|
670
720
|
```
|
|
671
721
|
gh pr merge #{pr_number} --squash --delete-branch
|
|
672
722
|
```
|
|
673
|
-
|
|
723
|
+
#{target_note}
|
|
674
724
|
Note: You cannot self-approve PRs you authored. Merge directly since gates have approved.
|
|
675
725
|
|
|
676
726
|
After merging, update the Fizzy card with a brief status comment.
|
|
@@ -741,12 +791,12 @@ module Brainiac
|
|
|
741
791
|
|
|
742
792
|
# Guard: don't spawn a duplicate if a session is already running for this task
|
|
743
793
|
session_alive = if defined?(session_active?)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
794
|
+
session_active?(card_key)
|
|
795
|
+
elsif Object.respond_to?(:session_active?, true)
|
|
796
|
+
Object.send(:session_active?, card_key)
|
|
797
|
+
else
|
|
798
|
+
false
|
|
799
|
+
end
|
|
750
800
|
|
|
751
801
|
if session_alive
|
|
752
802
|
LOG.info "[Basecamp:Hooks] Session already active for #{card_key} — skipping dispatch" if defined?(LOG)
|
|
@@ -785,11 +835,11 @@ module Brainiac
|
|
|
785
835
|
prompt = <<~PROMPT
|
|
786
836
|
## Changes Requested — Fizzy Card ##{card_number}
|
|
787
837
|
|
|
788
|
-
Review gates have requested changes on your PR#{
|
|
838
|
+
Review gates have requested changes on your PR#{" ##{pr_number}" if pr_number}.
|
|
789
839
|
Reviewers who requested changes: #{changers}
|
|
790
840
|
|
|
791
841
|
Your job:
|
|
792
|
-
1. Read the review feedback: #{pr_number ? "`gh pr view #{pr_number} --comments`" :
|
|
842
|
+
1. Read the review feedback: #{pr_number ? "`gh pr view #{pr_number} --comments`" : 'check the Fizzy card comments'}
|
|
793
843
|
2. Address all requested changes
|
|
794
844
|
3. Commit and push your fixes
|
|
795
845
|
|
|
@@ -764,14 +764,43 @@ module Brainiac
|
|
|
764
764
|
end
|
|
765
765
|
|
|
766
766
|
# Create epic branches for all projects involved in this epic.
|
|
767
|
+
# CRITICAL: If this fails, epic_branches will be empty and agents will
|
|
768
|
+
# open PRs against the default branch instead of the epic branch.
|
|
769
|
+
# We log loudly and retry resolution of project repos if initial attempt fails.
|
|
767
770
|
def create_epic_branches_for(epic)
|
|
768
771
|
project_repos = resolve_project_repos(epic)
|
|
769
|
-
|
|
772
|
+
|
|
773
|
+
if project_repos.empty?
|
|
774
|
+
# Fallback: try to resolve from the basecamp project mapping directly
|
|
775
|
+
mapped_project = Config.brainiac_project_for(epic["basecamp_project_id"])
|
|
776
|
+
if mapped_project
|
|
777
|
+
projects_file = File.join(BRAINIAC_DIR, "projects.json")
|
|
778
|
+
all_projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
779
|
+
repo = all_projects.dig(mapped_project, "repo_path")
|
|
780
|
+
project_repos = { mapped_project => repo } if repo
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
if project_repos.empty?
|
|
785
|
+
LOG.error "[Basecamp:Orchestrator] Cannot create epic branches — no project repos resolved. " \
|
|
786
|
+
"Tasks have projects: #{(epic['tasks'] || []).map { |t| t['project'] }.compact.uniq.inspect}. " \
|
|
787
|
+
"Epic will proceed WITHOUT epic branches — PRs will target the default branch!" if defined?(LOG)
|
|
788
|
+
log_event(epic, "branches_failed", "No project repos resolved — epic branches not created")
|
|
789
|
+
return
|
|
790
|
+
end
|
|
770
791
|
|
|
771
792
|
epic["epic_branches"] = EpicBranch.create_epic_branches(epic, project_repos)
|
|
772
|
-
|
|
793
|
+
|
|
794
|
+
if epic["epic_branches"].empty?
|
|
795
|
+
LOG.error "[Basecamp:Orchestrator] create_epic_branches returned empty — branch creation failed" if defined?(LOG)
|
|
796
|
+
log_event(epic, "branches_failed", "Branch creation returned empty for repos: #{project_repos.keys.join(', ')}")
|
|
797
|
+
else
|
|
798
|
+
log_event(epic, "branches_created", "Epic branches: #{epic['epic_branches'].values.uniq.join(', ')}")
|
|
799
|
+
end
|
|
773
800
|
rescue StandardError => e
|
|
774
|
-
LOG.error "[Basecamp:Orchestrator] Failed to create epic branches: #{e.message}"
|
|
801
|
+
LOG.error "[Basecamp:Orchestrator] Failed to create epic branches: #{e.class}: #{e.message}" \
|
|
802
|
+
"\n#{e.backtrace.first(3).join("\n")}" if defined?(LOG)
|
|
803
|
+
log_event(epic, "branches_failed", "Exception: #{e.message}")
|
|
775
804
|
end
|
|
776
805
|
|
|
777
806
|
# Open final PRs from epic branches to main.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
3
4
|
require_relative "basecamp/version"
|
|
4
5
|
require_relative "basecamp/metadata"
|
|
5
6
|
require_relative "basecamp/config"
|
|
@@ -99,16 +100,27 @@ module Brainiac
|
|
|
99
100
|
|
|
100
101
|
case status
|
|
101
102
|
when "final_decision"
|
|
102
|
-
# Check if PR is already merged
|
|
103
|
-
|
|
103
|
+
# Check if PR is already merged — resolve pr_number first if missing/zero
|
|
104
|
+
effective_pr = pr_number
|
|
105
|
+
if (effective_pr.nil? || effective_pr.zero?) && project_key
|
|
106
|
+
resolved = resolve_pr_for_task(task)
|
|
107
|
+
if resolved
|
|
108
|
+
task["pr_number"] = resolved[:number]
|
|
109
|
+
task["pr_repo"] = resolved[:repo] if resolved[:repo]
|
|
110
|
+
effective_pr = resolved[:number]
|
|
111
|
+
LOG.info "[Basecamp:HealthCheck] Resolved PR ##{effective_pr} for card ##{card_number}" if defined?(LOG)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if effective_pr&.positive? && project_key
|
|
104
116
|
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
105
117
|
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
106
118
|
github_repo = projects.dig(project_key, "github_repo")
|
|
107
119
|
|
|
108
120
|
if github_repo
|
|
109
|
-
pr_state, = Open3.capture2("gh", "pr", "view",
|
|
121
|
+
pr_state, = Open3.capture2("gh", "pr", "view", effective_pr.to_s, "--repo", github_repo, "--json", "state", "-q", ".state")
|
|
110
122
|
if pr_state.strip == "MERGED"
|
|
111
|
-
LOG.info "[Basecamp:HealthCheck] PR ##{
|
|
123
|
+
LOG.info "[Basecamp:HealthCheck] PR ##{effective_pr} merged but task ##{card_number} stuck in final_decision — healing" if defined?(LOG)
|
|
112
124
|
Orchestrator.on_card_completed(card_number)
|
|
113
125
|
healed_any = true
|
|
114
126
|
end
|
|
@@ -388,6 +400,35 @@ module Brainiac
|
|
|
388
400
|
card_number = task["fizzy_card"]
|
|
389
401
|
LOG.info "[Basecamp] Resume: checking final_decision task ##{card_number}, awaiting=#{task['awaiting_final_decision']}" if defined?(LOG)
|
|
390
402
|
|
|
403
|
+
# First, resolve PR number if missing or zero (branch creation may have failed)
|
|
404
|
+
pr_number = task["pr_number"]
|
|
405
|
+
if (pr_number.nil? || pr_number.zero?) && task["project"]
|
|
406
|
+
resolved_pr = resolve_pr_for_task(task)
|
|
407
|
+
if resolved_pr
|
|
408
|
+
task["pr_number"] = resolved_pr[:number]
|
|
409
|
+
task["pr_repo"] = resolved_pr[:repo] if resolved_pr[:repo]
|
|
410
|
+
pr_number = resolved_pr[:number]
|
|
411
|
+
LOG.info "[Basecamp] Resume: resolved PR ##{pr_number} for card ##{card_number}" if defined?(LOG)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Check if PR is already merged (handles manual merges, webhook misses, agent merges to wrong branch)
|
|
416
|
+
if pr_number&.positive?
|
|
417
|
+
project_key = task["project"]
|
|
418
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
419
|
+
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
420
|
+
github_repo = projects.dig(project_key, "github_repo")
|
|
421
|
+
|
|
422
|
+
if github_repo
|
|
423
|
+
pr_state, = Open3.capture2("gh", "pr", "view", pr_number.to_s, "--repo", github_repo, "--json", "state", "-q", ".state")
|
|
424
|
+
if pr_state.strip == "MERGED"
|
|
425
|
+
LOG.info "[Basecamp] Resume: PR ##{pr_number} already merged — marking card ##{card_number} complete" if defined?(LOG)
|
|
426
|
+
Orchestrator.on_card_completed(card_number)
|
|
427
|
+
return
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
391
432
|
# If awaiting_final_decision is set, re-dispatch
|
|
392
433
|
# Also handle the case where it's nil but gates are all approved (stale state)
|
|
393
434
|
if task["awaiting_final_decision"] || ReviewGate.all_gates_passed?(task)
|
|
@@ -400,6 +441,38 @@ module Brainiac
|
|
|
400
441
|
end
|
|
401
442
|
end
|
|
402
443
|
|
|
444
|
+
# Resolve a PR number for a task by searching GitHub for matching branch patterns.
|
|
445
|
+
# Used when pr_number is 0 or nil (e.g., branch creation failed so PR was never tracked).
|
|
446
|
+
def resolve_pr_for_task(task)
|
|
447
|
+
card_number = task["fizzy_card"]
|
|
448
|
+
project_key = task["project"]
|
|
449
|
+
return nil unless project_key
|
|
450
|
+
|
|
451
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
452
|
+
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
453
|
+
repo_path = projects.dig(project_key, "repo_path")
|
|
454
|
+
github_repo = projects.dig(project_key, "github_repo")
|
|
455
|
+
return nil unless repo_path
|
|
456
|
+
|
|
457
|
+
# Search for PR by fizzy-NNNN branch pattern (any state)
|
|
458
|
+
stdout, _, status = Open3.capture3(
|
|
459
|
+
"gh", "pr", "list", "--state", "all",
|
|
460
|
+
"--json", "number,headRefName,state",
|
|
461
|
+
"--jq", ".[] | select(.headRefName | startswith(\"fizzy-#{card_number}\")) | [.number, .state] | @tsv",
|
|
462
|
+
chdir: repo_path
|
|
463
|
+
)
|
|
464
|
+
return nil unless status.success? && !stdout.strip.empty?
|
|
465
|
+
|
|
466
|
+
# Take the first match
|
|
467
|
+
number, _state = stdout.strip.split("\n").first.split("\t")
|
|
468
|
+
return nil unless number
|
|
469
|
+
|
|
470
|
+
{ number: number.to_i, repo: github_repo }
|
|
471
|
+
rescue StandardError => e
|
|
472
|
+
LOG.warn "[Basecamp] Error resolving PR for card ##{card_number}: #{e.message}" if defined?(LOG)
|
|
473
|
+
nil
|
|
474
|
+
end
|
|
475
|
+
|
|
403
476
|
def setup_routes(app)
|
|
404
477
|
setup_webhook_route(app)
|
|
405
478
|
setup_api_routes(app)
|