mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
feat(run): show runs like gh, watch them live, and download artifacts
run list prints a table with a status icon, the workflow name, title, branch, event, elapsed time and age, and the total from the server; runs carry workflow, workflow_path and run_number in JSON. run view shows the run's state and trigger, its jobs with their steps and durations, the next steps and the run's URL; --json includes the jobs, --log prints their logs and --web opens the run. Run commands accept #N and run URLs, and ask for a recent run in a terminal when the argument is left out. run watch redraws the run on the alternate screen, then prints it with a summary; an expired --timeout names the run and suspects a missing runner when no job started. run cancel no longer asks for confirmation, rerun refuses unfinished runs up front, reruns a single job with --job and reports the new attempt, and delete names the run in its prompt. run logs follows several jobs with --follow, prefixing lines with the job's name, selects jobs by name or with --failed, and downloads the archive with --output or --dir. run download extracts the artifacts of a run by name, and artifact list shows sizes and expiry. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
19 files changed
+2636
-646
No files matched your search
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,8 +22,22 @@ import (
|
||||
// Job identifies one workflow job without exposing server-specific metadata. StartedAt and
|
||||
// CompletedAt are nil until the job starts and completes.
|
||||
type Job struct {
|
||||
RunAttempt int64 `json:"run_attempt"`
|
||||
ID int64 `json:"id"`
|
||||
RunAttempt int64 `json:"run_attempt"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion"`
|
||||
URL string `json:"url"`
|
||||
Labels []string `json:"labels"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
StartedAt *string `json:"started_at"`
|
||||
CompletedAt *string `json:"completed_at"`
|
||||
Steps []Step `json:"steps"`
|
||||
}
|
||||
|
||||
// Step is one step of a job. StartedAt and CompletedAt are nil until the step starts and completes.
|
||||
type Step struct {
|
||||
Number int64 `json:"number"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion"`
|
||||
@@ -30,14 +45,44 @@ type Job struct {
|
||||
CompletedAt *string `json:"completed_at"`
|
||||
}
|
||||
|
||||
// Jobs lists every page of jobs in the selected run.
|
||||
// Completed reports whether the job finished, whatever its conclusion.
|
||||
func (j Job) Completed() bool { return completedActionStatus(j.Status) }
|
||||
|
||||
// Jobs lists every job in the selected run.
|
||||
func (s Actions) Jobs(ctx context.Context, run int64) ([]Job, error) {
|
||||
return listAll(func(page int) ([]Job, error) { return s.jobsPage(ctx, run, 100, page) })
|
||||
jobs := []Job{}
|
||||
for page := 1; page <= maxListPages; page++ {
|
||||
window, err := s.JobsPage(ctx, run, 100, page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs = append(jobs, window.Items...)
|
||||
if !window.More || len(window.Items) == 0 {
|
||||
return jobs, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("listing exceeded %d pages", maxListPages)
|
||||
}
|
||||
|
||||
// JobsPage lists a page of jobs in the selected run.
|
||||
func (s Actions) JobsPage(ctx context.Context, run int64, limit, page int) ([]Job, error) {
|
||||
return s.jobsPage(ctx, run, limit, page)
|
||||
func (s Actions) JobsPage(ctx context.Context, run int64, limit, page int) (Page[Job], error) {
|
||||
window, err := listPage(limit, page, func(limit, page int) ([]*gitea.ActionsWorkflowJob, http.Header, error) {
|
||||
response, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.ActionsWorkflowJobsResponse, *gitea.Response, error) {
|
||||
owner, repo, err := splitRepository(s.Repository)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return c.Actions.ListRepoJobsByRun(ctx, owner, repo, run, gitea.ListRepoActionsJobsOptions{ListOptions: listOptions(limit, page)})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, headerOf(resp), err
|
||||
}
|
||||
return response.Jobs, headerOf(resp), nil
|
||||
})
|
||||
if err != nil {
|
||||
return Page[Job]{}, err
|
||||
}
|
||||
return Page[Job]{Items: convertAll(window.Items, jobFrom), More: window.More, Total: window.Total, Counted: window.Counted}, nil
|
||||
}
|
||||
|
||||
// WaitForFirstJob polls a freshly dispatched run until the server has created its first job.
|
||||
@@ -59,20 +104,18 @@ func (s Actions) WaitForFirstJob(ctx context.Context, run int64, interval time.D
|
||||
}
|
||||
}
|
||||
|
||||
func (s Actions) jobsPage(ctx context.Context, run int64, limit, page int) ([]Job, error) {
|
||||
response, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.ActionsWorkflowJobsResponse, *gitea.Response, error) {
|
||||
return c.Actions.ListRepoJobsByRun(ctx, owner, repo, run, gitea.ListRepoActionsJobsOptions{ListOptions: listOptions(limit, page)})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertAll(response.Jobs, jobFrom), nil
|
||||
}
|
||||
|
||||
func jobFrom(job *gitea.ActionsWorkflowJob) Job {
|
||||
return Job{
|
||||
RunAttempt: job.RunAttempt, ID: job.ID, Name: job.Name, Status: job.Status, Conclusion: job.Conclusion,
|
||||
StartedAt: actionTime(job.StartedAt), CompletedAt: actionTime(job.CompletedAt),
|
||||
URL: job.HTMLURL, Labels: append([]string{}, job.Labels...), CreatedAt: timestamp(job.CreatedAt),
|
||||
StartedAt: actionTime(job.StartedAt), CompletedAt: actionTime(job.CompletedAt), Steps: convertAll(job.Steps, stepFrom),
|
||||
}
|
||||
}
|
||||
|
||||
func stepFrom(step *gitea.ActionsWorkflowStep) Step {
|
||||
return Step{
|
||||
Number: step.Number, Name: step.Name, Status: step.Status, Conclusion: step.Conclusion,
|
||||
StartedAt: actionTime(step.StartedAt), CompletedAt: actionTime(step.CompletedAt),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,9 +230,11 @@ func (s Actions) FollowJobLogs(ctx context.Context, run, id int64, interval time
|
||||
}
|
||||
}
|
||||
|
||||
// RunLogsArchive streams the server's ZIP archive without extracting its entries.
|
||||
// RunLogsArchive streams the server's ZIP archive without extracting its entries. A run without
|
||||
// logs, such as one that never started, is reported as not found.
|
||||
func (s Actions) RunLogsArchive(ctx context.Context, run int64) (io.ReadCloser, error) {
|
||||
return repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (io.ReadCloser, *gitea.Response, error) {
|
||||
body, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (io.ReadCloser, *gitea.Response, error) {
|
||||
return c.Actions.GetRepoRunLogsReader(ctx, owner, repo, run)
|
||||
})
|
||||
return body, Classify(err, "logs of run", strconv.FormatInt(run, 10), s.Repository)
|
||||
}
|
||||
Reference in new issue
Block a user