feat: add actions run watch rerun and cancel

This commit is contained in:
Romain Bertrand 2026-01-18 11:49:03 +01:00
parent 3ccef4e1c6
commit c2ee338f1c

View file

@ -109,6 +109,30 @@ var runViewCmd = &cobra.Command{
RunE: runRunView,
}
var runWatchCmd = &cobra.Command{
Use: "watch <run-id>",
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 <run-id>",
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 <run-id>",
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)