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)
runListCmd.Flags().IntP("limit", "L", 20, "Maximum number of runs to list")
addRepoFlags(runViewCmd)
runViewCmd.Flags().BoolP("verbose", "v", false, "Show job details")
runViewCmd.Flags().BoolP("log", "", false, "Show logs for all jobs")
runViewCmd.Flags().Int64P("job", "j", 0, "Show logs for a specific job ID")
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")
// Add flags for secret commands
addRepoFlags(actionsSecretListCmd)
@ -294,7 +295,17 @@ func runRunView(cmd *cobra.Command, args []string) error {
verbose, _ := cmd.Flags().GetBool("verbose")
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
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"))
}
// If --log or --job flag is provided, or --verbose, fetch jobs
if verbose || showLog || jobID > 0 {
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)
}
// Fetch jobs if needed for verbose, log, or job-specific views
needsJobs := verbose || showLog || showLogFailed || jobID > 0
if !needsJobs {
return nil
}
// Filter tasks for this run number
var runTasks []ActionTask
for _, task := range taskList.WorkflowRuns {
if task.RunNumber == run.IndexInRepo {
runTasks = append(runTasks, task)
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)
}
// 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 len(runTasks) == 0 {
fmt.Println("\nNo jobs found for this run")
return nil
if !found {
return fmt.Errorf("job %d not found in this run", jobID)
}
}
// Show jobs if verbose (and not showing logs)
if verbose && !showLog && jobID == 0 {
fmt.Println("\nJobs:")
for _, task := range runTasks {
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 {
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"))
}
// Case 1: --verbose (show job steps/details without logs)
if verbose && !showLog && !showLogFailed {
fmt.Println("\nJobs:")
for _, task := range runTasks {
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 {
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
}
// Show logs for specific job or all jobs
if showLog || jobID > 0 {
tasksToShow := runTasks
if jobID > 0 {
// Filter to specific job
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)
}
// Case 2: --log or --log-failed (show logs)
if showLog || showLogFailed {
for _, task := range runTasks {
if err := showJobLog(client, owner, name, run.IndexInRepo, task, showLogFailed); err != nil {
fmt.Printf("\nError fetching log for job %s: %v\n", task.Name, err)
}
}
return nil
}
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)
}
}
// 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
}
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
logURL := fmt.Sprintf("https://%s/%s/%s/actions/runs/%d/jobs/%d/logs",
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
}
// 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.Println()

View file

@ -84,3 +84,39 @@ func (c *Client) GetJSON(path string, result any) error {
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
}