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,8 +341,12 @@ 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
if !needsJobs {
return nil
}
tasksEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, name) tasksEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, name)
var taskList ActionTaskList var taskList ActionTaskList
if err := client.GetJSON(tasksEndpoint, &taskList); err != nil { if err := client.GetJSON(tasksEndpoint, &taskList); err != nil {
@ -351,8 +366,23 @@ func runRunView(cmd *cobra.Command, args []string) error {
return nil return nil
} }
// Show jobs if verbose (and not showing logs) // If --job is specified, filter to that job
if verbose && !showLog && jobID == 0 { if jobID > 0 {
var found bool
for _, task := range runTasks {
if task.ID == jobID {
runTasks = []ActionTask{task}
found = true
break
}
}
if !found {
return fmt.Errorf("job %d not found in this run", jobID)
}
}
// Case 1: --verbose (show job steps/details without logs)
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)
@ -366,35 +396,35 @@ func runRunView(cmd *cobra.Command, args []string) error {
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
if jobID > 0 {
// Filter to specific job
tasksToShow = nil
for _, task := range runTasks { for _, task := range runTasks {
if task.ID == jobID { if err := showJobLog(client, owner, name, run.IndexInRepo, task, showLogFailed); err != nil {
tasksToShow = []ActionTask{task}
break
}
}
if len(tasksToShow) == 0 {
return fmt.Errorf("job %d not found in this run", jobID)
}
}
for _, task := range tasksToShow {
if err := showJobLog(client, owner, name, run.IndexInRepo, task); err != nil {
fmt.Printf("\nError fetching log for job %s: %v\n", task.Name, err) fmt.Printf("\nError fetching log for job %s: %v\n", task.Name, err)
} }
} }
return nil
}
// Case 3: --job without --log or --verbose (show job details only)
if jobID > 0 {
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
}