feat: add refinery package and CLI commands
- internal/refinery: Types for Refinery, MergeRequest, queue items - internal/refinery: Manager with start/stop/status/queue operations - gt refinery start: Start refinery for a rig (--foreground option) - gt refinery stop: Stop running refinery - gt refinery status: Show refinery state and statistics - gt refinery queue: Show pending merge requests - Auto-discover polecat work branches as queue items - JSON output support for status and queue commands Closes gt-rm3 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
303
internal/cmd/refinery.go
Normal file
303
internal/cmd/refinery.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/refinery"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
// Refinery command flags
|
||||
var (
|
||||
refineryForeground bool
|
||||
refineryStatusJSON bool
|
||||
refineryQueueJSON bool
|
||||
)
|
||||
|
||||
var refineryCmd = &cobra.Command{
|
||||
Use: "refinery",
|
||||
Short: "Manage the merge queue processor",
|
||||
Long: `Manage the Refinery merge queue processor for a rig.
|
||||
|
||||
The Refinery processes merge requests from polecats, merging their work
|
||||
into integration branches and ultimately to main.`,
|
||||
}
|
||||
|
||||
var refineryStartCmd = &cobra.Command{
|
||||
Use: "start <rig>",
|
||||
Short: "Start the refinery",
|
||||
Long: `Start the Refinery for a rig.
|
||||
|
||||
Launches the merge queue processor which monitors for polecat work branches
|
||||
and merges them to the appropriate target branches.
|
||||
|
||||
Examples:
|
||||
gt refinery start gastown
|
||||
gt refinery start gastown --foreground`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRefineryStart,
|
||||
}
|
||||
|
||||
var refineryStopCmd = &cobra.Command{
|
||||
Use: "stop <rig>",
|
||||
Short: "Stop the refinery",
|
||||
Long: `Stop a running Refinery.
|
||||
|
||||
Gracefully stops the refinery, completing any in-progress merge first.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRefineryStop,
|
||||
}
|
||||
|
||||
var refineryStatusCmd = &cobra.Command{
|
||||
Use: "status <rig>",
|
||||
Short: "Show refinery status",
|
||||
Long: `Show the status of a rig's Refinery.
|
||||
|
||||
Displays running state, current work, queue length, and statistics.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRefineryStatus,
|
||||
}
|
||||
|
||||
var refineryQueueCmd = &cobra.Command{
|
||||
Use: "queue <rig>",
|
||||
Short: "Show merge queue",
|
||||
Long: `Show the merge queue for a rig.
|
||||
|
||||
Lists all pending merge requests waiting to be processed.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRefineryQueue,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Start flags
|
||||
refineryStartCmd.Flags().BoolVar(&refineryForeground, "foreground", false, "Run in foreground (default: background)")
|
||||
|
||||
// Status flags
|
||||
refineryStatusCmd.Flags().BoolVar(&refineryStatusJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Queue flags
|
||||
refineryQueueCmd.Flags().BoolVar(&refineryQueueJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Add subcommands
|
||||
refineryCmd.AddCommand(refineryStartCmd)
|
||||
refineryCmd.AddCommand(refineryStopCmd)
|
||||
refineryCmd.AddCommand(refineryStatusCmd)
|
||||
refineryCmd.AddCommand(refineryQueueCmd)
|
||||
|
||||
rootCmd.AddCommand(refineryCmd)
|
||||
}
|
||||
|
||||
// getRefineryManager creates a refinery manager for a rig.
|
||||
func getRefineryManager(rigName string) (*refinery.Manager, *rig.Rig, error) {
|
||||
townRoot, err := workspace.FindFromCwdOrError()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||
}
|
||||
|
||||
rigsConfigPath := filepath.Join(townRoot, "config", "rigs.json")
|
||||
rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
|
||||
if err != nil {
|
||||
rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
|
||||
}
|
||||
|
||||
g := git.NewGit(townRoot)
|
||||
rigMgr := rig.NewManager(townRoot, rigsConfig, g)
|
||||
r, err := rigMgr.GetRig(rigName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("rig '%s' not found", rigName)
|
||||
}
|
||||
|
||||
mgr := refinery.NewManager(r)
|
||||
return mgr, r, nil
|
||||
}
|
||||
|
||||
func runRefineryStart(cmd *cobra.Command, args []string) error {
|
||||
rigName := args[0]
|
||||
|
||||
mgr, _, err := getRefineryManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Starting refinery for %s...\n", rigName)
|
||||
|
||||
if err := mgr.Start(refineryForeground); err != nil {
|
||||
if err == refinery.ErrAlreadyRunning {
|
||||
fmt.Printf("%s Refinery is already running\n", style.Dim.Render("⚠"))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("starting refinery: %w", err)
|
||||
}
|
||||
|
||||
if refineryForeground {
|
||||
// This will block until stopped
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s Refinery started for %s\n", style.Bold.Render("✓"), rigName)
|
||||
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt refinery status' to check progress"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRefineryStop(cmd *cobra.Command, args []string) error {
|
||||
rigName := args[0]
|
||||
|
||||
mgr, _, err := getRefineryManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := mgr.Stop(); err != nil {
|
||||
if err == refinery.ErrNotRunning {
|
||||
fmt.Printf("%s Refinery is not running\n", style.Dim.Render("⚠"))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("stopping refinery: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Refinery stopped for %s\n", style.Bold.Render("✓"), rigName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRefineryStatus(cmd *cobra.Command, args []string) error {
|
||||
rigName := args[0]
|
||||
|
||||
mgr, _, err := getRefineryManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, err := mgr.Status()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting status: %w", err)
|
||||
}
|
||||
|
||||
// JSON output
|
||||
if refineryStatusJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(ref)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("%s Refinery: %s\n\n", style.Bold.Render("⚙"), rigName)
|
||||
|
||||
stateStr := string(ref.State)
|
||||
switch ref.State {
|
||||
case refinery.StateRunning:
|
||||
stateStr = style.Bold.Render("● running")
|
||||
case refinery.StateStopped:
|
||||
stateStr = style.Dim.Render("○ stopped")
|
||||
case refinery.StatePaused:
|
||||
stateStr = style.Dim.Render("⏸ paused")
|
||||
}
|
||||
fmt.Printf(" State: %s\n", stateStr)
|
||||
|
||||
if ref.StartedAt != nil {
|
||||
fmt.Printf(" Started: %s\n", ref.StartedAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
if ref.CurrentMR != nil {
|
||||
fmt.Printf("\n %s\n", style.Bold.Render("Currently Processing:"))
|
||||
fmt.Printf(" Branch: %s\n", ref.CurrentMR.Branch)
|
||||
fmt.Printf(" Worker: %s\n", ref.CurrentMR.Worker)
|
||||
if ref.CurrentMR.IssueID != "" {
|
||||
fmt.Printf(" Issue: %s\n", ref.CurrentMR.IssueID)
|
||||
}
|
||||
}
|
||||
|
||||
// Get queue length
|
||||
queue, _ := mgr.Queue()
|
||||
pendingCount := 0
|
||||
for _, item := range queue {
|
||||
if item.Position > 0 { // Not currently processing
|
||||
pendingCount++
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n Queue: %d pending\n", pendingCount)
|
||||
|
||||
if ref.LastMergeAt != nil {
|
||||
fmt.Printf(" Last merge: %s\n", ref.LastMergeAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
fmt.Printf("\n %s\n", style.Bold.Render("Statistics:"))
|
||||
fmt.Printf(" Merged today: %d\n", ref.Stats.TodayMerged)
|
||||
fmt.Printf(" Failed today: %d\n", ref.Stats.TodayFailed)
|
||||
fmt.Printf(" Total merged: %d\n", ref.Stats.TotalMerged)
|
||||
fmt.Printf(" Total failed: %d\n", ref.Stats.TotalFailed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRefineryQueue(cmd *cobra.Command, args []string) error {
|
||||
rigName := args[0]
|
||||
|
||||
mgr, _, err := getRefineryManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queue, err := mgr.Queue()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting queue: %w", err)
|
||||
}
|
||||
|
||||
// JSON output
|
||||
if refineryQueueJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(queue)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("%s Merge queue for '%s':\n\n", style.Bold.Render("📋"), rigName)
|
||||
|
||||
if len(queue) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(empty)"))
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, item := range queue {
|
||||
status := ""
|
||||
prefix := fmt.Sprintf(" %d.", item.Position)
|
||||
|
||||
if item.Position == 0 {
|
||||
prefix = " ▶"
|
||||
status = style.Bold.Render("[processing]")
|
||||
} else {
|
||||
switch item.MR.Status {
|
||||
case refinery.MRPending:
|
||||
status = style.Dim.Render("[pending]")
|
||||
case refinery.MRMerged:
|
||||
status = style.Bold.Render("[merged]")
|
||||
case refinery.MRFailed:
|
||||
status = style.Dim.Render("[failed]")
|
||||
case refinery.MRSkipped:
|
||||
status = style.Dim.Render("[skipped]")
|
||||
}
|
||||
}
|
||||
|
||||
issueInfo := ""
|
||||
if item.MR.IssueID != "" {
|
||||
issueInfo = fmt.Sprintf(" (%s)", item.MR.IssueID)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s %s/%s%s %s\n",
|
||||
prefix,
|
||||
status,
|
||||
item.MR.Worker,
|
||||
item.MR.Branch,
|
||||
issueInfo,
|
||||
style.Dim.Render(item.Age))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user