feat: add json output for list and view commands

This commit is contained in:
Romain Bertrand 2026-01-18 11:48:08 +01:00
parent fe23f2fce3
commit 3ccef4e1c6
5 changed files with 192 additions and 52 deletions

View file

@ -16,17 +16,17 @@ import (
// ActionRun represents a workflow run
type ActionRun struct {
ID int64 `json:"id"`
Title string `json:"title"`
WorkflowID string `json:"workflow_id"`
IndexInRepo int64 `json:"index_in_repo"`
Event string `json:"event"`
Status string `json:"status"`
CommitSHA string `json:"commit_sha"`
PrettyRef string `json:"prettyref"`
Created string `json:"created"`
Updated string `json:"updated"`
Started string `json:"started"`
ID int64 `json:"id"`
Title string `json:"title"`
WorkflowID string `json:"workflow_id"`
IndexInRepo int64 `json:"index_in_repo"`
Event string `json:"event"`
Status string `json:"status"`
CommitSHA string `json:"commit_sha"`
PrettyRef string `json:"prettyref"`
Created string `json:"created"`
Updated string `json:"updated"`
Started string `json:"started"`
}
// ActionRunList represents a list of workflow runs
@ -37,19 +37,19 @@ type ActionRunList struct {
// ActionTask represents a job/task within a workflow run
type ActionTask struct {
ID int64 `json:"id"`
Name string `json:"name"`
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
RunNumber int64 `json:"run_number"`
Event string `json:"event"`
DisplayTitle string `json:"display_title"`
Status string `json:"status"`
WorkflowID string `json:"workflow_id"`
URL string `json:"url"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
RunStartedAt string `json:"run_started_at"`
ID int64 `json:"id"`
Name string `json:"name"`
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
RunNumber int64 `json:"run_number"`
Event string `json:"event"`
DisplayTitle string `json:"display_title"`
Status string `json:"status"`
WorkflowID string `json:"workflow_id"`
URL string `json:"url"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
RunStartedAt string `json:"run_started_at"`
}
// ActionTaskList represents a list of tasks/jobs
@ -246,16 +246,20 @@ func init() {
// Add flags for run commands
addRepoFlags(runListCmd)
runListCmd.Flags().IntP("limit", "L", 20, "Maximum number of runs to list")
runListCmd.Flags().Bool("json", false, "Output workflow runs as JSON")
addRepoFlags(runViewCmd)
runViewCmd.Flags().BoolP("verbose", "v", false, "Show job steps")
runViewCmd.Flags().BoolP("log", "", false, "View full log for either a run or specific job")
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")
// Add flags for workflow commands
addRepoFlags(workflowListCmd)
workflowListCmd.Flags().IntP("limit", "L", 20, "Maximum number of workflows to list")
workflowListCmd.Flags().Bool("json", false, "Output workflows as JSON")
addRepoFlags(workflowViewCmd)
workflowViewCmd.Flags().Bool("json", false, "Output workflow as JSON")
addRepoFlags(workflowRunCmd)
workflowRunCmd.Flags().StringP("ref", "r", "", "Branch or tag name to run the workflow on (defaults to repository's default branch)")
workflowRunCmd.Flags().StringSliceP("field", "f", nil, "Add a string parameter in key=value format (can be used multiple times)")
@ -307,6 +311,10 @@ func runRunList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to list runs: %w", err)
}
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
return writeJSON(runList.WorkflowRuns)
}
if len(runList.WorkflowRuns) == 0 {
fmt.Println("No workflow runs found")
return nil
@ -364,6 +372,7 @@ func runRunView(cmd *cobra.Command, args []string) error {
showLog, _ := cmd.Flags().GetBool("log")
jobIDStr, _ := cmd.Flags().GetString("job")
showLogFailed, _ := cmd.Flags().GetBool("log-failed")
jsonOutput, _ := cmd.Flags().GetBool("json")
var jobID int64
if jobIDStr != "" {
@ -374,6 +383,10 @@ func runRunView(cmd *cobra.Command, args []string) error {
}
}
if jsonOutput && (showLog || showLogFailed) {
return fmt.Errorf("--json cannot be used with --log or --log-failed")
}
// Call the API endpoint directly since SDK doesn't have it yet
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", owner, name, runID)
@ -382,6 +395,48 @@ func runRunView(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to get run: %w", err)
}
needsJobs := verbose || showLog || showLogFailed || jobID > 0
if jsonOutput {
var runTasks []ActionTask
if needsJobs {
tasksEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, name)
var taskList ActionTaskList
if err := client.GetJSON(tasksEndpoint, &taskList); err != nil {
return fmt.Errorf("failed to get tasks: %w", err)
}
for _, task := range taskList.WorkflowRuns {
if task.RunNumber == run.IndexInRepo {
runTasks = append(runTasks, task)
}
}
if jobID > 0 {
var filtered []ActionTask
for _, task := range runTasks {
if task.ID == jobID {
filtered = append(filtered, task)
break
}
}
if len(filtered) == 0 {
return fmt.Errorf("job %d not found in this run", jobID)
}
runTasks = filtered
}
}
payload := struct {
Run ActionRun `json:"run"`
Tasks []ActionTask `json:"tasks,omitempty"`
}{
Run: run,
Tasks: runTasks,
}
return writeJSON(payload)
}
// Display run information
fmt.Printf("Title: %s\n", run.Title)
fmt.Printf("Workflow: %s\n", run.WorkflowID)
@ -409,7 +464,6 @@ func runRunView(cmd *cobra.Command, args []string) error {
}
// Fetch jobs if needed for verbose, log, or job-specific views
needsJobs := verbose || showLog || showLogFailed || jobID > 0
if !needsJobs {
return nil
}
@ -620,10 +674,17 @@ func runWorkflowList(cmd *cobra.Command, args []string) error {
}
if len(allWorkflows) == 0 {
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
return writeJSON(allWorkflows)
}
fmt.Println("No workflows found")
return nil
}
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
return writeJSON(allWorkflows)
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
if _, err := fmt.Fprintln(w, "NAME\tSTATE\tPATH"); err != nil {
return fmt.Errorf("failed to write header: %w", err)
@ -694,26 +755,39 @@ func runWorkflowView(cmd *cobra.Command, args []string) error {
return fmt.Errorf("workflow '%s' not found", workflowIdentifier)
}
jsonOutput, _ := cmd.Flags().GetBool("json")
var latestRun *ActionRun
// Get the latest run for this workflow
runsEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?workflow_id=%s&limit=1", owner, name, workflow.Path)
var runList ActionRunList
if err := client.GetJSON(runsEndpoint, &runList); err == nil && len(runList.WorkflowRuns) > 0 {
latestRun = &runList.WorkflowRuns[0]
}
if jsonOutput {
payload := struct {
Workflow *Workflow `json:"workflow"`
LatestRun *ActionRun `json:"latest_run,omitempty"`
}{
Workflow: workflow,
LatestRun: latestRun,
}
return writeJSON(payload)
}
// Display workflow information
fmt.Printf("Name: %s\n", workflow.Name)
fmt.Printf("Path: %s\n", workflow.Path)
fmt.Printf("State: %s\n", workflow.State)
// Get the latest run for this workflow
runsEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?workflow_id=%s&limit=1", owner, name, workflow.Path)
var runList ActionRunList
if err := client.GetJSON(runsEndpoint, &runList); err != nil {
// If we can't get runs, just display workflow info without latest run
return nil
}
if len(runList.WorkflowRuns) > 0 {
run := runList.WorkflowRuns[0]
if latestRun != nil {
fmt.Printf("\nLatest run:\n")
fmt.Printf(" Status: %s\n", formatStatus(run.Status))
fmt.Printf(" Event: %s\n", run.Event)
fmt.Printf(" Ref: %s\n", run.PrettyRef)
if createdTime, err := time.Parse(time.RFC3339, run.Created); err == nil {
fmt.Printf(" Status: %s\n", formatStatus(latestRun.Status))
fmt.Printf(" Event: %s\n", latestRun.Event)
fmt.Printf(" Ref: %s\n", latestRun.PrettyRef)
if createdTime, err := time.Parse(time.RFC3339, latestRun.Created); err == nil {
fmt.Printf(" Created: %s\n", formatTimeSince(createdTime))
}
}