Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions internal/dispatch/fixtures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ package dispatch

var (
currentUserResponse string = `{
"data": {
"viewer": {
"login": "mdb"
}
}
"login": "mdb"
}`

getWorkflowsResponse string = `{
Expand Down
6 changes: 1 addition & 5 deletions internal/dispatch/ghrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ func getRepoOption(cmd *cobra.Command) (*ghRepo, error) {
return repo, nil
}

// ghRepo satisfies the ghrepo interface.
// In the context of gh-dispatch, it enables the reuse of
// functions packaged in the upstream github.com/cli/cli
// codebase for rendering GH Actions run output.
// See github.com/cli/cli/v2/internal/ghrepo.
// ghRepo identifies a GitHub repository and the host on which it lives.
type ghRepo struct {
Owner string
Name string
Expand Down
112 changes: 89 additions & 23 deletions internal/dispatch/renderutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import (
"bytes"
"fmt"
"io"
"strings"
"time"

cliapi "github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/pkg/cmd/run/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/go-gh/v2/pkg/api"
)

func render(ios *iostreams.IOStreams, client *cliapi.Client, repo *ghRepo, run *shared.Run) error {
func render(ios *iostreams.IOStreams, client *api.RESTClient, repo *ghRepo, run *workflowRun) error {
cs := ios.ColorScheme()
annotationCache := map[int64][]shared.Annotation{}
annotationCache := map[int64][]annotation{}
out := &bytes.Buffer{}
ios.StartAlternateScreenBuffer()

Expand Down Expand Up @@ -42,7 +42,7 @@ func render(ios *iostreams.IOStreams, client *cliapi.Client, repo *ghRepo, run *
return err
}

if run.Status == shared.Completed {
if run.Status == statusCompleted {
break
}

Expand All @@ -55,15 +55,15 @@ func render(ios *iostreams.IOStreams, client *cliapi.Client, repo *ghRepo, run *

ios.StopAlternateScreenBuffer()

symbol, symbolColor := shared.Symbol(cs, run.Status, run.Conclusion)
symbol, symbolColor := renderSymbol(cs, run.Status, run.Conclusion)
id := cs.Cyanf("%d", run.ID)

if ios.IsStdoutTTY() {
fmt.Fprintln(ios.Out)
fmt.Fprintf(ios.Out, "%s %s (%s) completed with '%s'\n", symbolColor(symbol), cs.Bold(run.Name), id, run.Conclusion)
}

if run.Conclusion != shared.Success {
if run.Conclusion != conclusionSuccess {
return cmdutil.SilentError
}

Expand All @@ -72,33 +72,33 @@ func render(ios *iostreams.IOStreams, client *cliapi.Client, repo *ghRepo, run *

// renderRun is largely an emulation of the upstream 'gh run watch' implementation...
// https://github.com/cli/cli/blob/v2.20.2/pkg/cmd/run/watch/watch.go
func renderRun(out io.Writer, cs *iostreams.ColorScheme, client *cliapi.Client, repo *ghRepo, run *shared.Run, annotationCache map[int64][]shared.Annotation) (*shared.Run, error) {
run, err := shared.GetRun(client, repo, fmt.Sprintf("%d", run.ID), 0)
func renderRun(out io.Writer, cs *iostreams.ColorScheme, client *api.RESTClient, repo *ghRepo, run *workflowRun, annotationCache map[int64][]annotation) (*workflowRun, error) {
run, err := getRun(client, repo, run.ID)
if err != nil {
return nil, fmt.Errorf("failed to get run: %w", err)
}

jobs, err := shared.GetJobs(client, repo, run, 0)
jobs, err := getJobs(client, run)
if err != nil {
return nil, fmt.Errorf("failed to get jobs: %w", err)
}

var annotations []shared.Annotation
var annotations []annotation
var annotationErr error
var as []shared.Annotation
var as []annotation
for _, job := range jobs {
if as, ok := annotationCache[job.ID]; ok {
annotations = as
continue
}

as, annotationErr = shared.GetAnnotations(client, repo, job)
as, annotationErr = getAnnotations(client, repo, job)
if annotationErr != nil {
break
}
annotations = append(annotations, as...)

if job.Status != shared.InProgress {
if job.Status != statusInProgress {
annotationCache[job.ID] = annotations
}
}
Expand All @@ -107,36 +107,33 @@ func renderRun(out io.Writer, cs *iostreams.ColorScheme, client *cliapi.Client,
return nil, fmt.Errorf("failed to get annotations: %w", annotationErr)
}

fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, "", "", 0))
fmt.Fprintln(out, renderRunHeader(cs, run))
fmt.Fprintln(out)

if len(jobs) == 0 {
return run, nil
}

fmt.Fprintln(out, cs.Bold("JOBS"))
fmt.Fprintln(out, shared.RenderJobs(cs, jobs, true))
fmt.Fprintln(out, renderJobs(cs, jobs))

if len(annotations) > 0 {
fmt.Fprintln(out)
fmt.Fprintln(out, cs.Bold("ANNOTATIONS"))
fmt.Fprintln(out, shared.RenderAnnotations(cs, annotations))
fmt.Fprintln(out, renderAnnotations(cs, annotations))
}

return run, nil
}

func getRunID(client *cliapi.Client, repo *ghRepo, event string, workflowID int64, dispatchedAt time.Time) (int64, error) {
actor, err := cliapi.CurrentLoginName(client, repo.RepoHost())
func getRunID(client *api.RESTClient, repo *ghRepo, event string, workflowID int64, dispatchedAt time.Time) (int64, error) {
actor, err := currentLoginName(client)
if err != nil {
return 0, err
}

for {
runs, err := shared.GetRunsWithFilter(client, repo, &shared.FilterOptions{
WorkflowID: workflowID,
Actor: actor,
}, 1, func(run shared.Run) bool {
runs, err := getRuns(client, repo, workflowID, actor, func(run workflowRun) bool {
// TODO: should this try to match on a branch too?
// https://github.com/cli/cli/blob/trunk/pkg/cmd/run/shared/shared.go#L281
return run.WorkflowID == workflowID && run.Event == event && !run.CreatedAt.Before(dispatchedAt)
Expand All @@ -150,3 +147,72 @@ func getRunID(client *cliapi.Client, repo *ghRepo, event string, workflowID int6
}
}
}

// The following render functions are adapted from the upstream
// github.com/cli/cli pkg/cmd/run/shared package.

func renderRunHeader(cs *iostreams.ColorScheme, run *workflowRun) string {
symbol, symbolColor := renderSymbol(cs, run.Status, run.Conclusion)
title := fmt.Sprintf("%s %s", cs.Bold(run.HeadBranch), run.WorkflowName)
id := cs.Cyanf("%d", run.ID)

return fmt.Sprintf("%s %s · %s\nTriggered via %s ", symbolColor(symbol), title, id, run.Event)
}

func renderJobs(cs *iostreams.ColorScheme, jobs []job) string {
lines := []string{}
for _, j := range jobs {
elapsed := j.CompletedAt.Sub(j.StartedAt)
elapsedStr := fmt.Sprintf(" in %s", elapsed)
if elapsed < 0 {
elapsedStr = ""
}
symbol, symbolColor := renderSymbol(cs, j.Status, j.Conclusion)
id := cs.Cyanf("%d", j.ID)
lines = append(lines, fmt.Sprintf("%s %s%s (ID %s)", symbolColor(symbol), cs.Bold(j.Name), elapsedStr, id))
for _, s := range j.Steps {
stepSymbol, stepSymbolColor := renderSymbol(cs, s.Status, s.Conclusion)
lines = append(lines, fmt.Sprintf(" %s %s", stepSymbolColor(stepSymbol), s.Name))
}
}

return strings.Join(lines, "\n")
}

func renderAnnotations(cs *iostreams.ColorScheme, annotations []annotation) string {
lines := []string{}
for _, a := range annotations {
lines = append(lines, fmt.Sprintf("%s %s", renderAnnotationSymbol(cs, a), a.Message))
// Following newline is essential for spacing between annotations
lines = append(lines, cs.Mutedf("%s: %s#%d\n", a.JobName, a.Path, a.StartLine))
}

return strings.Join(lines, "\n")
}

func renderSymbol(cs *iostreams.ColorScheme, s status, c conclusion) (string, func(string) string) {
noColor := func(s string) string { return s }
if s == statusCompleted {
switch c {
case conclusionSuccess:
return cs.SuccessIconWithColor(noColor), cs.Green
case conclusionSkipped, conclusionNeutral:
return "-", cs.Muted
default:
return cs.FailureIconWithColor(noColor), cs.Red
}
}

return "*", cs.Yellow
}

func renderAnnotationSymbol(cs *iostreams.ColorScheme, a annotation) string {
switch a.Level {
case annotationFailure:
return cs.FailureIcon()
case annotationWarning:
return cs.WarningIcon()
default:
return "-"
}
}
50 changes: 11 additions & 39 deletions internal/dispatch/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import (
"time"

"github.com/MakeNowJust/heredoc"
cliapi "github.com/cli/cli/v2/api"
runShared "github.com/cli/cli/v2/pkg/cmd/run/shared"
"github.com/cli/cli/v2/pkg/cmd/workflow/shared"
"github.com/cli/cli/v2/pkg/iostreams"
ghapi "github.com/cli/go-gh/v2/pkg/api"
"github.com/cli/go-gh/v2/pkg/api"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -70,15 +67,14 @@ func NewCmdRepository() *cobra.Command {
var repoClientPayload any
json.Unmarshal(b, &repoClientPayload)

ios := iostreams.System()
ghClient, err := ghapi.DefaultHTTPClient()
client, err := api.NewRESTClient(api.ClientOptions{Host: repo.RepoHost()})
if err != nil {
return err
}
dOptions := dispatchOptions{
repo: repo,
httpClient: ghClient,
io: ios,
repo: repo,
client: client,
io: iostreams.System(),
}

return repositoryDispatchRun(&repositoryDispatchOptions{
Expand All @@ -101,7 +97,7 @@ func NewCmdRepository() *cobra.Command {
}

func repositoryDispatchRun(opts *repositoryDispatchOptions) error {
ghClient := cliapi.NewClientFromHTTP(opts.httpClient)
client := opts.client

var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(repositoryDispatchRequest{
Expand All @@ -114,12 +110,12 @@ func repositoryDispatchRun(opts *repositoryDispatchOptions) error {

var in any
dispatchedAt := time.Now()
err = ghClient.REST(opts.repo.RepoHost(), "POST", fmt.Sprintf("repos/%s/dispatches", opts.repo.RepoFullName()), &buf, &in)
err = client.Post(fmt.Sprintf("repos/%s/dispatches", opts.repo.RepoFullName()), &buf, &in)
if err != nil {
return err
}

wfs, err := getWorkflows(ghClient, opts.repo.RepoHost(), opts.repo.RepoFullName())
wfs, err := getWorkflows(client, opts.repo)
if err != nil {
return err
}
Expand All @@ -132,39 +128,15 @@ func repositoryDispatchRun(opts *repositoryDispatchOptions) error {
}
}

runID, err := getRunID(ghClient, opts.repo, "repository_dispatch", workflowID, dispatchedAt)
runID, err := getRunID(client, opts.repo, "repository_dispatch", workflowID, dispatchedAt)
if err != nil {
return err
}

run, err := runShared.GetRun(ghClient, opts.repo, fmt.Sprintf("%d", runID), 0)
run, err := getRun(client, opts.repo, runID)
if err != nil {
return fmt.Errorf("failed to get run: %w", err)
}

return render(opts.io, ghClient, opts.repo, run)
}

func getWorkflows(client *cliapi.Client, repoHost string, repoFullName string) ([]shared.Workflow, error) {
perPage := 100
page := 1
workflows := []shared.Workflow{}

for {
result := shared.WorkflowsPayload{}
path := fmt.Sprintf("repos/%s/actions/workflows?per_page=%d&page=%d", repoFullName, perPage, page)
err := client.REST(repoHost, "GET", path, nil, &result)
if err != nil {
return nil, err
}

workflows = append(workflows, result.Workflows...)
if len(result.Workflows) < perPage {
break
}

page++
}

return workflows, nil
return render(opts.io, client, opts.repo, run)
}
Loading