feat(convoy): add specific convoy ID check and dry-run flag
Add support for checking a specific convoy by ID instead of all convoys: - `gt convoy check <convoy-id>` - check specific convoy - `gt convoy check` - check all (existing behavior) - `gt convoy check --dry-run` - preview mode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,7 @@ var (
|
|||||||
convoyStrandedJSON bool
|
convoyStrandedJSON bool
|
||||||
convoyCloseReason string
|
convoyCloseReason string
|
||||||
convoyCloseNotify string
|
convoyCloseNotify string
|
||||||
|
convoyCheckDryRun bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var convoyCmd = &cobra.Command{
|
var convoyCmd = &cobra.Command{
|
||||||
@@ -177,14 +178,22 @@ Examples:
|
|||||||
}
|
}
|
||||||
|
|
||||||
var convoyCheckCmd = &cobra.Command{
|
var convoyCheckCmd = &cobra.Command{
|
||||||
Use: "check",
|
Use: "check [convoy-id]",
|
||||||
Short: "Check and auto-close completed convoys",
|
Short: "Check and auto-close completed convoys",
|
||||||
Long: `Check all open convoys and auto-close any where all tracked issues are complete.
|
Long: `Check convoys and auto-close any where all tracked issues are complete.
|
||||||
|
|
||||||
|
Without arguments, checks all open convoys. With a convoy ID, checks only that convoy.
|
||||||
|
|
||||||
This handles cross-rig convoy completion: convoys in town beads tracking issues
|
This handles cross-rig convoy completion: convoys in town beads tracking issues
|
||||||
in rig beads won't auto-close via bd close alone. This command bridges that gap.
|
in rig beads won't auto-close via bd close alone. This command bridges that gap.
|
||||||
|
|
||||||
Can be run manually or by deacon patrol to ensure convoys close promptly.`,
|
Can be run manually or by deacon patrol to ensure convoys close promptly.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt convoy check # Check all open convoys
|
||||||
|
gt convoy check hq-cv-abc # Check specific convoy
|
||||||
|
gt convoy check --dry-run # Preview what would close without acting`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: runConvoyCheck,
|
RunE: runConvoyCheck,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +257,9 @@ func init() {
|
|||||||
// Interactive TUI flag (on parent command)
|
// Interactive TUI flag (on parent command)
|
||||||
convoyCmd.Flags().BoolVarP(&convoyInteractive, "interactive", "i", false, "Interactive tree view")
|
convoyCmd.Flags().BoolVarP(&convoyInteractive, "interactive", "i", false, "Interactive tree view")
|
||||||
|
|
||||||
|
// Check flags
|
||||||
|
convoyCheckCmd.Flags().BoolVar(&convoyCheckDryRun, "dry-run", false, "Preview what would close without acting")
|
||||||
|
|
||||||
// Stranded flags
|
// Stranded flags
|
||||||
convoyStrandedCmd.Flags().BoolVar(&convoyStrandedJSON, "json", false, "Output as JSON")
|
convoyStrandedCmd.Flags().BoolVar(&convoyStrandedJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
@@ -478,15 +490,26 @@ func runConvoyCheck(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
closed, err := checkAndCloseCompletedConvoys(townBeads)
|
// If a specific convoy ID is provided, check only that convoy
|
||||||
|
if len(args) == 1 {
|
||||||
|
convoyID := args[0]
|
||||||
|
return checkSingleConvoy(townBeads, convoyID, convoyCheckDryRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all open convoys
|
||||||
|
closed, err := checkAndCloseCompletedConvoys(townBeads, convoyCheckDryRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(closed) == 0 {
|
if len(closed) == 0 {
|
||||||
fmt.Println("No convoys ready to close.")
|
fmt.Println("No convoys ready to close.")
|
||||||
|
} else {
|
||||||
|
if convoyCheckDryRun {
|
||||||
|
fmt.Printf("%s Would auto-close %d convoy(s):\n", style.Warning.Render("⚠"), len(closed))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s Auto-closed %d convoy(s):\n", style.Bold.Render("✓"), len(closed))
|
fmt.Printf("%s Auto-closed %d convoy(s):\n", style.Bold.Render("✓"), len(closed))
|
||||||
|
}
|
||||||
for _, c := range closed {
|
for _, c := range closed {
|
||||||
fmt.Printf(" 🚚 %s: %s\n", c.ID, c.Title)
|
fmt.Printf(" 🚚 %s: %s\n", c.ID, c.Title)
|
||||||
}
|
}
|
||||||
@@ -495,6 +518,92 @@ func runConvoyCheck(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkSingleConvoy checks a specific convoy and closes it if all tracked issues are complete.
|
||||||
|
func checkSingleConvoy(townBeads, convoyID string, dryRun bool) error {
|
||||||
|
// Get convoy details
|
||||||
|
showArgs := []string{"show", convoyID, "--json"}
|
||||||
|
showCmd := exec.Command("bd", showArgs...)
|
||||||
|
showCmd.Dir = townBeads
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
showCmd.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := showCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("convoy '%s' not found", convoyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var convoys []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Type string `json:"issue_type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &convoys); err != nil {
|
||||||
|
return fmt.Errorf("parsing convoy data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(convoys) == 0 {
|
||||||
|
return fmt.Errorf("convoy '%s' not found", convoyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
convoy := convoys[0]
|
||||||
|
|
||||||
|
// Verify it's actually a convoy type
|
||||||
|
if convoy.Type != "convoy" {
|
||||||
|
return fmt.Errorf("'%s' is not a convoy (type: %s)", convoyID, convoy.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if convoy is already closed
|
||||||
|
if convoy.Status == "closed" {
|
||||||
|
fmt.Printf("%s Convoy %s is already closed\n", style.Dim.Render("○"), convoyID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tracked issues
|
||||||
|
tracked := getTrackedIssues(townBeads, convoyID)
|
||||||
|
if len(tracked) == 0 {
|
||||||
|
fmt.Printf("%s Convoy %s has no tracked issues\n", style.Dim.Render("○"), convoyID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all tracked issues are closed
|
||||||
|
allClosed := true
|
||||||
|
openCount := 0
|
||||||
|
for _, t := range tracked {
|
||||||
|
if t.Status != "closed" && t.Status != "tombstone" {
|
||||||
|
allClosed = false
|
||||||
|
openCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allClosed {
|
||||||
|
fmt.Printf("%s Convoy %s has %d open issue(s) remaining\n", style.Dim.Render("○"), convoyID, openCount)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All tracked issues are complete - close the convoy
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("%s Would auto-close convoy 🚚 %s: %s\n", style.Warning.Render("⚠"), convoyID, convoy.Title)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actually close the convoy
|
||||||
|
closeArgs := []string{"close", convoyID, "-r", "All tracked issues completed"}
|
||||||
|
closeCmd := exec.Command("bd", closeArgs...)
|
||||||
|
closeCmd.Dir = townBeads
|
||||||
|
|
||||||
|
if err := closeCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("closing convoy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Auto-closed convoy 🚚 %s: %s\n", style.Bold.Render("✓"), convoyID, convoy.Title)
|
||||||
|
|
||||||
|
// Send completion notification
|
||||||
|
notifyConvoyCompletion(townBeads, convoyID, convoy.Title)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func runConvoyClose(cmd *cobra.Command, args []string) error {
|
func runConvoyClose(cmd *cobra.Command, args []string) error {
|
||||||
convoyID := args[0]
|
convoyID := args[0]
|
||||||
|
|
||||||
@@ -761,8 +870,9 @@ func isReadyIssue(t trackedIssueInfo, blockedIssues map[string]bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkAndCloseCompletedConvoys finds open convoys where all tracked issues are closed
|
// checkAndCloseCompletedConvoys finds open convoys where all tracked issues are closed
|
||||||
// and auto-closes them. Returns the list of convoys that were closed.
|
// and auto-closes them. Returns the list of convoys that were closed (or would be closed in dry-run mode).
|
||||||
func checkAndCloseCompletedConvoys(townBeads string) ([]struct{ ID, Title string }, error) {
|
// If dryRun is true, no changes are made and the function returns what would have been closed.
|
||||||
|
func checkAndCloseCompletedConvoys(townBeads string, dryRun bool) ([]struct{ ID, Title string }, error) {
|
||||||
var closed []struct{ ID, Title string }
|
var closed []struct{ ID, Title string }
|
||||||
|
|
||||||
// List all open convoys
|
// List all open convoys
|
||||||
@@ -801,6 +911,12 @@ func checkAndCloseCompletedConvoys(townBeads string) ([]struct{ ID, Title string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if allClosed {
|
if allClosed {
|
||||||
|
if dryRun {
|
||||||
|
// In dry-run mode, just record what would be closed
|
||||||
|
closed = append(closed, struct{ ID, Title string }{convoy.ID, convoy.Title})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Close the convoy
|
// Close the convoy
|
||||||
closeArgs := []string{"close", convoy.ID, "-r", "All tracked issues completed"}
|
closeArgs := []string{"close", convoy.ID, "-r", "All tracked issues completed"}
|
||||||
closeCmd := exec.Command("bd", closeArgs...)
|
closeCmd := exec.Command("bd", closeArgs...)
|
||||||
|
|||||||
Reference in New Issue
Block a user