diff --git a/Makefile b/Makefile index 023b935..d69ce0e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ SOURCE=./... GOFMT_FILES?=$$(find . -type f -name '*.go') -VERSION?=0.0.4 +VERSION?=0.1.0 default: build diff --git a/README.md b/README.md index 8d4f236..485a869 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Install a locally built `gh-dispatch` for use as `gh dispatch`: make install ``` -Run acceptance tests against a local `gh-dispatch` installation: +Run acceptance tests against a local `gh-dispatch` installation, verifying the local `gh-dispatch` is able to trigger dispatch events and render the expected output: ``` make acc-test diff --git a/demo.gif b/demo.gif index de53b82..6ba9cef 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/demo.tape b/demo.tape index dd551a2..3c4e308 100644 --- a/demo.tape +++ b/demo.tape @@ -46,7 +46,7 @@ Set FontSize 28 Set Width 1200 Set Height 650 -Type 'gh dispatch repository --repo "mdb/gh-dispatch" --event-type "hello" --client-payload "{\"name\": \"mike\"}" --workflow "Hello"' +Type 'gh dispatch repository --event-type "hello" --client-payload "{\"name\": \"mike\"}" --workflow "Hello"' Sleep 500ms diff --git a/internal/dispatch/fixtures_test.go b/internal/dispatch/fixtures_test.go index a1cf53b..62eb789 100644 --- a/internal/dispatch/fixtures_test.go +++ b/internal/dispatch/fixtures_test.go @@ -1,6 +1,14 @@ package dispatch var ( + currentUserResponse string = `{ + "data": { + "viewer": { + "login": "mdb" + } + } + }` + getWorkflowsResponse string = `{ "total_count": 1, "workflows": [{ @@ -10,7 +18,8 @@ var ( }` getWorkflowResponse string = `{ - "id": 456 + "id": 456, + "name": "foo" }` getWorkflowRunsResponse string = `{ @@ -18,9 +27,11 @@ var ( "workflow_runs": [{ "id": 123, "workflow_id": 456, + "event": "%s", "name": "foo", "status": "queued", - "conclusion": null + "conclusion": null, + "jobs_url": "https://api.github.com/repos/%s/actions/runs/123/jobs" }] }` diff --git a/internal/dispatch/repository.go b/internal/dispatch/repository.go index 32abaad..cef592b 100644 --- a/internal/dispatch/repository.go +++ b/internal/dispatch/repository.go @@ -4,13 +4,13 @@ import ( "bytes" "encoding/json" "fmt" - "net/http" + "strings" "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" - "github.com/cli/go-gh" - "github.com/cli/go-gh/pkg/api" "github.com/spf13/cobra" ) @@ -59,18 +59,25 @@ func NewCmdRepository() *cobra.Command { --workflow Hello `), RunE: func(cmd *cobra.Command, args []string) error { - repo, _ := cmd.Flags().GetString("repo") + r, _ := cmd.Flags().GetString("repo") + repoParts := strings.Split(r, "/") + repo := &ghRepo{ + Owner: repoParts[0], + Name: repoParts[1], + } b := []byte(repositoryClientPayload) var repoClientPayload interface{} json.Unmarshal(b, &repoClientPayload) ios := iostreams.System() - + ghClient, _ := cliapi.NewHTTPClient(cliapi.HTTPClientOptions{ + Config: &Conf{}, + }) dOptions := dispatchOptions{ - repo: repo, - httpTransport: http.DefaultTransport, - io: ios, + repo: repo, + httpClient: ghClient, + io: ios, } return repositoryDispatchRun(&repositoryDispatchOptions{ @@ -102,22 +109,16 @@ func repositoryDispatchRun(opts *repositoryDispatchOptions) error { return err } - client, err := gh.RESTClient(&api.ClientOptions{ - Transport: opts.httpTransport, - AuthToken: opts.authToken, - }) - if err != nil { - return err - } + ghClient := cliapi.NewClientFromHTTP(opts.httpClient) var in interface{} - err = client.Post(fmt.Sprintf("repos/%s/dispatches", opts.repo), &buf, &in) + err = ghClient.REST(opts.repo.RepoHost(), "POST", fmt.Sprintf("repos/%s/dispatches", opts.repo.RepoFullName()), &buf, &in) if err != nil { return err } var wfs shared.WorkflowsPayload - err = client.Get(fmt.Sprintf("repos/%s/actions/workflows", opts.repo), &wfs) + err = ghClient.REST(opts.repo.RepoHost(), "GET", fmt.Sprintf("repos/%s/actions/workflows", opts.repo.RepoFullName()), nil, &wfs) if err != nil { return err } @@ -130,15 +131,15 @@ func repositoryDispatchRun(opts *repositoryDispatchOptions) error { } } - runID, err := getRunID(client, opts.repo, "repository_dispatch", workflowID) + runID, err := getRunID(ghClient, opts.repo, "repository_dispatch", workflowID) if err != nil { return err } - run, err := getRun(client, opts.repo, runID) + run, err := runShared.GetRun(ghClient, opts.repo, fmt.Sprintf("%d", runID)) if err != nil { return fmt.Errorf("failed to get run: %w", err) } - return render(opts.io, client, opts.repo, run) + return render(opts.io, ghClient, opts.repo, run) } diff --git a/internal/dispatch/repository_test.go b/internal/dispatch/repository_test.go index 32ef979..1bde627 100644 --- a/internal/dispatch/repository_test.go +++ b/internal/dispatch/repository_test.go @@ -2,6 +2,7 @@ package dispatch import ( "fmt" + "net/http" "net/url" "testing" @@ -11,7 +12,13 @@ import ( ) func TestRepositoryDispatchRun(t *testing.T) { - repo := "OWNER/REPO" + ghRepo := &ghRepo{ + Name: "REPO", + Owner: "OWNER", + } + repo := ghRepo.RepoFullName() + event := "repository_dispatch" + tests := []struct { name string opts *repositoryDispatchOptions @@ -35,36 +42,76 @@ func TestRepositoryDispatchRun(t *testing.T) { httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows", repo)), httpmock.StringResponse(getWorkflowsResponse)) + reg.Register( + httpmock.GraphQL("query UserCurrent{viewer{login}}"), + httpmock.StringResponse(currentUserResponse)) + v := url.Values{} - v.Set("event", "repository_dispatch") + v.Set("per_page", "50") + + reg.Register( + httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/workflows/456/runs", repo), v), + httpmock.StringResponse(fmt.Sprintf(getWorkflowRunsResponse, event, repo))) + + q := url.Values{} + q.Set("per_page", "100") + q.Set("page", "1") + + reg.Register( + httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/workflows", repo), q), + httpmock.StringResponse(fmt.Sprintf(getWorkflowRunsResponse, event, repo))) reg.Register( - httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/runs", repo), v), - httpmock.StringResponse(getWorkflowRunsResponse)) + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) + + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123", repo)), httpmock.StringResponse(`{ - "id": 123 + "id": 123, + "workflow_id": 456, + "event": "repository_dispatch" }`)) + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) + reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123", repo)), - httpmock.StringResponse(`{ + httpmock.StringResponse(fmt.Sprintf(`{ "id": 123, + "workflow_id": 456, "status": "completed", - "conclusion": "success" - }`)) + "event": "repository_dispatch", + "conclusion": "success", + "jobs_url": "https://api.github.com/repos/%s/actions/runs/123/jobs" + }`, repo))) reg.Register( - httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123/attempts/1/jobs", repo)), + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123/jobs", repo)), httpmock.StringResponse(getJobsResponse)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/check-runs/123/annotations", repo)), httpmock.StringResponse("[]")) }, - wantOut: "Refreshing run status every 2 seconds. Press Ctrl+C to quit.\n\nhttps://github.com/OWNER/REPO/actions/runs/123\n\n\nJOBS\n✓ build in 1m59s (ID 123)\n ✓ Run actions/checkout@v2\n ✓ Test\n", + wantOut: `Refreshing run status every 2 seconds. Press Ctrl+C to quit. + +https://github.com/OWNER/REPO/actions/runs/123 + +✓ foo · 123 +Triggered via repository_dispatch + +JOBS +✓ build in 1m59s (ID 123) + ✓ Run actions/checkout@v2 + ✓ Test +`, }, { name: "unsuccessful workflow run", opts: &repositoryDispatchOptions{ @@ -81,36 +128,79 @@ func TestRepositoryDispatchRun(t *testing.T) { httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows", repo)), httpmock.StringResponse(getWorkflowsResponse)) + reg.Register( + httpmock.GraphQL("query UserCurrent{viewer{login}}"), + httpmock.StringResponse(currentUserResponse)) + v := url.Values{} - v.Set("event", "repository_dispatch") + v.Set("per_page", "50") + + reg.Register( + httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/workflows/456/runs", repo), v), + httpmock.StringResponse(fmt.Sprintf(getWorkflowRunsResponse, event, repo))) + + q := url.Values{} + q.Set("per_page", "100") + q.Set("page", "1") + + // TODO: is this correct? is it the correct response? + reg.Register( + httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/workflows", repo), q), + httpmock.StringResponse(fmt.Sprintf(getWorkflowRunsResponse, event, repo))) + + // TODO: is this correct? is it the correct response? + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) + // TODO: is this correct? is it the correct response? reg.Register( - httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/runs", repo), v), - httpmock.StringResponse(getWorkflowRunsResponse)) + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123", repo)), httpmock.StringResponse(`{ - "id": 123 + "id": 123, + "workflow_id": 456, + "event": "repository_dispatch" }`)) + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) + reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123", repo)), - httpmock.StringResponse(`{ + httpmock.StringResponse(fmt.Sprintf(`{ "id": 123, + "workflow_id": 456, "status": "completed", - "conclusion": "failure" - }`)) + "event": "repository_dispatch", + "conclusion": "failure", + "jobs_url": "https://api.github.com/repos/%s/actions/runs/123/jobs" + }`, repo))) reg.Register( - httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123/attempts/1/jobs", repo)), + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123/jobs", repo)), httpmock.StringResponse(getFailingJobsResponse)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/check-runs/123/annotations", repo)), httpmock.StringResponse("[]")) }, - wantOut: "Refreshing run status every 2 seconds. Press Ctrl+C to quit.\n\nhttps://github.com/OWNER/REPO/actions/runs/123\n\n\nJOBS\n✓ build in 1m59s (ID 123)\n ✓ Run actions/checkout@v2\n X Test\n", + wantOut: `Refreshing run status every 2 seconds. Press Ctrl+C to quit. + +https://github.com/OWNER/REPO/actions/runs/123 + +X foo · 123 +Triggered via repository_dispatch + +JOBS +✓ build in 1m59s (ID 123) + ✓ Run actions/checkout@v2 + X Test +`, wantErr: true, errMsg: "SilentError", }, { @@ -136,10 +226,11 @@ func TestRepositoryDispatchRun(t *testing.T) { ios.SetStdoutTTY(false) ios.SetAlternateScreenBufferEnabled(false) - tt.opts.repo = repo + tt.opts.repo = ghRepo tt.opts.io = ios - tt.opts.httpTransport = reg - tt.opts.authToken = "123" + tt.opts.httpClient = &http.Client{ + Transport: reg, + } t.Run(tt.name, func(t *testing.T) { err := repositoryDispatchRun(tt.opts) diff --git a/internal/dispatch/shared.go b/internal/dispatch/shared.go index e4c0698..f11a8ec 100644 --- a/internal/dispatch/shared.go +++ b/internal/dispatch/shared.go @@ -7,12 +7,25 @@ import ( "net/http" "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/pkg/api" + "github.com/cli/go-gh/pkg/auth" + "github.com/cli/go-gh/pkg/config" ) +// Conf implements the cliapi tokenGetter interface +type Conf struct { + *config.Config +} + +// AuthToken implements the cliapi tokenGetter interface +// by providing a method for retrieving the auth token. +func (c *Conf) AuthToken(hostname string) (string, string) { + return auth.TokenForHost(hostname) +} + type workflowRun struct { ID int64 `json:"id"` WorkflowID int `json:"workflow_id"` @@ -26,32 +39,37 @@ type workflowRunsResponse struct { } type dispatchOptions struct { - repo string - httpTransport http.RoundTripper - io *iostreams.IOStreams - authToken string + repo *ghRepo + httpClient *http.Client + io *iostreams.IOStreams } -func getRunID(client api.RESTClient, repo, event string, workflowID int64) (int64, error) { - for { - var wRuns workflowRunsResponse - err := client.Get(fmt.Sprintf("repos/%s/actions/runs?event=%s", repo, event), &wRuns) - if err != nil { - return 0, err - } +// ghRepo satisfies the ghrepo interface... +// See github.com/cli/cli/v2/internal/ghrepo. +type ghRepo struct { + Name string + Owner string +} - // TODO: match on workflow name, or somehow more accurately ensure we are fetching - // _the_ workflow triggered by the `gh dispatch` command. - for _, run := range wRuns.WorkflowRuns { - // TODO: should this also try to match on run.triggering_actor.login? - if run.Status != shared.Completed && run.WorkflowID == int(workflowID) { - return run.ID, nil - } - } - } +func (r ghRepo) RepoName() string { + return r.Name +} + +func (r ghRepo) RepoOwner() string { + return r.Owner } -func render(ios *iostreams.IOStreams, client api.RESTClient, repo string, run *shared.Run) error { +func (r ghRepo) RepoHost() string { + host, _ := auth.DefaultHost() + + return host +} + +func (r ghRepo) RepoFullName() string { + return fmt.Sprintf("%s/%s", r.RepoOwner(), r.RepoName()) +} + +func render(ios *iostreams.IOStreams, client *cliapi.Client, repo *ghRepo, run *shared.Run) error { cs := ios.ColorScheme() annotationCache := map[int64][]shared.Annotation{} out := &bytes.Buffer{} @@ -68,10 +86,11 @@ func render(ios *iostreams.IOStreams, client api.RESTClient, repo string, run *s // If not completed, refresh the screen buffer and write the temporary buffer to stdout ios.RefreshScreen() + // TODO: should the refresh interval be configurable? interval := 2 fmt.Fprintln(ios.Out, cs.Boldf("Refreshing run status every %d seconds. Press Ctrl+C to quit.", interval)) fmt.Fprintln(ios.Out) - fmt.Fprintln(ios.Out, cs.Boldf("https://github.com/%s/actions/runs/%d", repo, run.ID)) + fmt.Fprintln(ios.Out, cs.Boldf("https://github.com/%s/actions/runs/%d", repo.RepoFullName(), run.ID)) fmt.Fprintln(ios.Out) _, err = io.Copy(ios.Out, out) @@ -104,13 +123,15 @@ func render(ios *iostreams.IOStreams, client api.RESTClient, repo string, run *s return nil } -func renderRun(out io.Writer, cs *iostreams.ColorScheme, client api.RESTClient, repo string, run *shared.Run, annotationCache map[int64][]shared.Annotation) (*shared.Run, error) { - run, err := getRun(client, repo, run.ID) +// 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)) if err != nil { return nil, fmt.Errorf("failed to get run: %w", err) } - jobs, err := getJobs(client, repo, run.ID) + jobs, err := shared.GetJobs(client, repo, *run) if err != nil { return nil, fmt.Errorf("failed to get jobs: %w", err) } @@ -124,7 +145,7 @@ func renderRun(out io.Writer, cs *iostreams.ColorScheme, client api.RESTClient, continue } - as, annotationErr = getAnnotations(client, repo, job) + as, annotationErr = shared.GetAnnotations(client, repo, job) if annotationErr != nil { break } @@ -139,6 +160,7 @@ func renderRun(out io.Writer, cs *iostreams.ColorScheme, client api.RESTClient, return nil, fmt.Errorf("failed to get annotations: %w", annotationErr) } + fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, "", "")) fmt.Fprintln(out) if len(jobs) == 0 { @@ -157,38 +179,27 @@ func renderRun(out io.Writer, cs *iostreams.ColorScheme, client api.RESTClient, return run, nil } -func getRun(client api.RESTClient, repo string, runID int64) (*shared.Run, error) { - var result shared.Run - err := client.Get(fmt.Sprintf("repos/%s/actions/runs/%d", repo, runID), &result) - if err != nil { - return nil, err - } - - return &result, nil -} - -func getJobs(client api.RESTClient, repo string, runID int64) ([]shared.Job, error) { - var result shared.JobsPayload - err := client.Get(fmt.Sprintf("repos/%s/actions/runs/%d/attempts/1/jobs", repo, runID), &result) +func getRunID(client *cliapi.Client, repo *ghRepo, event string, workflowID int64) (int64, error) { + actor, err := cliapi.CurrentLoginName(client, repo.RepoHost()) if err != nil { - return nil, err + return 0, err } - return result.Jobs, nil -} - -func getAnnotations(client api.RESTClient, repo string, job shared.Job) ([]shared.Annotation, error) { - var result []*shared.Annotation - err := client.Get(fmt.Sprintf("repos/%s/check-runs/%d/annotations", repo, job.ID), &result) - if err != nil { - return nil, err - } + for { + runs, err := shared.GetRunsWithFilter(client, repo, &shared.FilterOptions{ + WorkflowID: workflowID, + Actor: actor, + }, 1, func(run shared.Run) 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.Status != shared.Completed && run.WorkflowID == workflowID && run.Event == event + }) + if err != nil { + return 0, err + } - annotations := []shared.Annotation{} - for _, annotation := range result { - annotation.JobName = job.Name - annotations = append(annotations, *annotation) + if len(runs) > 0 { + return runs[0].ID, nil + } } - - return annotations, nil } diff --git a/internal/dispatch/workflow.go b/internal/dispatch/workflow.go index 3a4a08c..cf4c2ca 100644 --- a/internal/dispatch/workflow.go +++ b/internal/dispatch/workflow.go @@ -4,13 +4,13 @@ import ( "bytes" "encoding/json" "fmt" - "net/http" + "strings" "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" - "github.com/cli/go-gh" - "github.com/cli/go-gh/pkg/api" "github.com/spf13/cobra" ) @@ -64,18 +64,25 @@ func NewCmdWorkflow() *cobra.Command { --ref my-feature-branch `), RunE: func(cmd *cobra.Command, args []string) error { - repo, _ := cmd.Flags().GetString("repo") + r, _ := cmd.Flags().GetString("repo") + repoParts := strings.Split(r, "/") + repo := &ghRepo{ + Owner: repoParts[0], + Name: repoParts[1], + } b := []byte(workflowInputs) var wInputs interface{} json.Unmarshal(b, &wInputs) ios := iostreams.System() - + ghClient, _ := cliapi.NewHTTPClient(cliapi.HTTPClientOptions{ + Config: &Conf{}, + }) dOptions := dispatchOptions{ - repo: repo, - httpTransport: http.DefaultTransport, - io: ios, + repo: repo, + httpClient: ghClient, + io: ios, } return workflowDispatchRun(&workflowDispatchOptions{ @@ -112,35 +119,29 @@ func workflowDispatchRun(opts *workflowDispatchOptions) error { return err } - client, err := gh.RESTClient(&api.ClientOptions{ - Transport: opts.httpTransport, - AuthToken: opts.authToken, - }) - if err != nil { - return err - } + ghClient := cliapi.NewClientFromHTTP(opts.httpClient) var in interface{} - err = client.Post(fmt.Sprintf("repos/%s/actions/workflows/%s/dispatches", opts.repo, opts.workflow), &buf, &in) + err = ghClient.REST(opts.repo.RepoHost(), "POST", fmt.Sprintf("repos/%s/actions/workflows/%s/dispatches", opts.repo.RepoFullName(), opts.workflow), &buf, &in) if err != nil { return err } var wf shared.Workflow - err = client.Get(fmt.Sprintf("repos/%s/actions/workflows/%s", opts.repo, opts.workflow), &wf) + err = ghClient.REST(opts.repo.RepoHost(), "GET", fmt.Sprintf("repos/%s/actions/workflows/%s", opts.repo.RepoFullName(), opts.workflow), nil, &wf) if err != nil { return err } - runID, err := getRunID(client, opts.repo, "workflow_dispatch", wf.ID) + runID, err := getRunID(ghClient, opts.repo, "workflow_dispatch", wf.ID) if err != nil { return err } - run, err := getRun(client, opts.repo, runID) + run, err := runShared.GetRun(ghClient, opts.repo, fmt.Sprintf("%d", runID)) if err != nil { return fmt.Errorf("failed to get run: %w", err) } - return render(opts.io, client, opts.repo, run) + return render(opts.io, ghClient, opts.repo, run) } diff --git a/internal/dispatch/workflow_test.go b/internal/dispatch/workflow_test.go index 88371a0..bf2773f 100644 --- a/internal/dispatch/workflow_test.go +++ b/internal/dispatch/workflow_test.go @@ -2,6 +2,7 @@ package dispatch import ( "fmt" + "net/http" "net/url" "testing" @@ -11,8 +12,14 @@ import ( ) func TestWorkflowDispatchRun(t *testing.T) { - repo := "OWNER/REPO" + ghRepo := &ghRepo{ + Owner: "OWNER", + Name: "REPO", + } + repo := ghRepo.RepoFullName() workflow := "workflow.yaml" + event := "workflow_dispatch" + tests := []struct { name string opts *workflowDispatchOptions @@ -37,36 +44,76 @@ func TestWorkflowDispatchRun(t *testing.T) { httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/workflow.yaml", repo)), httpmock.StringResponse(getWorkflowResponse)) + reg.Register( + httpmock.GraphQL("query UserCurrent{viewer{login}}"), + httpmock.StringResponse(currentUserResponse)) + v := url.Values{} - v.Set("event", "workflow_dispatch") + v.Set("per_page", "50") reg.Register( - httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/runs", repo), v), - httpmock.StringResponse(getWorkflowRunsResponse)) + httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/workflows/456/runs", repo), v), + httpmock.StringResponse(fmt.Sprintf(getWorkflowRunsResponse, event))) + + q := url.Values{} + q.Set("per_page", "100") + q.Set("page", "1") + + reg.Register( + httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/workflows", repo), q), + httpmock.StringResponse(fmt.Sprintf(getWorkflowRunsResponse, event))) + + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123", repo)), httpmock.StringResponse(`{ - "id": 123 + "id": 123, + "workflow_id": 456, + "event": "workflow_dispatch" }`)) + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) + + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) + reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123", repo)), - httpmock.StringResponse(`{ + httpmock.StringResponse(fmt.Sprintf(`{ "id": 123, + "workflow_id": 456, + "event": "workflow_dispatch", "status": "completed", - "conclusion": "success" - }`)) + "conclusion": "success", + "jobs_url": "https://api.github.com/repos/%s/actions/runs/123/jobs" + }`, repo))) reg.Register( - httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123/attempts/1/jobs", repo)), + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123/jobs", repo)), httpmock.StringResponse(getJobsResponse)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/check-runs/123/annotations", repo)), httpmock.StringResponse("[]")) }, - wantOut: "Refreshing run status every 2 seconds. Press Ctrl+C to quit.\n\nhttps://github.com/OWNER/REPO/actions/runs/123\n\n\nJOBS\n✓ build in 1m59s (ID 123)\n ✓ Run actions/checkout@v2\n ✓ Test\n", + wantOut: `Refreshing run status every 2 seconds. Press Ctrl+C to quit. + +https://github.com/OWNER/REPO/actions/runs/123 + +✓ foo · 123 +Triggered via workflow_dispatch + +JOBS +✓ build in 1m59s (ID 123) + ✓ Run actions/checkout@v2 + ✓ Test +`, }, { name: "unsuccessful workflow run", opts: &workflowDispatchOptions{ @@ -83,36 +130,77 @@ func TestWorkflowDispatchRun(t *testing.T) { httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/workflow.yaml", repo)), httpmock.StringResponse(getWorkflowResponse)) + reg.Register( + httpmock.GraphQL("query UserCurrent{viewer{login}}"), + httpmock.StringResponse(currentUserResponse)) + v := url.Values{} - v.Set("event", "workflow_dispatch") + v.Set("per_page", "50") + + reg.Register( + httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/workflows/456/runs", repo), v), + httpmock.StringResponse(fmt.Sprintf(getWorkflowRunsResponse, event))) + + q := url.Values{} + q.Set("per_page", "100") + q.Set("page", "1") reg.Register( - httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/runs", repo), v), - httpmock.StringResponse(getWorkflowRunsResponse)) + httpmock.QueryMatcher("GET", fmt.Sprintf("repos/%s/actions/workflows", repo), q), + httpmock.StringResponse(fmt.Sprintf(getWorkflowRunsResponse, event))) + + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) + + // TODO: is this correct? is it the correct response? + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123", repo)), httpmock.StringResponse(`{ - "id": 123 + "id": 123, + "workflow_id": 456, + "event": "workflow_dispatch" }`)) + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/workflows/456", repo)), + httpmock.StringResponse(getWorkflowResponse)) + reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123", repo)), - httpmock.StringResponse(`{ + httpmock.StringResponse(fmt.Sprintf(`{ "id": 123, + "workflow_id": 456, + "event": "workflow_dispatch", "status": "completed", - "conclusion": "failure" - }`)) + "conclusion": "failure", + "jobs_url": "https://api.github.com/repos/%s/actions/runs/123/jobs" + }`, repo))) reg.Register( - httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123/attempts/1/jobs", repo)), + httpmock.REST("GET", fmt.Sprintf("repos/%s/actions/runs/123/jobs", repo)), httpmock.StringResponse(getFailingJobsResponse)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/check-runs/123/annotations", repo)), httpmock.StringResponse("[]")) }, - wantOut: "Refreshing run status every 2 seconds. Press Ctrl+C to quit.\n\nhttps://github.com/OWNER/REPO/actions/runs/123\n\n\nJOBS\n✓ build in 1m59s (ID 123)\n ✓ Run actions/checkout@v2\n X Test\n", + wantOut: `Refreshing run status every 2 seconds. Press Ctrl+C to quit. + +https://github.com/OWNER/REPO/actions/runs/123 + +X foo · 123 +Triggered via workflow_dispatch + +JOBS +✓ build in 1m59s (ID 123) + ✓ Run actions/checkout@v2 + X Test +`, wantErr: true, errMsg: "SilentError", }, { @@ -139,10 +227,11 @@ func TestWorkflowDispatchRun(t *testing.T) { ios.SetStdoutTTY(false) ios.SetAlternateScreenBufferEnabled(false) - tt.opts.repo = repo + tt.opts.repo = ghRepo tt.opts.io = ios - tt.opts.httpTransport = reg - tt.opts.authToken = "123" + tt.opts.httpClient = &http.Client{ + Transport: reg, + } t.Run(tt.name, func(t *testing.T) { err := workflowDispatchRun(tt.opts) @@ -156,6 +245,7 @@ func TestWorkflowDispatchRun(t *testing.T) { if got := stdout.String(); got != tt.wantOut { t.Errorf("got stdout:\n%q\nwant:\n%q", got, tt.wantOut) } + reg.Verify(t) }) }