diff --git a/cmd/actions.go b/cmd/actions.go index 8d7aa28..1e48823 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -109,6 +109,30 @@ var runViewCmd = &cobra.Command{ RunE: runRunView, } +var runWatchCmd = &cobra.Command{ + Use: "watch ", + Short: "Watch a workflow run", + Long: "Poll a workflow run until it completes.", + Args: cobra.ExactArgs(1), + RunE: runRunWatch, +} + +var runRerunCmd = &cobra.Command{ + Use: "rerun ", + Short: "Rerun a workflow run", + Long: "Trigger a rerun for a specific workflow run.", + Args: cobra.ExactArgs(1), + RunE: runRunRerun, +} + +var runCancelCmd = &cobra.Command{ + Use: "cancel ", + Short: "Cancel a workflow run", + Long: "Cancel a running workflow run.", + Args: cobra.ExactArgs(1), + RunE: runRunCancel, +} + // Workflow commands var workflowCmd = &cobra.Command{ Use: "workflow", @@ -222,6 +246,9 @@ func init() { actionsCmd.AddCommand(runCmd) runCmd.AddCommand(runListCmd) runCmd.AddCommand(runViewCmd) + runCmd.AddCommand(runWatchCmd) + runCmd.AddCommand(runRerunCmd) + runCmd.AddCommand(runCancelCmd) // Add workflow commands (gh workflow compatible) actionsCmd.AddCommand(workflowCmd) @@ -253,6 +280,10 @@ func init() { runViewCmd.Flags().StringP("job", "j", "", "View a specific job ID from a run") runViewCmd.Flags().BoolP("log-failed", "", false, "View the log for any failed steps in a run or specific job") runViewCmd.Flags().Bool("json", false, "Output workflow run as JSON") + addRepoFlags(runWatchCmd) + runWatchCmd.Flags().DurationP("interval", "i", 5*time.Second, "Polling interval") + addRepoFlags(runRerunCmd) + addRepoFlags(runCancelCmd) // Add flags for workflow commands addRepoFlags(workflowListCmd) @@ -545,6 +576,118 @@ func runRunView(cmd *cobra.Command, args []string) error { return nil } +func runRunWatch(cmd *cobra.Command, args []string) error { + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + repo, _ := cmd.Flags().GetString("repo") + owner, name, err := parseRepo(repo) + if err != nil { + return err + } + + runID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid run ID: %w", err) + } + + interval, _ := cmd.Flags().GetDuration("interval") + if interval <= 0 { + return fmt.Errorf("interval must be greater than 0") + } + + endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", owner, name, runID) + + var lastStatus string + for { + var run ActionRun + if err := client.GetJSON(endpoint, &run); err != nil { + return fmt.Errorf("failed to get run: %w", err) + } + + if run.Status != lastStatus { + fmt.Printf("Status: %s\n", formatStatus(run.Status)) + lastStatus = run.Status + } + + if isRunComplete(run.Status) { + fmt.Printf("Run #%d completed with status %s\n", run.IndexInRepo, formatStatus(run.Status)) + return nil + } + + time.Sleep(interval) + } +} + +func runRunRerun(cmd *cobra.Command, args []string) error { + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + repo, _ := cmd.Flags().GetString("repo") + owner, name, err := parseRepo(repo) + if err != nil { + return err + } + + runID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid run ID: %w", err) + } + + endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/rerun", owner, name, runID) + if err := client.PostJSON(endpoint, nil, nil); err != nil { + return fmt.Errorf("failed to rerun workflow: %w", err) + } + + fmt.Printf("✓ Rerun requested for run %d\n", runID) + return nil +} + +func runRunCancel(cmd *cobra.Command, args []string) error { + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + repo, _ := cmd.Flags().GetString("repo") + owner, name, err := parseRepo(repo) + if err != nil { + return err + } + + runID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid run ID: %w", err) + } + + endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/cancel", owner, name, runID) + if err := client.PostJSON(endpoint, nil, nil); err != nil { + return fmt.Errorf("failed to cancel workflow run: %w", err) + } + + fmt.Printf("✓ Cancel requested for run %d\n", runID) + return nil +} + func showJobLog(client *api.Client, owner, name string, runNumber int64, task ActionTask, logFailed bool) error { // Fetch log from /repos/{owner}/{repo}/actions/runs/{run_number}/jobs/{job_id}/logs logURL := fmt.Sprintf("https://%s/%s/%s/actions/runs/%d/jobs/%d/logs", @@ -594,6 +737,15 @@ func formatStatus(status string) string { } } +func isRunComplete(status string) bool { + switch status { + case "success", "failure", "cancelled", "skipped": + return true + default: + return false + } +} + func formatTimeSince(t time.Time) string { duration := time.Since(t)