mirror of
https://gitea.com/gitea/tea.git
synced 2026-09-10 07:26:33 -04:00
feat(actions): add structured run output
Signed-off-by: Noel <n@noeljackson.com>
This commit is contained in:
parent
f34697c5ed
commit
7246c8ec1e
|
|
@ -42,6 +42,10 @@ var CmdRunsList = cli.Command{
|
|||
Name: "actor",
|
||||
Usage: "Filter by actor username (who triggered the run)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "head-sha",
|
||||
Usage: "Filter by head commit SHA",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "since",
|
||||
Usage: "Show runs started after this time (e.g., '24h', '2024-01-01')",
|
||||
|
|
@ -106,13 +110,7 @@ func RunRunsList(ctx stdctx.Context, cmd *cli.Command) error {
|
|||
// Build list options
|
||||
listOpts := flags.GetListOptions(cmd)
|
||||
|
||||
runs, _, err := client.Actions.ListRepoRuns(ctx, c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{
|
||||
ListOptions: listOpts,
|
||||
Status: cmd.String("status"),
|
||||
Branch: cmd.String("branch"),
|
||||
Event: cmd.String("event"),
|
||||
Actor: cmd.String("actor"),
|
||||
})
|
||||
runs, _, err := client.Actions.ListRepoRuns(ctx, c.Owner, c.Repo, listRepoActionRunsOptions(cmd, listOpts))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -127,6 +125,17 @@ func RunRunsList(ctx stdctx.Context, cmd *cli.Command) error {
|
|||
return print.ActionRunsList(filteredRuns, c.Output)
|
||||
}
|
||||
|
||||
func listRepoActionRunsOptions(cmd *cli.Command, listOpts gitea.ListOptions) gitea.ListRepoActionRunsOptions {
|
||||
return gitea.ListRepoActionRunsOptions{
|
||||
ListOptions: listOpts,
|
||||
Status: cmd.String("status"),
|
||||
Branch: cmd.String("branch"),
|
||||
Event: cmd.String("event"),
|
||||
Actor: cmd.String("actor"),
|
||||
HeadSHA: cmd.String("head-sha"),
|
||||
}
|
||||
}
|
||||
|
||||
// filterRunsByTime filters runs based on time range
|
||||
func filterRunsByTime(runs []*gitea.ActionWorkflowRun, since, until time.Time) []*gitea.ActionWorkflowRun {
|
||||
if since.IsZero() && until.IsZero() {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,31 @@ import (
|
|||
"gitea.dev/tea/modules/config"
|
||||
)
|
||||
|
||||
func TestRunRunsListPassesHeadSHAFilter(t *testing.T) {
|
||||
const headSHA = "0123456789abcdef"
|
||||
cmd := &cli.Command{Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "status"},
|
||||
&cli.StringFlag{Name: "branch"},
|
||||
&cli.StringFlag{Name: "event"},
|
||||
&cli.StringFlag{Name: "actor"},
|
||||
&cli.StringFlag{Name: "head-sha"},
|
||||
}}
|
||||
require.NoError(t, cmd.Set("head-sha", headSHA))
|
||||
options := listRepoActionRunsOptions(cmd, gitea.ListOptions{Page: 2, PageSize: 50})
|
||||
require.Equal(t, headSHA, options.HeadSHA)
|
||||
require.Equal(t, 2, options.Page)
|
||||
require.Equal(t, 50, options.PageSize)
|
||||
|
||||
var found bool
|
||||
for _, flag := range CmdRunsList.Flags {
|
||||
if flag.Names()[0] == "head-sha" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found)
|
||||
}
|
||||
|
||||
func TestFilterRunsByTime(t *testing.T) {
|
||||
now := time.Now()
|
||||
runs := []*gitea.ActionWorkflowRun{
|
||||
|
|
|
|||
|
|
@ -58,10 +58,7 @@ func runRunsView(ctx stdctx.Context, cmd *cli.Command) error {
|
|||
return fmt.Errorf("failed to get run: %w", err)
|
||||
}
|
||||
|
||||
// Print run details
|
||||
print.ActionRunDetails(run)
|
||||
|
||||
// Fetch and print jobs if requested
|
||||
var runJobs []*gitea.ActionWorkflowJob
|
||||
if cmd.Bool("jobs") {
|
||||
jobs, _, err := client.Actions.ListRepoJobsByRun(ctx, c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{
|
||||
ListOptions: flags.GetListOptions(cmd),
|
||||
|
|
@ -70,13 +67,10 @@ func runRunsView(ctx stdctx.Context, cmd *cli.Command) error {
|
|||
return fmt.Errorf("failed to get jobs: %w", err)
|
||||
}
|
||||
|
||||
if jobs != nil && len(jobs.Jobs) > 0 {
|
||||
fmt.Printf("\nJobs:\n\n")
|
||||
if err := print.ActionWorkflowJobsList(jobs.Jobs, c.Output); err != nil {
|
||||
return err
|
||||
}
|
||||
if jobs != nil {
|
||||
runJobs = jobs.Jobs
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return print.ActionRunView(run, runJobs, c.Output)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1575,6 +1575,8 @@ List workflow runs
|
|||
|
||||
**--event**="": Filter by event type (push, pull_request, etc.)
|
||||
|
||||
**--head-sha**="": Filter by head commit SHA
|
||||
|
||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@
|
|||
package print
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// formatDurationMinutes formats duration in a human-readable way
|
||||
|
|
@ -22,6 +26,9 @@ func formatDurationMinutes(started, completed time.Time) string {
|
|||
}
|
||||
|
||||
duration := end.Sub(started)
|
||||
if duration < 0 {
|
||||
return ""
|
||||
}
|
||||
if duration < time.Minute {
|
||||
return fmt.Sprintf("%ds", int(duration.Seconds()))
|
||||
}
|
||||
|
|
@ -33,6 +40,16 @@ func formatDurationMinutes(started, completed time.Time) string {
|
|||
return fmt.Sprintf("%dh%dm", hours, minutes)
|
||||
}
|
||||
|
||||
// validCompletion returns a completion timestamp only when it can represent a
|
||||
// real completion for the supplied start time. Some Gitea versions serialize
|
||||
// an absent completion as the Unix epoch.
|
||||
func validCompletion(started, completed time.Time) time.Time {
|
||||
if completed.IsZero() || (!started.IsZero() && completed.Before(started)) {
|
||||
return time.Time{}
|
||||
}
|
||||
return completed
|
||||
}
|
||||
|
||||
// getWorkflowDisplayName returns the display title or falls back to path
|
||||
func getWorkflowDisplayName(run *gitea.ActionWorkflowRun) string {
|
||||
if run.DisplayTitle != "" {
|
||||
|
|
@ -43,36 +60,63 @@ func getWorkflowDisplayName(run *gitea.ActionWorkflowRun) string {
|
|||
|
||||
// ActionRunsList prints a list of workflow runs
|
||||
func ActionRunsList(runs []*gitea.ActionWorkflowRun, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
machineReadable := isMachineReadable(output)
|
||||
var headers []string
|
||||
if machineReadable {
|
||||
headers = []string{
|
||||
"ID",
|
||||
"Status",
|
||||
"Conclusion",
|
||||
"Workflow",
|
||||
"Path",
|
||||
"Branch",
|
||||
"Event",
|
||||
"Started",
|
||||
"HeadSHA",
|
||||
"StartedAt",
|
||||
"CompletedAt",
|
||||
"Duration",
|
||||
},
|
||||
}
|
||||
} else {
|
||||
headers = []string{"ID", "Status", "Workflow", "Branch", "Event", "Started", "Duration"}
|
||||
}
|
||||
|
||||
machineReadable := isMachineReadable(output)
|
||||
t := table{headers: headers}
|
||||
|
||||
for _, run := range runs {
|
||||
workflowName := getWorkflowDisplayName(run)
|
||||
duration := formatDurationMinutes(run.StartedAt, run.CompletedAt)
|
||||
completedAt := validCompletion(run.StartedAt, run.CompletedAt)
|
||||
duration := formatDurationMinutes(run.StartedAt, completedAt)
|
||||
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", run.ID),
|
||||
run.Status,
|
||||
workflowName,
|
||||
run.HeadBranch,
|
||||
run.Event,
|
||||
FormatTime(run.StartedAt, machineReadable),
|
||||
duration,
|
||||
)
|
||||
if machineReadable {
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", run.ID),
|
||||
run.Status,
|
||||
run.Conclusion,
|
||||
workflowName,
|
||||
run.Path,
|
||||
run.HeadBranch,
|
||||
run.Event,
|
||||
run.HeadSha,
|
||||
FormatTime(run.StartedAt, true),
|
||||
FormatTime(completedAt, true),
|
||||
duration,
|
||||
)
|
||||
} else {
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", run.ID),
|
||||
run.Status,
|
||||
workflowName,
|
||||
run.HeadBranch,
|
||||
run.Event,
|
||||
FormatTime(run.StartedAt, false),
|
||||
duration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(runs) == 0 {
|
||||
if machineReadable {
|
||||
return t.print(output)
|
||||
}
|
||||
fmt.Printf("No workflow runs found\n")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -97,9 +141,10 @@ func ActionRunDetails(run *gitea.ActionWorkflowRun) {
|
|||
fmt.Printf("Event: %s\n", run.Event)
|
||||
fmt.Printf("Head SHA: %s\n", run.HeadSha)
|
||||
fmt.Printf("Started: %s\n", FormatTime(run.StartedAt, false))
|
||||
if !run.CompletedAt.IsZero() {
|
||||
fmt.Printf("Completed: %s\n", FormatTime(run.CompletedAt, false))
|
||||
duration := formatDurationMinutes(run.StartedAt, run.CompletedAt)
|
||||
completedAt := validCompletion(run.StartedAt, run.CompletedAt)
|
||||
if !completedAt.IsZero() {
|
||||
fmt.Printf("Completed: %s\n", FormatTime(completedAt, false))
|
||||
duration := formatDurationMinutes(run.StartedAt, completedAt)
|
||||
fmt.Printf("Duration: %s\n", duration)
|
||||
}
|
||||
if run.RunAttempt > 1 {
|
||||
|
|
@ -113,39 +158,182 @@ func ActionRunDetails(run *gitea.ActionWorkflowRun) {
|
|||
}
|
||||
}
|
||||
|
||||
type actionRunOutput struct {
|
||||
ID int64 `json:"id" yaml:"id"`
|
||||
RunNumber int64 `json:"run_number" yaml:"run_number"`
|
||||
Status string `json:"status" yaml:"status"`
|
||||
Conclusion string `json:"conclusion" yaml:"conclusion"`
|
||||
Workflow string `json:"workflow" yaml:"workflow"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Branch string `json:"branch" yaml:"branch"`
|
||||
Event string `json:"event" yaml:"event"`
|
||||
HeadSHA string `json:"head_sha" yaml:"head_sha"`
|
||||
StartedAt string `json:"started_at" yaml:"started_at"`
|
||||
CompletedAt string `json:"completed_at" yaml:"completed_at"`
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Attempt int64 `json:"attempt" yaml:"attempt"`
|
||||
Actor string `json:"actor" yaml:"actor"`
|
||||
URL string `json:"url" yaml:"url"`
|
||||
}
|
||||
|
||||
type actionJobOutput struct {
|
||||
ID int64 `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Status string `json:"status" yaml:"status"`
|
||||
Conclusion string `json:"conclusion" yaml:"conclusion"`
|
||||
Runner string `json:"runner" yaml:"runner"`
|
||||
HeadSHA string `json:"head_sha" yaml:"head_sha"`
|
||||
StartedAt string `json:"started_at" yaml:"started_at"`
|
||||
CompletedAt string `json:"completed_at" yaml:"completed_at"`
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
}
|
||||
|
||||
type actionRunViewOutput struct {
|
||||
Run actionRunOutput `json:"run" yaml:"run"`
|
||||
Jobs []actionJobOutput `json:"jobs" yaml:"jobs"`
|
||||
}
|
||||
|
||||
func newActionRunOutput(run *gitea.ActionWorkflowRun) actionRunOutput {
|
||||
completedAt := validCompletion(run.StartedAt, run.CompletedAt)
|
||||
actor := ""
|
||||
if run.Actor != nil {
|
||||
actor = run.Actor.UserName
|
||||
}
|
||||
return actionRunOutput{
|
||||
ID: run.ID,
|
||||
RunNumber: run.RunNumber,
|
||||
Status: run.Status,
|
||||
Conclusion: run.Conclusion,
|
||||
Workflow: getWorkflowDisplayName(run),
|
||||
Path: run.Path,
|
||||
Branch: run.HeadBranch,
|
||||
Event: run.Event,
|
||||
HeadSHA: run.HeadSha,
|
||||
StartedAt: FormatTime(run.StartedAt, true),
|
||||
CompletedAt: FormatTime(completedAt, true),
|
||||
Duration: formatDurationMinutes(run.StartedAt, completedAt),
|
||||
Attempt: run.RunAttempt,
|
||||
Actor: actor,
|
||||
URL: run.HTMLURL,
|
||||
}
|
||||
}
|
||||
|
||||
func newActionJobOutput(job *gitea.ActionWorkflowJob) actionJobOutput {
|
||||
completedAt := validCompletion(job.StartedAt, job.CompletedAt)
|
||||
runner := job.RunnerName
|
||||
if runner == "" {
|
||||
runner = "-"
|
||||
}
|
||||
return actionJobOutput{
|
||||
ID: job.ID,
|
||||
Name: job.Name,
|
||||
Status: job.Status,
|
||||
Conclusion: job.Conclusion,
|
||||
Runner: runner,
|
||||
HeadSHA: job.HeadSha,
|
||||
StartedAt: FormatTime(job.StartedAt, true),
|
||||
CompletedAt: FormatTime(completedAt, true),
|
||||
Duration: formatDurationMinutes(job.StartedAt, completedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ActionRunView prints a workflow run and its jobs as a single document for
|
||||
// machine-readable output formats.
|
||||
func ActionRunView(run *gitea.ActionWorkflowRun, jobs []*gitea.ActionWorkflowJob, output string) error {
|
||||
switch output {
|
||||
case "json", "yaml", "yml":
|
||||
return fprintActionRunView(os.Stdout, run, jobs, output)
|
||||
case "csv", "tsv":
|
||||
return fmt.Errorf("workflow run view does not support %s output; use json or yaml for structured output", output)
|
||||
default:
|
||||
ActionRunDetails(run)
|
||||
if len(jobs) > 0 {
|
||||
fmt.Printf("\nJobs:\n\n")
|
||||
return ActionWorkflowJobsList(jobs, output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func fprintActionRunView(w io.Writer, run *gitea.ActionWorkflowRun, jobs []*gitea.ActionWorkflowJob, output string) error {
|
||||
view := actionRunViewOutput{
|
||||
Run: newActionRunOutput(run),
|
||||
Jobs: make([]actionJobOutput, 0, len(jobs)),
|
||||
}
|
||||
for _, job := range jobs {
|
||||
view.Jobs = append(view.Jobs, newActionJobOutput(job))
|
||||
}
|
||||
|
||||
if output == "json" {
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(view)
|
||||
}
|
||||
encoder := yaml.NewEncoder(w)
|
||||
if err := encoder.Encode(view); err != nil {
|
||||
_ = encoder.Close()
|
||||
return err
|
||||
}
|
||||
return encoder.Close()
|
||||
}
|
||||
|
||||
// ActionWorkflowJobsList prints a list of workflow jobs
|
||||
func ActionWorkflowJobsList(jobs []*gitea.ActionWorkflowJob, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
machineReadable := isMachineReadable(output)
|
||||
var headers []string
|
||||
if machineReadable {
|
||||
headers = []string{
|
||||
"ID",
|
||||
"Name",
|
||||
"Status",
|
||||
"Conclusion",
|
||||
"Runner",
|
||||
"Started",
|
||||
"HeadSHA",
|
||||
"StartedAt",
|
||||
"CompletedAt",
|
||||
"Duration",
|
||||
},
|
||||
}
|
||||
} else {
|
||||
headers = []string{"ID", "Name", "Status", "Runner", "Started", "Duration"}
|
||||
}
|
||||
|
||||
machineReadable := isMachineReadable(output)
|
||||
t := table{headers: headers}
|
||||
|
||||
for _, job := range jobs {
|
||||
duration := formatDurationMinutes(job.StartedAt, job.CompletedAt)
|
||||
completedAt := validCompletion(job.StartedAt, job.CompletedAt)
|
||||
duration := formatDurationMinutes(job.StartedAt, completedAt)
|
||||
runner := job.RunnerName
|
||||
if runner == "" {
|
||||
runner = "-"
|
||||
}
|
||||
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", job.ID),
|
||||
job.Name,
|
||||
job.Status,
|
||||
runner,
|
||||
FormatTime(job.StartedAt, machineReadable),
|
||||
duration,
|
||||
)
|
||||
if machineReadable {
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", job.ID),
|
||||
job.Name,
|
||||
job.Status,
|
||||
job.Conclusion,
|
||||
runner,
|
||||
job.HeadSha,
|
||||
FormatTime(job.StartedAt, true),
|
||||
FormatTime(completedAt, true),
|
||||
duration,
|
||||
)
|
||||
} else {
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", job.ID),
|
||||
job.Name,
|
||||
job.Status,
|
||||
runner,
|
||||
FormatTime(job.StartedAt, false),
|
||||
duration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jobs) == 0 {
|
||||
if machineReadable {
|
||||
return t.print(output)
|
||||
}
|
||||
fmt.Printf("No jobs found\n")
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@
|
|||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestActionRunsListEmpty(t *testing.T) {
|
||||
|
|
@ -53,6 +57,58 @@ func TestActionRunsListWithData(t *testing.T) {
|
|||
require.NoError(t, ActionRunsList(runs, ""))
|
||||
}
|
||||
|
||||
func TestActionRunsListJSONHasStableFields(t *testing.T) {
|
||||
startedAt := time.Date(2026, time.August, 2, 10, 0, 0, 0, time.UTC)
|
||||
runs := []*gitea.ActionWorkflowRun{{
|
||||
ID: 7,
|
||||
Status: "in_progress",
|
||||
DisplayTitle: "CI",
|
||||
Path: ".gitea/workflows/ci.yml",
|
||||
HeadBranch: "main",
|
||||
HeadSha: "abc123",
|
||||
Event: "push",
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: time.Unix(0, 0).UTC(),
|
||||
}}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
require.NoError(t, ActionRunsList(runs, "json"))
|
||||
})
|
||||
var rows []map[string]string
|
||||
require.NoError(t, json.Unmarshal([]byte(out), &rows))
|
||||
require.Len(t, rows, 1)
|
||||
assert.Equal(t, "abc123", rows[0]["head_sha"])
|
||||
assert.Equal(t, ".gitea/workflows/ci.yml", rows[0]["path"])
|
||||
assert.Equal(t, "in_progress", rows[0]["status"])
|
||||
assert.Equal(t, "", rows[0]["conclusion"])
|
||||
assert.Equal(t, "2026-08-02T10:00:00Z", rows[0]["started_at"])
|
||||
assert.Equal(t, "", rows[0]["completed_at"])
|
||||
assert.NotContains(t, rows[0]["duration"], "-")
|
||||
}
|
||||
|
||||
func TestActionRunsListEmptyJSONIsValid(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
require.NoError(t, ActionRunsList(nil, "json"))
|
||||
})
|
||||
var rows []map[string]string
|
||||
require.NoError(t, json.Unmarshal([]byte(out), &rows))
|
||||
assert.Empty(t, rows)
|
||||
}
|
||||
|
||||
func TestActionRunsListHumanColumnsRemainCompatible(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
require.NoError(t, ActionRunsList([]*gitea.ActionWorkflowRun{{
|
||||
ID: 1,
|
||||
Status: "completed",
|
||||
Conclusion: "success",
|
||||
DisplayTitle: "CI",
|
||||
}}, "table"))
|
||||
})
|
||||
assert.Contains(t, out, "WORKFLOW")
|
||||
assert.NotContains(t, out, "CONCLUSION")
|
||||
assert.NotContains(t, out, "HEAD SHA")
|
||||
}
|
||||
|
||||
func TestActionRunDetails(t *testing.T) {
|
||||
run := &gitea.ActionWorkflowRun{
|
||||
ID: 123,
|
||||
|
|
@ -83,6 +139,96 @@ func TestActionRunDetails(t *testing.T) {
|
|||
ActionRunDetails(run)
|
||||
}
|
||||
|
||||
func TestActionRunViewMachineOutputIsSingleDocument(t *testing.T) {
|
||||
startedAt := time.Date(2026, time.August, 2, 10, 0, 0, 0, time.UTC)
|
||||
completedAt := startedAt.Add(5 * time.Minute)
|
||||
run := &gitea.ActionWorkflowRun{
|
||||
ID: 123,
|
||||
RunNumber: 42,
|
||||
Status: "completed",
|
||||
Conclusion: "failure",
|
||||
DisplayTitle: "CI",
|
||||
Path: ".gitea/workflows/ci.yml",
|
||||
HeadBranch: "main",
|
||||
HeadSha: "abc123",
|
||||
Event: "push",
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
RunAttempt: 2,
|
||||
}
|
||||
jobs := []*gitea.ActionWorkflowJob{{
|
||||
ID: 456,
|
||||
Name: "test",
|
||||
Status: "completed",
|
||||
Conclusion: "failure",
|
||||
HeadSha: "abc123",
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
RunnerName: "runner-1",
|
||||
}}
|
||||
|
||||
for _, output := range []string{"json", "yaml"} {
|
||||
t.Run(output, func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, fprintActionRunView(buf, run, jobs, output))
|
||||
|
||||
var document struct {
|
||||
Run struct {
|
||||
HeadSHA string `json:"head_sha" yaml:"head_sha"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Status string `json:"status" yaml:"status"`
|
||||
Conclusion string `json:"conclusion" yaml:"conclusion"`
|
||||
StartedAt string `json:"started_at" yaml:"started_at"`
|
||||
CompletedAt string `json:"completed_at" yaml:"completed_at"`
|
||||
} `json:"run" yaml:"run"`
|
||||
Jobs []struct {
|
||||
Conclusion string `json:"conclusion" yaml:"conclusion"`
|
||||
} `json:"jobs" yaml:"jobs"`
|
||||
}
|
||||
if output == "json" {
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &document))
|
||||
} else {
|
||||
require.NoError(t, yaml.Unmarshal(buf.Bytes(), &document))
|
||||
}
|
||||
assert.Equal(t, "abc123", document.Run.HeadSHA)
|
||||
assert.Equal(t, ".gitea/workflows/ci.yml", document.Run.Path)
|
||||
assert.Equal(t, "completed", document.Run.Status)
|
||||
assert.Equal(t, "failure", document.Run.Conclusion)
|
||||
assert.Equal(t, "2026-08-02T10:00:00Z", document.Run.StartedAt)
|
||||
assert.Equal(t, "2026-08-02T10:05:00Z", document.Run.CompletedAt)
|
||||
require.Len(t, document.Jobs, 1)
|
||||
assert.Equal(t, "failure", document.Jobs[0].Conclusion)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionRunViewNormalizesEpochCompletion(t *testing.T) {
|
||||
startedAt := time.Date(2026, time.August, 2, 10, 0, 0, 0, time.UTC)
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, fprintActionRunView(buf, &gitea.ActionWorkflowRun{
|
||||
ID: 1,
|
||||
Status: "in_progress",
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: time.Unix(0, 0).UTC(),
|
||||
}, nil, "json"))
|
||||
|
||||
var document actionRunViewOutput
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &document))
|
||||
assert.Empty(t, document.Run.CompletedAt)
|
||||
assert.NotContains(t, document.Run.Duration, "-")
|
||||
assert.Empty(t, document.Jobs)
|
||||
}
|
||||
|
||||
func TestActionRunViewRejectsFlatStructuredFormats(t *testing.T) {
|
||||
run := &gitea.ActionWorkflowRun{ID: 1}
|
||||
for _, output := range []string{"csv", "tsv"} {
|
||||
t.Run(output, func(t *testing.T) {
|
||||
err := ActionRunView(run, nil, output)
|
||||
require.ErrorContains(t, err, "use json or yaml")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionWorkflowJobsListEmpty(t *testing.T) {
|
||||
// Test with empty jobs - should not panic
|
||||
defer func() {
|
||||
|
|
@ -123,6 +269,20 @@ func TestActionWorkflowJobsListWithData(t *testing.T) {
|
|||
require.NoError(t, ActionWorkflowJobsList(jobs, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowJobsListHumanColumnsRemainCompatible(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
require.NoError(t, ActionWorkflowJobsList([]*gitea.ActionWorkflowJob{{
|
||||
ID: 1,
|
||||
Name: "test",
|
||||
Status: "completed",
|
||||
Conclusion: "success",
|
||||
}}, "table"))
|
||||
})
|
||||
assert.Contains(t, out, "RUNNER")
|
||||
assert.NotContains(t, out, "CONCLUSION")
|
||||
assert.NotContains(t, out, "HEAD SHA")
|
||||
}
|
||||
|
||||
func TestActionWorkflowsListEmpty(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
|
@ -243,6 +403,12 @@ func TestFormatDurationMinutes(t *testing.T) {
|
|||
completed: now,
|
||||
expected: "2h30m",
|
||||
},
|
||||
{
|
||||
name: "completion before start",
|
||||
started: now,
|
||||
completed: now.Add(-time.Hour),
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
|
|||
Loading…
Reference in a new issue