Fix looking up workflow name for workflow runs (#6266)

The API field `WorkflowRun.name` is not guaranteed to correspond to the workflow name anymore. This introduces additional API lookups that resolve Workflows by their ID and look up their name in a future-proof fashion.

It also adds two new JSON fields for export: `displayTitle` and `workflowName`.

Co-authored-by: Christina Guo <61271066+guo-chris@users.noreply.github.com>
Co-authored-by: Mislav Marohnić <mislav@github.com>
This commit is contained in:
Isaac Shalom 2022-09-21 14:41:19 -04:00 committed by GitHub
parent 113acf9245
commit f1be4dc51c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 643 additions and 255 deletions

View file

@ -8,6 +8,7 @@ import (
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/run/shared"
workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -82,8 +83,8 @@ func TestNewCmdCancel(t *testing.T) {
}
func TestRunCancel(t *testing.T) {
inProgressRun := shared.TestRun("more runs", 1234, shared.InProgress, "")
completedRun := shared.TestRun("more runs", 4567, shared.Completed, shared.Failure)
inProgressRun := shared.TestRun(1234, shared.InProgress, "")
completedRun := shared.TestRun(4567, shared.Completed, shared.Failure)
tests := []struct {
name string
httpStubs func(*httpmock.Registry)
@ -103,6 +104,9 @@ func TestRunCancel(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(inProgressRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/cancel"),
httpmock.StatusStringResponse(202, "{}"))
@ -133,6 +137,9 @@ func TestRunCancel(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/4567"),
httpmock.JSONResponse(completedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/4567/cancel"),
httpmock.StatusStringResponse(409, ""),
@ -154,6 +161,13 @@ func TestRunCancel(t *testing.T) {
completedRun,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
},
},
{
@ -169,6 +183,13 @@ func TestRunCancel(t *testing.T) {
inProgressRun,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/cancel"),
httpmock.StatusStringResponse(202, "{}"))

View file

@ -84,9 +84,6 @@ func listRun(opts *ListOptions) error {
}
client := api.NewClientFromHTTP(c)
var runs []shared.Run
var workflow *workflowShared.Workflow
filters := &shared.FilterOptions{
Branch: opts.Branch,
Actor: opts.Actor,
@ -95,18 +92,19 @@ func listRun(opts *ListOptions) error {
opts.IO.StartProgressIndicator()
if opts.WorkflowSelector != "" {
states := []workflowShared.WorkflowState{workflowShared.Active}
workflow, err = workflowShared.ResolveWorkflow(
opts.IO, client, baseRepo, false, opts.WorkflowSelector, states)
if err == nil {
runs, err = shared.GetRunsByWorkflow(client, baseRepo, filters, opts.Limit, workflow.ID)
if workflow, err := workflowShared.ResolveWorkflow(opts.IO, client, baseRepo, false, opts.WorkflowSelector, states); err == nil {
filters.WorkflowID = workflow.ID
filters.WorkflowName = workflow.Name
} else {
return err
}
} else {
runs, err = shared.GetRuns(client, baseRepo, filters, opts.Limit)
}
runsResult, err := shared.GetRuns(client, baseRepo, filters, opts.Limit)
opts.IO.StopProgressIndicator()
if err != nil {
return fmt.Errorf("failed to get runs: %w", err)
}
runs := runsResult.WorkflowRuns
if err := opts.IO.StartPager(); err == nil {
defer opts.IO.StopPager()
@ -128,7 +126,7 @@ func listRun(opts *ListOptions) error {
if tp.IsTTY() {
tp.AddField("STATUS", nil, nil)
tp.AddField("NAME", nil, nil)
tp.AddField("TITLE", nil, nil)
tp.AddField("WORKFLOW", nil, nil)
tp.AddField("BRANCH", nil, nil)
tp.AddField("EVENT", nil, nil)
@ -147,9 +145,9 @@ func listRun(opts *ListOptions) error {
tp.AddField(string(run.Conclusion), nil, nil)
}
tp.AddField(run.CommitMsg(), nil, cs.Bold)
tp.AddField(run.Title(), nil, cs.Bold)
tp.AddField(run.Name, nil, nil)
tp.AddField(run.WorkflowName(), nil, nil)
tp.AddField(run.HeadBranch, nil, cs.Bold)
tp.AddField(string(run.Event), nil, nil)
tp.AddField(fmt.Sprintf("%d", run.ID), nil, cs.Cyan)

File diff suppressed because one or more lines are too long

View file

@ -9,6 +9,7 @@ import (
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/run/shared"
workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -180,6 +181,13 @@ func TestRerun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"),
httpmock.StringResponse("{}"))
@ -197,6 +205,13 @@ func TestRerun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"),
httpmock.StringResponse("{}"))
@ -230,6 +245,13 @@ func TestRerun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"),
httpmock.StringResponse("{}"))
@ -249,6 +271,13 @@ func TestRerun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"),
httpmock.StringResponse("{}"))
@ -289,6 +318,20 @@ func TestRerun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"),
httpmock.StringResponse("{}"))
@ -311,8 +354,15 @@ func TestRerun(t *testing.T) {
httpmock.JSONResponse(shared.RunsPayload{
WorkflowRuns: []shared.Run{
shared.SuccessfulRun,
shared.TestRun("in progress", 2, shared.InProgress, ""),
shared.TestRun(2, shared.InProgress, ""),
}}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
},
wantErr: true,
errOut: "no recent runs have failed; please specify a specific `<run-id>`",
@ -327,6 +377,13 @@ func TestRerun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
httpmock.JSONResponse(shared.SuccessfulRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/3/rerun"),
httpmock.StatusStringResponse(403, "no"))

View file

@ -9,7 +9,7 @@ import (
func RenderRunHeader(cs *iostreams.ColorScheme, run Run, ago, prNumber string) string {
title := fmt.Sprintf("%s %s%s",
cs.Bold(run.HeadBranch), run.Name, prNumber)
cs.Bold(run.HeadBranch), run.WorkflowName(), prNumber)
symbol, symbolColor := Symbol(cs, run.Status, run.Conclusion)
id := cs.Cyanf("%d", run.ID)

View file

@ -12,6 +12,7 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
)
@ -45,6 +46,7 @@ type Level string
var RunFields = []string{
"name",
"displayTitle",
"headBranch",
"headSha",
"createdAt",
@ -55,11 +57,13 @@ var RunFields = []string{
"event",
"databaseId",
"workflowDatabaseId",
"workflowName",
"url",
}
type Run struct {
Name string
Name string `json:"name"` // the semantics of this field are unclear
DisplayTitle string `json:"display_title"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
StartedAt time.Time `json:"run_started_at"`
@ -67,6 +71,7 @@ type Run struct {
Conclusion Conclusion
Event string
ID int64
workflowName string // cache column
WorkflowID int64 `json:"workflow_id"`
Attempts uint8 `json:"run_attempt"`
HeadBranch string `json:"head_branch"`
@ -107,7 +112,12 @@ type Commit struct {
Message string
}
func (r Run) CommitMsg() string {
// Title is the display title for a run, falling back to the commit subject if unavailable
func (r Run) Title() string {
if r.DisplayTitle != "" {
return r.DisplayTitle
}
commitLines := strings.Split(r.HeadCommit.Message, "\n")
if len(commitLines) > 0 {
return commitLines[0]
@ -116,6 +126,12 @@ func (r Run) CommitMsg() string {
}
}
// WorkflowName returns the human-readable name of the workflow that this run belongs to.
// TODO: consider lazy-loading the underlying API data to avoid extra API calls unless necessary
func (r Run) WorkflowName() string {
return r.workflowName
}
func (r *Run) ExportData(fields []string) map[string]interface{} {
v := reflect.ValueOf(r).Elem()
fieldByName := func(v reflect.Value, field string) reflect.Value {
@ -131,6 +147,8 @@ func (r *Run) ExportData(fields []string) map[string]interface{} {
data[f] = r.ID
case "workflowDatabaseId":
data[f] = r.WorkflowID
case "workflowName":
data[f] = r.WorkflowName()
default:
sf := fieldByName(v, f)
data[f] = sf.Interface()
@ -228,18 +246,22 @@ type RunsPayload struct {
}
type FilterOptions struct {
Branch string
Actor string
Branch string
Actor string
WorkflowID int64
// avoid loading workflow name separately and use the provided one
WorkflowName string
}
// GetRunsWithFilter fetches 50 runs from the API and filters them in-memory
func GetRunsWithFilter(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int, f func(Run) bool) ([]Run, error) {
path := fmt.Sprintf("repos/%s/actions/runs", ghrepo.FullName(repo))
runs, err := getRuns(client, repo, path, opts, 50)
runs, err := GetRuns(client, repo, opts, 50)
if err != nil {
return nil, err
}
filtered := []Run{}
for _, run := range runs {
var filtered []Run
for _, run := range runs.WorkflowRuns {
if f(run) {
filtered = append(filtered, run)
}
@ -251,70 +273,89 @@ func GetRunsWithFilter(client *api.Client, repo ghrepo.Interface, opts *FilterOp
return filtered, nil
}
func GetRunsByWorkflow(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int, workflowID int64) ([]Run, error) {
path := fmt.Sprintf("repos/%s/actions/workflows/%d/runs", ghrepo.FullName(repo), workflowID)
return getRuns(client, repo, path, opts, limit)
}
func GetRuns(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int) ([]Run, error) {
func GetRuns(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int) (*RunsPayload, error) {
path := fmt.Sprintf("repos/%s/actions/runs", ghrepo.FullName(repo))
return getRuns(client, repo, path, opts, limit)
}
if opts != nil && opts.WorkflowID > 0 {
path = fmt.Sprintf("repos/%s/actions/workflows/%d/runs", ghrepo.FullName(repo), opts.WorkflowID)
}
func getRuns(client *api.Client, repo ghrepo.Interface, path string, opts *FilterOptions, limit int) ([]Run, error) {
perPage := limit
page := 1
if limit > 100 {
perPage = 100
}
path += fmt.Sprintf("?per_page=%d", perPage)
runs := []Run{}
for len(runs) < limit {
var result RunsPayload
parsed, err := url.Parse(path)
if err != nil {
return nil, err
if opts != nil {
if opts.Branch != "" {
path += fmt.Sprintf("&branch=%s", url.QueryEscape(opts.Branch))
}
query := parsed.Query()
query.Set("per_page", fmt.Sprintf("%d", perPage))
query.Set("page", fmt.Sprintf("%d", page))
if opts != nil {
if opts.Branch != "" {
query.Set("branch", opts.Branch)
}
if opts.Actor != "" {
query.Set("actor", opts.Actor)
}
if opts.Actor != "" {
path += fmt.Sprintf("&actor=%s", url.QueryEscape(opts.Actor))
}
parsed.RawQuery = query.Encode()
pagedPath := parsed.String()
err = client.REST(repo.RepoHost(), "GET", pagedPath, nil, &result)
if err != nil {
return nil, err
}
if len(result.WorkflowRuns) == 0 {
break
}
for _, run := range result.WorkflowRuns {
runs = append(runs, run)
if len(runs) == limit {
break
}
}
if len(result.WorkflowRuns) < perPage {
break
}
page++
}
return runs, nil
var result *RunsPayload
pagination:
for path != "" {
var response RunsPayload
var err error
path, err = client.RESTWithNext(repo.RepoHost(), "GET", path, nil, &response)
if err != nil {
return nil, err
}
if result == nil {
result = &response
if len(result.WorkflowRuns) == limit {
break pagination
}
} else {
for _, run := range response.WorkflowRuns {
result.WorkflowRuns = append(result.WorkflowRuns, run)
if len(result.WorkflowRuns) == limit {
break pagination
}
}
}
}
if opts != nil && opts.WorkflowName != "" {
for i := range result.WorkflowRuns {
result.WorkflowRuns[i].workflowName = opts.WorkflowName
}
} else if len(result.WorkflowRuns) > 0 {
if err := preloadWorkflowNames(client, repo, result.WorkflowRuns); err != nil {
return result, err
}
}
return result, nil
}
func preloadWorkflowNames(client *api.Client, repo ghrepo.Interface, runs []Run) error {
workflows, err := workflowShared.GetWorkflows(client, repo, 0)
if err != nil {
return err
}
workflowMap := map[int64]string{}
for _, wf := range workflows {
workflowMap[wf.ID] = wf.Name
}
for i, run := range runs {
if _, ok := workflowMap[run.WorkflowID]; !ok {
// Look up workflow by ID because it may have been deleted
workflow, err := workflowShared.GetWorkflow(client, repo, run.WorkflowID)
if err != nil {
return err
}
workflowMap[run.WorkflowID] = workflow.Name
}
runs[i].workflowName = workflowMap[run.WorkflowID]
}
return nil
}
type JobsPayload struct {
@ -351,7 +392,7 @@ func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) {
symbol, _ := Symbol(cs, run.Status, run.Conclusion)
candidates = append(candidates,
// TODO truncate commit message, long ones look terrible
fmt.Sprintf("%s %s, %s (%s) %s", symbol, run.CommitMsg(), run.Name, run.HeadBranch, preciseAgo(now, run.StartedTime())))
fmt.Sprintf("%s %s, %s (%s) %s", symbol, run.Title(), run.WorkflowName(), run.HeadBranch, preciseAgo(now, run.StartedTime())))
}
// TODO consider custom filter so it's fuzzier. right now matches start anywhere in string but
@ -380,6 +421,14 @@ func GetRun(client *api.Client, repo ghrepo.Interface, runID string) (*Run, erro
return nil, err
}
// Set name to workflow name
workflow, err := workflowShared.GetWorkflow(client, repo, result.WorkflowID)
if err != nil {
return nil, err
} else {
result.workflowName = workflow.Name
}
return &result, nil
}

View file

@ -3,13 +3,19 @@ package shared
import (
"fmt"
"time"
workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared"
)
var TestRunStartTime, _ = time.Parse("2006-01-02 15:04:05", "2021-02-23 04:51:00")
func TestRun(name string, id int64, s Status, c Conclusion) Run {
func TestRun(id int64, s Status, c Conclusion) Run {
return TestRunWithCommit(id, s, c, "cool commit")
}
func TestRunWithCommit(id int64, s Status, c Conclusion, commit string) Run {
return Run{
Name: name,
WorkflowID: 123,
ID: id,
CreatedAt: TestRunStartTime,
UpdatedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34),
@ -19,7 +25,7 @@ func TestRun(name string, id int64, s Status, c Conclusion) Run {
HeadBranch: "trunk",
JobsURL: fmt.Sprintf("https://api.github.com/runs/%d/jobs", id),
HeadCommit: Commit{
Message: "cool commit",
Message: commit,
},
HeadSha: "1234567890",
URL: fmt.Sprintf("https://github.com/runs/%d", id),
@ -30,24 +36,24 @@ func TestRun(name string, id int64, s Status, c Conclusion) Run {
}
}
var SuccessfulRun Run = TestRun("successful", 3, Completed, Success)
var FailedRun Run = TestRun("failed", 1234, Completed, Failure)
var SuccessfulRun Run = TestRun(3, Completed, Success)
var FailedRun Run = TestRun(1234, Completed, Failure)
var TestRuns []Run = []Run{
TestRun("timed out", 1, Completed, TimedOut),
TestRun("in progress", 2, InProgress, ""),
TestRun(1, Completed, TimedOut),
TestRun(2, InProgress, ""),
SuccessfulRun,
TestRun("cancelled", 4, Completed, Cancelled),
TestRun(4, Completed, Cancelled),
FailedRun,
TestRun("neutral", 6, Completed, Neutral),
TestRun("skipped", 7, Completed, Skipped),
TestRun("requested", 8, Requested, ""),
TestRun("queued", 9, Queued, ""),
TestRun("stale", 10, Completed, Stale),
TestRun(6, Completed, Neutral),
TestRun(7, Completed, Skipped),
TestRun(8, Requested, ""),
TestRun(9, Queued, ""),
TestRun(10, Completed, Stale),
}
var WorkflowRuns []Run = []Run{
TestRun("in progress", 2, InProgress, ""),
TestRun(2, InProgress, ""),
SuccessfulRun,
FailedRun,
}
@ -111,3 +117,8 @@ var FailedJobAnnotations []Annotation = []Annotation{
StartLine: 420,
},
}
var TestWorkflow workflowShared.Workflow = workflowShared.Workflow{
Name: "CI",
ID: 123,
}

View file

@ -199,7 +199,7 @@ func runView(opts *ViewOptions) error {
if err != nil {
return fmt.Errorf("failed to get runs: %w", err)
}
runID, err = shared.PromptForRun(cs, runs)
runID, err = shared.PromptForRun(cs, runs.WorkflowRuns)
if err != nil {
return err
}
@ -466,15 +466,17 @@ func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp {
// This function takes a zip file of logs and a list of jobs.
// Structure of zip file
// zip/
// ├── jobname1/
// │ ├── 1_stepname.txt
// │ ├── 2_anotherstepname.txt
// │ ├── 3_stepstepname.txt
// │ └── 4_laststepname.txt
// └── jobname2/
// ├── 1_stepname.txt
// └── 2_somestepname.txt
//
// zip/
// ├── jobname1/
// │ ├── 1_stepname.txt
// │ ├── 2_anotherstepname.txt
// │ ├── 3_stepstepname.txt
// │ └── 4_laststepname.txt
// └── jobname2/
// ├── 1_stepname.txt
// └── 2_somestepname.txt
//
// It iterates through the list of jobs and trys to find the matching
// log in the zip file. If the matching log is found it is attached
// to the job.

View file

@ -13,6 +13,7 @@ import (
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/run/shared"
workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -180,22 +181,25 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
httpmock.JSONResponse(shared.SuccessfulRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"),
httpmock.StringResponse(`{}`))
reg.Register(
httpmock.GraphQL(`query PullRequestForRun`),
httpmock.StringResponse(`{"data": {
"repository": {
"pullRequests": {
"nodes": [
{"number": 2898,
"headRepository": {
"owner": {
"login": "OWNER"
},
"name": "REPO"}}
]}}}}`))
"repository": {
"pullRequests": {
"nodes": [
{"number": 2898,
"headRepository": {
"owner": {
"login": "OWNER"
},
"name": "REPO"}}
]}}}}`))
reg.Register(
httpmock.REST("GET", "runs/3/jobs"),
httpmock.JSONResponse(shared.JobsPayload{
@ -207,7 +211,7 @@ func TestViewRun(t *testing.T) {
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"),
httpmock.JSONResponse([]shared.Annotation{}))
},
wantOut: "\n✓ trunk successful #2898 · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n",
wantOut: "\n✓ trunk CI #2898 · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n",
},
{
name: "exit status, failed run",
@ -219,6 +223,9 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/artifacts"),
httpmock.StringResponse(`{}`))
@ -236,7 +243,7 @@ func TestViewRun(t *testing.T) {
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"),
httpmock.JSONResponse(shared.FailedJobAnnotations))
},
wantOut: "\nX trunk failed · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n",
wantOut: "\nX trunk CI · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n",
wantErr: true,
},
{
@ -263,10 +270,13 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "runs/3/jobs"),
httpmock.JSONResponse(shared.JobsPayload{}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: heredoc.Doc(`
trunk successful · 3
trunk CI · 3
Triggered via push about 59 minutes ago
JOBS
@ -307,8 +317,11 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"),
httpmock.JSONResponse([]shared.Annotation{}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n",
wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n",
},
{
name: "verbose",
@ -342,8 +355,11 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"),
httpmock.JSONResponse(shared.FailedJobAnnotations))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: "\nX trunk failed · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n",
wantOut: "\nX trunk CI · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n",
},
{
name: "prompts for choice, one job",
@ -373,6 +389,16 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"),
httpmock.JSONResponse([]shared.Annotation{}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
@ -381,7 +407,7 @@ func TestViewRun(t *testing.T) {
opts: &ViewOptions{
Prompt: true,
},
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n",
wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n",
},
{
name: "interactive with log",
@ -410,6 +436,16 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
httpmock.FileResponse("./fixtures/run_log.zip"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
},
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
@ -435,6 +471,9 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
httpmock.FileResponse("./fixtures/run_log.zip"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: coolJobRunLogOutput,
},
@ -465,6 +504,16 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
httpmock.FileResponse("./fixtures/run_log.zip"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
},
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
@ -496,6 +545,9 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
httpmock.FileResponse("./fixtures/run_log.zip"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: expectedRunLogOutput,
},
@ -526,6 +578,16 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
httpmock.FileResponse("./fixtures/run_log.zip"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
},
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
@ -551,6 +613,9 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
httpmock.FileResponse("./fixtures/run_log.zip"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: quuxTheBarfLogOutput,
},
@ -581,6 +646,16 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
httpmock.FileResponse("./fixtures/run_log.zip"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
@ -612,6 +687,9 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
httpmock.FileResponse("./fixtures/run_log.zip"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: quuxTheBarfLogOutput,
},
@ -625,12 +703,15 @@ func TestViewRun(t *testing.T) {
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"),
httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, "")))
httpmock.JSONResponse(shared.TestRun(2, shared.InProgress, "")))
reg.Register(
httpmock.REST("GET", "runs/2/jobs"),
httpmock.JSONResponse(shared.JobsPayload{
Jobs: []shared.Job{},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantErr: true,
errMsg: "run 2 is still in progress; logs will be available when it is complete",
@ -652,7 +733,10 @@ func TestViewRun(t *testing.T) {
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"),
httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, "")))
httpmock.JSONResponse(shared.TestRun(2, shared.InProgress, "")))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantErr: true,
errMsg: "job 20 is still in progress; logs will be available when it is complete",
@ -672,8 +756,11 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"),
httpmock.JSONResponse([]shared.Annotation{}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n",
wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n",
},
{
name: "interactive, multiple jobs, choose all jobs",
@ -707,6 +794,16 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"),
httpmock.JSONResponse(shared.FailedJobAnnotations))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
@ -714,7 +811,7 @@ func TestViewRun(t *testing.T) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(0)
},
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=<job-id>\nView this run on GitHub: https://github.com/runs/3\n",
wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=<job-id>\nView this run on GitHub: https://github.com/runs/3\n",
},
{
name: "interactive, multiple jobs, choose specific jobs",
@ -742,6 +839,16 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"),
httpmock.JSONResponse([]shared.Annotation{}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
@ -749,7 +856,7 @@ func TestViewRun(t *testing.T) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(1)
},
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n",
wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n",
},
{
name: "web run",
@ -762,6 +869,9 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
httpmock.JSONResponse(shared.SuccessfulRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
browsedURL: "https://github.com/runs/3",
wantOut: "Opening github.com/runs/3 in your browser.\n",
@ -780,6 +890,9 @@ func TestViewRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
httpmock.JSONResponse(shared.SuccessfulRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
browsedURL: "https://github.com/jobs/10?check_suite_focus=true",
wantOut: "Opening github.com/jobs/10 in your browser.\n",
@ -793,15 +906,18 @@ func TestViewRun(t *testing.T) {
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123"),
httpmock.JSONResponse(shared.TestRun("failed no job", 123, shared.Completed, shared.Failure)))
httpmock.JSONResponse(shared.TestRun(123, shared.Completed, shared.Failure)))
reg.Register(
httpmock.REST("GET", "runs/123/jobs"),
httpmock.JSONResponse(shared.JobsPayload{Jobs: []shared.Job{}}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123/artifacts"),
httpmock.StringResponse(`{}`))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: "\nX trunk failed no job · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n",
wantOut: "\nX trunk CI · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n",
},
{
name: "hide job header, startup_failure",
@ -812,15 +928,18 @@ func TestViewRun(t *testing.T) {
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123"),
httpmock.JSONResponse(shared.TestRun("failed no job", 123, shared.Completed, shared.StartupFailure)))
httpmock.JSONResponse(shared.TestRun(123, shared.Completed, shared.StartupFailure)))
reg.Register(
httpmock.REST("GET", "runs/123/jobs"),
httpmock.JSONResponse(shared.JobsPayload{Jobs: []shared.Job{}}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123/artifacts"),
httpmock.StringResponse(`{}`))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: "\nX trunk failed no job · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n",
wantOut: "\nX trunk CI · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n",
},
}
@ -879,13 +998,14 @@ func TestViewRun(t *testing.T) {
}
// Structure of fixture zip file
// run log/
// ├── cool job/
// │ ├── 1_fob the barz.txt
// │ └── 2_barz the fob.txt
// └── sad job/
// ├── 1_barf the quux.txt
// └── 2_quux the barf.txt
//
// run log/
// ├── cool job/
// │ ├── 1_fob the barz.txt
// │ └── 2_barz the fob.txt
// └── sad job/
// ├── 1_barf the quux.txt
// └── 2_quux the barf.txt
func Test_attachRunLog(t *testing.T) {
tests := []struct {
name string

View file

@ -121,7 +121,7 @@ func watchRun(opts *WatchOptions) error {
}
if run.Status == shared.Completed {
fmt.Fprintf(opts.IO.Out, "Run %s (%s) has already completed with '%s'\n", cs.Bold(run.Name), cs.Cyanf("%d", run.ID), run.Conclusion)
fmt.Fprintf(opts.IO.Out, "Run %s (%s) has already completed with '%s'\n", cs.Bold(run.WorkflowName()), cs.Cyanf("%d", run.ID), run.Conclusion)
if opts.ExitStatus && run.Conclusion != shared.Success {
return cmdutil.SilentError
}
@ -186,7 +186,7 @@ func watchRun(opts *WatchOptions) error {
if opts.IO.IsStdoutTTY() {
fmt.Fprintln(opts.IO.Out)
fmt.Fprintf(opts.IO.Out, "%s Run %s (%s) completed with '%s'\n", symbolColor(symbol), cs.Bold(run.Name), id, run.Conclusion)
fmt.Fprintf(opts.IO.Out, "%s Run %s (%s) completed with '%s'\n", symbolColor(symbol), cs.Bold(run.WorkflowName()), id, run.Conclusion)
}
if opts.ExitStatus && run.Conclusion != shared.Success {

View file

@ -9,6 +9,7 @@ import (
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/run/shared"
workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -98,13 +99,13 @@ func TestNewCmdWatch(t *testing.T) {
func TestWatchRun(t *testing.T) {
failedRunStubs := func(reg *httpmock.Registry) {
inProgressRun := shared.TestRun("more runs", 2, shared.InProgress, "")
completedRun := shared.TestRun("more runs", 2, shared.Completed, shared.Failure)
inProgressRun := shared.TestRunWithCommit(2, shared.InProgress, "", "commit2")
completedRun := shared.TestRun(2, shared.Completed, shared.Failure)
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
httpmock.JSONResponse(shared.RunsPayload{
WorkflowRuns: []shared.Run{
shared.TestRun("run", 1, shared.InProgress, ""),
shared.TestRunWithCommit(1, shared.InProgress, "", "commit1"),
inProgressRun,
},
}))
@ -128,15 +129,28 @@ func TestWatchRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"),
httpmock.JSONResponse(shared.FailedJobAnnotations))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
}
successfulRunStubs := func(reg *httpmock.Registry) {
inProgressRun := shared.TestRun("more runs", 2, shared.InProgress, "")
completedRun := shared.TestRun("more runs", 2, shared.Completed, shared.Success)
inProgressRun := shared.TestRunWithCommit(2, shared.InProgress, "", "commit2")
completedRun := shared.TestRun(2, shared.Completed, shared.Success)
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
httpmock.JSONResponse(shared.RunsPayload{
WorkflowRuns: []shared.Run{
shared.TestRun("run", 1, shared.InProgress, ""),
shared.TestRunWithCommit(1, shared.InProgress, "", "commit1"),
inProgressRun,
},
}))
@ -163,6 +177,19 @@ func TestWatchRun(t *testing.T) {
shared.SuccessfulJob,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
}
tests := []struct {
@ -184,8 +211,11 @@ func TestWatchRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: "Run failed (1234) has already completed with 'failure'\n",
wantOut: "Run CI (1234) has already completed with 'failure'\n",
},
{
name: "already completed, exit status",
@ -197,8 +227,11 @@ func TestWatchRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"),
httpmock.JSONResponse(shared.TestWorkflow))
},
wantOut: "Run failed (1234) has already completed with 'failure'\n",
wantOut: "Run CI (1234) has already completed with 'failure'\n",
wantErr: true,
errMsg: "SilentError",
},
@ -219,6 +252,13 @@ func TestWatchRun(t *testing.T) {
shared.SuccessfulRun,
},
}))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(workflowShared.WorkflowsPayload{
Workflows: []workflowShared.Workflow{
shared.TestWorkflow,
},
}))
},
},
{
@ -231,10 +271,10 @@ func TestWatchRun(t *testing.T) {
httpStubs: successfulRunStubs,
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("Select a workflow run").
AssertOptions([]string{"* cool commit, run (trunk) Feb 23, 2021", "* cool commit, more runs (trunk) Feb 23, 2021"}).
AnswerWith("* cool commit, more runs (trunk) Feb 23, 2021")
AssertOptions([]string{"* commit1, CI (trunk) Feb 23, 2021", "* commit2, CI (trunk) Feb 23, 2021"}).
AnswerWith("* commit2, CI (trunk) Feb 23, 2021")
},
wantOut: "\x1b[?1049h\x1b[0;0H\x1b[JRefreshing run status every 0 seconds. Press Ctrl+C to quit.\n\n* trunk more runs · 2\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\x1b[?1049l✓ trunk more runs · 2\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\n✓ Run more runs (2) completed with 'success'\n",
wantOut: "\x1b[?1049h\x1b[0;0H\x1b[JRefreshing run status every 0 seconds. Press Ctrl+C to quit.\n\n* trunk CI · 2\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\x1b[?1049l✓ trunk CI · 2\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\n✓ Run CI (2) completed with 'success'\n",
},
{
name: "exit status respected",
@ -247,10 +287,10 @@ func TestWatchRun(t *testing.T) {
httpStubs: failedRunStubs,
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("Select a workflow run").
AssertOptions([]string{"* cool commit, run (trunk) Feb 23, 2021", "* cool commit, more runs (trunk) Feb 23, 2021"}).
AnswerWith("* cool commit, more runs (trunk) Feb 23, 2021")
AssertOptions([]string{"* commit1, CI (trunk) Feb 23, 2021", "* commit2, CI (trunk) Feb 23, 2021"}).
AnswerWith("* commit2, CI (trunk) Feb 23, 2021")
},
wantOut: "\x1b[?1049h\x1b[0;0H\x1b[JRefreshing run status every 0 seconds. Press Ctrl+C to quit.\n\n* trunk more runs · 2\nTriggered via push about 59 minutes ago\n\n\x1b[?1049lX trunk more runs · 2\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nX Run more runs (2) completed with 'failure'\n",
wantOut: "\x1b[?1049h\x1b[0;0H\x1b[JRefreshing run status every 0 seconds. Press Ctrl+C to quit.\n\n* trunk CI · 2\nTriggered via push about 59 minutes ago\n\n\x1b[?1049lX trunk CI · 2\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nX Run CI (2) completed with 'failure'\n",
wantErr: true,
errMsg: "SilentError",
},

View file

@ -132,9 +132,6 @@ func TestDisableRun(t *testing.T) {
},
tty: true,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{
@ -157,9 +154,6 @@ func TestDisableRun(t *testing.T) {
},
tty: true,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/another%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{
@ -216,9 +210,6 @@ func TestDisableRun(t *testing.T) {
Selector: "a workflow",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{

View file

@ -132,9 +132,6 @@ func TestEnableRun(t *testing.T) {
},
tty: true,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/terrible%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{
@ -158,9 +155,6 @@ func TestEnableRun(t *testing.T) {
},
tty: true,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20disabled%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{
@ -187,9 +181,6 @@ func TestEnableRun(t *testing.T) {
},
tty: true,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20disabled%20inactivity%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{
@ -242,9 +233,6 @@ func TestEnableRun(t *testing.T) {
Selector: "terrible workflow",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/terrible%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{
@ -267,9 +255,6 @@ func TestEnableRun(t *testing.T) {
Selector: "a disabled inactivity workflow",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20disabled%20inactivity%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{
@ -291,9 +276,6 @@ func TestEnableRun(t *testing.T) {
Selector: "a disabled workflow",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20disabled%20workflow"),
httpmock.StatusStringResponse(404, "not found"))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
httpmock.JSONResponse(shared.WorkflowsPayload{

View file

@ -6,6 +6,7 @@ import (
"fmt"
"net/url"
"path"
"strconv"
"strings"
"github.com/AlecAivazis/survey/v2"
@ -117,32 +118,33 @@ func SelectWorkflow(workflows []Workflow, promptMsg string, states []WorkflowSta
return &filtered[selected], nil
}
// FindWorkflow looks up a workflow either by numeric database ID, file name, or its Name field
func FindWorkflow(client *api.Client, repo ghrepo.Interface, workflowSelector string, states []WorkflowState) ([]Workflow, error) {
if workflowSelector == "" {
return nil, errors.New("empty workflow selector")
}
workflow, err := getWorkflowByID(client, repo, workflowSelector)
if err == nil {
return []Workflow{*workflow}, nil
} else {
var httpErr api.HTTPError
if !errors.As(err, &httpErr) || httpErr.StatusCode != 404 {
if _, err := strconv.Atoi(workflowSelector); err == nil || strings.HasSuffix(workflowSelector, ".yml") {
workflow, err := getWorkflowByID(client, repo, workflowSelector)
if err != nil {
return nil, err
}
return []Workflow{*workflow}, nil
}
return getWorkflowsByName(client, repo, workflowSelector, states)
}
func GetWorkflow(client *api.Client, repo ghrepo.Interface, workflowID int64) (*Workflow, error) {
return getWorkflowByID(client, repo, strconv.FormatInt(workflowID, 10))
}
// ID can be either a numeric database ID or the workflow file name
func getWorkflowByID(client *api.Client, repo ghrepo.Interface, ID string) (*Workflow, error) {
var workflow Workflow
err := client.REST(repo.RepoHost(), "GET",
fmt.Sprintf("repos/%s/actions/workflows/%s", ghrepo.FullName(repo), url.PathEscape(ID)),
nil, &workflow)
if err != nil {
path := fmt.Sprintf("repos/%s/actions/workflows/%s", ghrepo.FullName(repo), url.PathEscape(ID))
if err := client.REST(repo.RepoHost(), "GET", path, nil, &workflow); err != nil {
return nil, err
}
@ -154,24 +156,17 @@ func getWorkflowsByName(client *api.Client, repo ghrepo.Interface, name string,
if err != nil {
return nil, fmt.Errorf("couldn't fetch workflows for %s: %w", ghrepo.FullName(repo), err)
}
filtered := []Workflow{}
var filtered []Workflow
for _, workflow := range workflows {
desiredState := false
for _, state := range states {
if workflow.State == state {
desiredState = true
break
}
}
if !desiredState {
if !strings.EqualFold(workflow.Name, name) {
continue
}
// TODO consider fuzzy or prefix match
if strings.EqualFold(workflow.Name, name) {
filtered = append(filtered, workflow)
for _, state := range states {
if workflow.State == state {
filtered = append(filtered, workflow)
break
}
}
}

View file

@ -1,31 +0,0 @@
package view
import (
"fmt"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
runShared "github.com/cli/cli/v2/pkg/cmd/run/shared"
"github.com/cli/cli/v2/pkg/cmd/workflow/shared"
)
type workflowRuns struct {
Total int
Runs []runShared.Run
}
func getWorkflowRuns(client *api.Client, repo ghrepo.Interface, workflow *shared.Workflow) (workflowRuns, error) {
var wr workflowRuns
var result runShared.RunsPayload
path := fmt.Sprintf("repos/%s/actions/workflows/%d/runs?per_page=%d&page=%d", ghrepo.FullName(repo), workflow.ID, 5, 1)
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
if err != nil {
return wr, err
}
wr.Total = result.TotalCount
wr.Runs = append(wr.Runs, result.WorkflowRuns...)
return wr, nil
}

View file

@ -195,7 +195,10 @@ func viewWorkflowContent(opts *ViewOptions, client *api.Client, repo ghrepo.Inte
}
func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interface, workflow *shared.Workflow) error {
wr, err := getWorkflowRuns(client, repo, workflow)
wr, err := runShared.GetRuns(client, repo, &runShared.FilterOptions{
WorkflowID: workflow.ID,
WorkflowName: workflow.Name,
}, 5)
if err != nil {
return fmt.Errorf("failed to get runs: %w", err)
}
@ -210,13 +213,13 @@ func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interfa
fmt.Fprintf(out, "ID: %s\n\n", cs.Cyanf("%d", workflow.ID))
// Runs
fmt.Fprintf(out, "Total runs %d\n", wr.Total)
fmt.Fprintf(out, "Total runs %d\n", wr.TotalCount)
if wr.Total != 0 {
if wr.TotalCount != 0 {
fmt.Fprintln(out, "Recent runs")
}
for _, run := range wr.Runs {
for _, run := range wr.WorkflowRuns {
if opts.Raw {
tp.AddField(string(run.Status), nil, nil)
tp.AddField(string(run.Conclusion), nil, nil)
@ -225,9 +228,9 @@ func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interfa
tp.AddField(symbol, nil, symbolColor)
}
tp.AddField(run.CommitMsg(), nil, cs.Bold)
tp.AddField(run.Title(), nil, cs.Bold)
tp.AddField(run.Name, nil, nil)
tp.AddField(run.WorkflowName(), nil, nil)
tp.AddField(run.HeadBranch, nil, cs.Bold)
tp.AddField(string(run.Event), nil, nil)
@ -248,7 +251,7 @@ func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interfa
fmt.Fprintln(out)
// Footer
if wr.Total != 0 {
if wr.TotalCount != 0 {
fmt.Fprintf(out, "To see more runs for this workflow, try: gh run list --workflow %s\n", filename)
}
fmt.Fprintf(out, "To see the YAML for this workflow, try: gh workflow view %s --yaml\n", filename)

View file

@ -176,10 +176,10 @@ func TestViewRun(t *testing.T) {
Total runs 10
Recent runs
X cool commit timed out trunk push 1
* cool commit in progress trunk push 2
cool commit successful trunk push 3
X cool commit cancelled trunk push 4
X cool commit a workflow trunk push 1
* cool commit a workflow trunk push 2
cool commit a workflow trunk push 3
X cool commit a workflow trunk push 4
To see more runs for this workflow, try: gh run list --workflow flow.yml
To see the YAML for this workflow, try: gh workflow view flow.yml --yaml