feat: actions support

This commit is contained in:
Romain Bertrand 2025-12-09 13:41:08 +01:00
parent ca17526594
commit 8db012cb7a
2 changed files with 127 additions and 53 deletions

View file

@ -189,9 +189,10 @@ func init() {
addRepoFlags(runListCmd) addRepoFlags(runListCmd)
runListCmd.Flags().IntP("limit", "L", 20, "Maximum number of runs to list") runListCmd.Flags().IntP("limit", "L", 20, "Maximum number of runs to list")
addRepoFlags(runViewCmd) addRepoFlags(runViewCmd)
runViewCmd.Flags().BoolP("verbose", "v", false, "Show job details") runViewCmd.Flags().BoolP("verbose", "v", false, "Show job steps")
runViewCmd.Flags().BoolP("log", "", false, "Show logs for all jobs") runViewCmd.Flags().BoolP("log", "", false, "View full log for either a run or specific job")
runViewCmd.Flags().Int64P("job", "j", 0, "Show logs for a specific job ID") 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")
// Add flags for secret commands // Add flags for secret commands
addRepoFlags(actionsSecretListCmd) addRepoFlags(actionsSecretListCmd)
@ -294,7 +295,17 @@ func runRunView(cmd *cobra.Command, args []string) error {
verbose, _ := cmd.Flags().GetBool("verbose") verbose, _ := cmd.Flags().GetBool("verbose")
showLog, _ := cmd.Flags().GetBool("log") showLog, _ := cmd.Flags().GetBool("log")
jobID, _ := cmd.Flags().GetInt64("job") jobIDStr, _ := cmd.Flags().GetString("job")
showLogFailed, _ := cmd.Flags().GetBool("log-failed")
var jobID int64
if jobIDStr != "" {
var err error
jobID, err = strconv.ParseInt(jobIDStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid job ID: %w", err)
}
}
// Call the API endpoint directly since SDK doesn't have it yet // 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) endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", owner, name, runID)
@ -330,71 +341,90 @@ func runRunView(cmd *cobra.Command, args []string) error {
fmt.Printf("Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05")) fmt.Printf("Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
} }
// If --log or --job flag is provided, or --verbose, fetch jobs // Fetch jobs if needed for verbose, log, or job-specific views
if verbose || showLog || jobID > 0 { needsJobs := verbose || showLog || showLogFailed || jobID > 0
tasksEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, name) if !needsJobs {
var taskList ActionTaskList return nil
if err := client.GetJSON(tasksEndpoint, &taskList); err != nil { }
return fmt.Errorf("failed to get tasks: %w", err)
}
// Filter tasks for this run number tasksEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, name)
var runTasks []ActionTask var taskList ActionTaskList
for _, task := range taskList.WorkflowRuns { if err := client.GetJSON(tasksEndpoint, &taskList); err != nil {
if task.RunNumber == run.IndexInRepo { return fmt.Errorf("failed to get tasks: %w", err)
runTasks = append(runTasks, task) }
// Filter tasks for this run number
var runTasks []ActionTask
for _, task := range taskList.WorkflowRuns {
if task.RunNumber == run.IndexInRepo {
runTasks = append(runTasks, task)
}
}
if len(runTasks) == 0 {
fmt.Println("\nNo jobs found for this run")
return nil
}
// If --job is specified, filter to that job
if jobID > 0 {
var found bool
for _, task := range runTasks {
if task.ID == jobID {
runTasks = []ActionTask{task}
found = true
break
} }
} }
if !found {
if len(runTasks) == 0 { return fmt.Errorf("job %d not found in this run", jobID)
fmt.Println("\nNo jobs found for this run")
return nil
} }
}
// Show jobs if verbose (and not showing logs) // Case 1: --verbose (show job steps/details without logs)
if verbose && !showLog && jobID == 0 { if verbose && !showLog && !showLogFailed {
fmt.Println("\nJobs:") fmt.Println("\nJobs:")
for _, task := range runTasks { for _, task := range runTasks {
fmt.Printf("\n %s - %s (ID: %d)\n", formatStatus(task.Status), task.Name, task.ID) fmt.Printf("\n %s - %s (ID: %d)\n", formatStatus(task.Status), task.Name, task.ID)
if startedTime, err := time.Parse(time.RFC3339, task.RunStartedAt); err == nil { if startedTime, err := time.Parse(time.RFC3339, task.RunStartedAt); err == nil {
fmt.Printf(" Started: %s\n", startedTime.Format("2006-01-02 15:04:05")) fmt.Printf(" Started: %s\n", startedTime.Format("2006-01-02 15:04:05"))
} }
if updatedTime, err := time.Parse(time.RFC3339, task.UpdatedAt); err == nil { if updatedTime, err := time.Parse(time.RFC3339, task.UpdatedAt); err == nil {
fmt.Printf(" Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05")) fmt.Printf(" Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
}
} }
return nil
} }
return nil
}
// Show logs for specific job or all jobs // Case 2: --log or --log-failed (show logs)
if showLog || jobID > 0 { if showLog || showLogFailed {
tasksToShow := runTasks for _, task := range runTasks {
if jobID > 0 { if err := showJobLog(client, owner, name, run.IndexInRepo, task, showLogFailed); err != nil {
// Filter to specific job fmt.Printf("\nError fetching log for job %s: %v\n", task.Name, err)
tasksToShow = nil
for _, task := range runTasks {
if task.ID == jobID {
tasksToShow = []ActionTask{task}
break
}
}
if len(tasksToShow) == 0 {
return fmt.Errorf("job %d not found in this run", jobID)
}
} }
}
return nil
}
for _, task := range tasksToShow { // Case 3: --job without --log or --verbose (show job details only)
if err := showJobLog(client, owner, name, run.IndexInRepo, task); err != nil { if jobID > 0 {
fmt.Printf("\nError fetching log for job %s: %v\n", task.Name, err) task := runTasks[0]
} fmt.Println("\nJob Details:")
} fmt.Printf(" Name: %s\n", task.Name)
fmt.Printf(" ID: %d\n", task.ID)
fmt.Printf(" Status: %s\n", formatStatus(task.Status))
if startedTime, err := time.Parse(time.RFC3339, task.RunStartedAt); err == nil {
fmt.Printf(" Started: %s\n", startedTime.Format("2006-01-02 15:04:05"))
}
if updatedTime, err := time.Parse(time.RFC3339, task.UpdatedAt); err == nil {
fmt.Printf(" Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
} }
} }
return nil return nil
} }
func showJobLog(client *api.Client, owner, name string, runNumber int64, task ActionTask) error { 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 // 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", logURL := fmt.Sprintf("https://%s/%s/%s/actions/runs/%d/jobs/%d/logs",
client.Hostname(), owner, name, runNumber, task.ID) client.Hostname(), owner, name, runNumber, task.ID)
@ -410,6 +440,14 @@ func showJobLog(client *api.Client, owner, name string, runNumber int64, task Ac
return err return err
} }
// If --log-failed, filter to only show failed steps
// For now, just show all logs (filtering failed steps would require parsing the log format)
if logFailed {
// TODO: Implement filtering for failed steps only
// This would require parsing the log format and identifying failed step markers
fmt.Println("Note: --log-failed filtering not yet implemented, showing all logs")
}
fmt.Print(logContent) fmt.Print(logContent)
fmt.Println() fmt.Println()

View file

@ -84,3 +84,39 @@ func (c *Client) GetJSON(path string, result any) error {
return nil return nil
} }
// GetRawLog performs a GET request and returns the raw response body as string
func (c *Client) GetRawLog(url string) (string, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// Set authentication header
if c.token != "" {
req.Header.Set("Authorization", "token "+c.token)
}
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to perform request: %w", err)
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("failed to close response body: %w", closeErr)
}
}()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
return string(bodyBytes), nil
}