Fix convoy check to query external rig databases for cross-rig tracking (#916)
Fixes #915 `gt convoy check` was failing to detect closed beads in external rig databases, causing convoys to remain perpetually open despite tracked work being completed. Changes: - Modified getTrackedIssues() to parse external:rig:id format and track rig ownership - Added getExternalIssueDetails() to query external rig databases by running bd show from the rig directory - Changed from issueIDs []string to issueRefs []issueRef struct to track both ID and rig name for each dependency The fix enables proper cross-rig convoy completion by querying the appropriate database (town or rig) for each tracked bead's status. Testing: Verified that convoy hq-cv-u7k7w tracking external:claycantrell:cl-niwe now correctly detects the closed status and auto-closes the convoy.
This commit is contained in:
@@ -1336,30 +1336,51 @@ func getTrackedIssues(townBeads, convoyID string) []trackedIssueInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// First pass: collect all issue IDs (normalized from external refs)
|
// First pass: collect all issue IDs and track which rig they belong to
|
||||||
issueIDs := make([]string, 0, len(deps))
|
type issueRef struct {
|
||||||
|
ID string
|
||||||
|
RigName string // empty for local issues, rig name for external
|
||||||
|
}
|
||||||
|
issueRefs := make([]issueRef, 0, len(deps))
|
||||||
idToDepType := make(map[string]string)
|
idToDepType := make(map[string]string)
|
||||||
|
|
||||||
for _, dep := range deps {
|
for _, dep := range deps {
|
||||||
issueID := dep.DependsOnID
|
issueID := dep.DependsOnID
|
||||||
|
rigName := "" // Local issue by default
|
||||||
|
|
||||||
// Handle external reference format: external:rig:issue-id
|
// Handle external reference format: external:rig:issue-id
|
||||||
if strings.HasPrefix(issueID, "external:") {
|
if strings.HasPrefix(issueID, "external:") {
|
||||||
parts := strings.SplitN(issueID, ":", 3)
|
parts := strings.SplitN(issueID, ":", 3)
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
|
rigName = parts[1] // Extract rig name
|
||||||
issueID = parts[2] // Extract the actual issue ID
|
issueID = parts[2] // Extract the actual issue ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
issueIDs = append(issueIDs, issueID)
|
issueRefs = append(issueRefs, issueRef{ID: issueID, RigName: rigName})
|
||||||
idToDepType[issueID] = dep.Type
|
idToDepType[issueID] = dep.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single batch call to get all issue details
|
// Query issues, grouped by rig
|
||||||
detailsMap := getIssueDetailsBatch(issueIDs)
|
detailsMap := make(map[string]*issueDetails)
|
||||||
|
for _, ref := range issueRefs {
|
||||||
|
var details *issueDetails
|
||||||
|
if ref.RigName != "" {
|
||||||
|
// External reference: query the rig database
|
||||||
|
details = getExternalIssueDetails(townBeads, ref.RigName, ref.ID)
|
||||||
|
} else {
|
||||||
|
// Local reference: query town database
|
||||||
|
details = getIssueDetails(ref.ID)
|
||||||
|
}
|
||||||
|
if details != nil {
|
||||||
|
detailsMap[ref.ID] = details
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get workers for these issues (only for non-closed issues)
|
// Get workers for these issues (only for non-closed issues)
|
||||||
openIssueIDs := make([]string, 0, len(issueIDs))
|
openIssueIDs := make([]string, 0)
|
||||||
for _, id := range issueIDs {
|
for _, ref := range issueRefs {
|
||||||
|
id := ref.ID
|
||||||
if details, ok := detailsMap[id]; ok && details.Status != "closed" {
|
if details, ok := detailsMap[id]; ok && details.Status != "closed" {
|
||||||
openIssueIDs = append(openIssueIDs, id)
|
openIssueIDs = append(openIssueIDs, id)
|
||||||
}
|
}
|
||||||
@@ -1368,7 +1389,8 @@ func getTrackedIssues(townBeads, convoyID string) []trackedIssueInfo {
|
|||||||
|
|
||||||
// Second pass: build result using the batch lookup
|
// Second pass: build result using the batch lookup
|
||||||
var tracked []trackedIssueInfo
|
var tracked []trackedIssueInfo
|
||||||
for _, issueID := range issueIDs {
|
for _, ref := range issueRefs {
|
||||||
|
issueID := ref.ID
|
||||||
info := trackedIssueInfo{
|
info := trackedIssueInfo{
|
||||||
ID: issueID,
|
ID: issueID,
|
||||||
Type: idToDepType[issueID],
|
Type: idToDepType[issueID],
|
||||||
@@ -1396,6 +1418,58 @@ func getTrackedIssues(townBeads, convoyID string) []trackedIssueInfo {
|
|||||||
return tracked
|
return tracked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getExternalIssueDetails fetches issue details from an external rig database.
|
||||||
|
// townBeads: path to town .beads directory
|
||||||
|
// rigName: name of the rig (e.g., "claycantrell")
|
||||||
|
// issueID: the issue ID to look up
|
||||||
|
func getExternalIssueDetails(townBeads, rigName, issueID string) *issueDetails {
|
||||||
|
// Resolve rig directory path: town parent + rig name
|
||||||
|
townParent := filepath.Dir(townBeads)
|
||||||
|
rigDir := filepath.Join(townParent, rigName)
|
||||||
|
|
||||||
|
// Check if rig directory exists
|
||||||
|
if _, err := os.Stat(rigDir); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query the rig database by running bd show from the rig directory
|
||||||
|
// Use --allow-stale to handle cases where JSONL and DB are out of sync
|
||||||
|
showCmd := exec.Command("bd", "--no-daemon", "show", issueID, "--json", "--allow-stale")
|
||||||
|
showCmd.Dir = rigDir // Set working directory to rig directory
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
showCmd.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := showCmd.Run(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if stdout.Len() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var issues []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
IssueType string `json:"issue_type"`
|
||||||
|
Assignee string `json:"assignee"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(issues) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
issue := issues[0]
|
||||||
|
return &issueDetails{
|
||||||
|
ID: issue.ID,
|
||||||
|
Title: issue.Title,
|
||||||
|
Status: issue.Status,
|
||||||
|
IssueType: issue.IssueType,
|
||||||
|
Assignee: issue.Assignee,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// issueDetails holds basic issue info.
|
// issueDetails holds basic issue info.
|
||||||
type issueDetails struct {
|
type issueDetails struct {
|
||||||
ID string
|
ID string
|
||||||
|
|||||||
Reference in New Issue
Block a user