feat: actions support
This commit is contained in:
parent
ca17526594
commit
8db012cb7a
2 changed files with 127 additions and 53 deletions
144
cmd/actions.go
144
cmd/actions.go
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue