Merge pull request #12695 from cli/babakks/retrieve-workflow-dispatch-run-id
feat(workflow run): retrieve workflow dispatch run details
This commit is contained in:
commit
027adc7bf5
5 changed files with 580 additions and 54 deletions
|
|
@ -28,6 +28,10 @@ func (md *DisabledDetectorMock) ReleaseFeatures() (ReleaseFeatures, error) {
|
|||
return ReleaseFeatures{}, nil
|
||||
}
|
||||
|
||||
func (md *DisabledDetectorMock) ActionsFeatures() (ActionsFeatures, error) {
|
||||
return ActionsFeatures{}, nil
|
||||
}
|
||||
|
||||
type EnabledDetectorMock struct{}
|
||||
|
||||
func (md *EnabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
|
||||
|
|
@ -56,6 +60,12 @@ func (md *EnabledDetectorMock) ReleaseFeatures() (ReleaseFeatures, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (md *EnabledDetectorMock) ActionsFeatures() (ActionsFeatures, error) {
|
||||
return ActionsFeatures{
|
||||
DispatchRunDetails: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type AdvancedIssueSearchDetectorMock struct {
|
||||
EnabledDetectorMock
|
||||
searchFeatures SearchFeatures
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ type Detector interface {
|
|||
ProjectsV1() gh.ProjectsV1Support
|
||||
SearchFeatures() (SearchFeatures, error)
|
||||
ReleaseFeatures() (ReleaseFeatures, error)
|
||||
ActionsFeatures() (ActionsFeatures, error)
|
||||
}
|
||||
|
||||
type IssueFeatures struct {
|
||||
|
|
@ -98,6 +99,16 @@ type ReleaseFeatures struct {
|
|||
ImmutableReleases bool
|
||||
}
|
||||
|
||||
type ActionsFeatures struct {
|
||||
// DispatchRunDetails indicates whether the API supports the `return_run_details`
|
||||
// field in workflow dispatches that, when set to true, will return the details
|
||||
// of the created workflow run in the response (with status code 200).
|
||||
//
|
||||
// On older API versions (e.g. GHES 3.20 or earlier), this new field is not
|
||||
// supported and setting it will cause an error.
|
||||
DispatchRunDetails bool
|
||||
}
|
||||
|
||||
type detector struct {
|
||||
host string
|
||||
httpClient *http.Client
|
||||
|
|
@ -393,6 +404,54 @@ func (d *detector) ReleaseFeatures() (ReleaseFeatures, error) {
|
|||
return ReleaseFeatures{}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
enterpriseWorkflowDispatchRunDetailsSupport = "3.21.0"
|
||||
)
|
||||
|
||||
func (d *detector) ActionsFeatures() (ActionsFeatures, error) {
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// Once GHES 3.20 support ends, we don't need feature detection for workflow dispatch (i.e. run details support).
|
||||
//
|
||||
// On github.com, workflow dispatch API now supports a new field named `return_run_details` that enabling it will
|
||||
// result in a 200 OK response with the details of the created workflow run. If not set (or set to false), the API
|
||||
// will keep the old behavior of returning a 204 No Content response.
|
||||
//
|
||||
// On GHES (current latest at 3.20), this new field is not available, and setting it will cause a 400 response.
|
||||
//
|
||||
// Once GHES 3.20 support ends, we can remove the feature detection and start using the new field in API calls.
|
||||
//
|
||||
// IMPORTANT: In the future REST API versions (i.e. breaking changes), the workflow dispatch endpoint is going to
|
||||
// always return the details of the created workflow run in the response, and the `return_run_details` field is
|
||||
// going to be ignored/removed. So, once we are migrating to the new API version we should double check the status
|
||||
// of the API.
|
||||
|
||||
if !ghauth.IsEnterprise(d.host) {
|
||||
return ActionsFeatures{
|
||||
DispatchRunDetails: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
minSupportedVersion, err := version.NewVersion(enterpriseWorkflowDispatchRunDetailsSupport)
|
||||
if err != nil {
|
||||
return ActionsFeatures{}, err
|
||||
}
|
||||
|
||||
hostVersion, err := resolveEnterpriseVersion(d.httpClient, d.host)
|
||||
if err != nil {
|
||||
return ActionsFeatures{}, err
|
||||
}
|
||||
|
||||
if hostVersion.GreaterThanOrEqual(minSupportedVersion) {
|
||||
return ActionsFeatures{
|
||||
DispatchRunDetails: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return ActionsFeatures{
|
||||
DispatchRunDetails: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveEnterpriseVersion(httpClient *http.Client, host string) (*version.Version, error) {
|
||||
var metaResponse struct {
|
||||
InstalledVersion string `json:"installed_version"`
|
||||
|
|
|
|||
|
|
@ -696,3 +696,71 @@ func TestReleaseFeatures(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsFeatures(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hostname string
|
||||
httpStubs func(*httpmock.Registry)
|
||||
wantFeatures ActionsFeatures
|
||||
}{
|
||||
{
|
||||
name: "github.com, workflow dispatch run details supported",
|
||||
hostname: "github.com",
|
||||
wantFeatures: ActionsFeatures{
|
||||
DispatchRunDetails: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ghec data residency (ghe.com), workflow dispatch run details supported",
|
||||
hostname: "stampname.ghe.com",
|
||||
wantFeatures: ActionsFeatures{
|
||||
DispatchRunDetails: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GHE 3.20, workflow dispatch run details not supported",
|
||||
hostname: "git.my.org",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "api/v3/meta"),
|
||||
httpmock.StringResponse(`{"installed_version":"3.20.999"}`),
|
||||
)
|
||||
},
|
||||
wantFeatures: ActionsFeatures{
|
||||
DispatchRunDetails: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GHE 3.21, workflow dispatch run details supported",
|
||||
hostname: "git.my.org",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "api/v3/meta"),
|
||||
httpmock.StringResponse(`{"installed_version":"3.21.0"}`),
|
||||
)
|
||||
},
|
||||
wantFeatures: ActionsFeatures{
|
||||
DispatchRunDetails: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
httpmock.ReplaceTripper(httpClient, reg)
|
||||
|
||||
detector := NewDetector(httpClient, tt.hostname)
|
||||
|
||||
features, err := detector.ActionsFeatures()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantFeatures, features)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/workflow/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -25,6 +28,7 @@ type RunOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Detector fd.Detector
|
||||
Prompter iprompter
|
||||
|
||||
Selector string
|
||||
|
|
@ -64,6 +68,8 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command
|
|||
- Interactively
|
||||
- Via %[1]s-f/--raw-field%[1]s or %[1]s-F/--field%[1]s flags
|
||||
- As JSON, via standard input
|
||||
|
||||
The created workflow run URL will be returned if available.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# Have gh prompt you for what workflow you'd like to run and interactively collect inputs
|
||||
|
|
@ -260,6 +266,11 @@ func runRun(opts *RunOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(c, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, repo.RepoHost())
|
||||
}
|
||||
|
||||
ref := opts.Ref
|
||||
|
||||
if ref == "" {
|
||||
|
|
@ -303,34 +314,77 @@ func runRun(opts *RunOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("repos/%s/actions/workflows/%d/dispatches",
|
||||
ghrepo.FullName(repo), workflow.ID)
|
||||
features, err := opts.Detector.ActionsFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
requestByte, err := json.Marshal(map[string]interface{}{
|
||||
path := fmt.Sprintf("repos/%s/%s/actions/workflows/%d/dispatches", url.PathEscape(repo.RepoOwner()), url.PathEscape(repo.RepoName()), workflow.ID)
|
||||
|
||||
requestBody := map[string]interface{}{
|
||||
"ref": ref,
|
||||
"inputs": providedInputs,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// We will have to always set the `return_run_details` field to true, unless
|
||||
// we opt into the the new REST API version, which will probably return the
|
||||
// details by default.
|
||||
if features.DispatchRunDetails {
|
||||
requestBody["return_run_details"] = true
|
||||
}
|
||||
|
||||
requestByte, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize workflow inputs: %w", err)
|
||||
}
|
||||
|
||||
body := bytes.NewReader(requestByte)
|
||||
|
||||
err = client.REST(repo.RepoHost(), "POST", path, body, nil)
|
||||
var response struct {
|
||||
WorkflowRunID int64 `json:"workflow_run_id"`
|
||||
RunURL string `json:"run_url"`
|
||||
HtmlURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
// Note that the workflow dispatch endpoint used to return 204 No Content
|
||||
// (with no body, obviously). Now it's possible for the endpoint to also
|
||||
// return 200 OK with created run details. So, we have to handle both cases
|
||||
// because old GHE versions still return 204. Even on github.com, we
|
||||
// may still get 204 for any reason.
|
||||
//
|
||||
// Our REST client library is smart enough to ignore JSON unmarshal when it
|
||||
// receives 204, so we're safe here anyway.
|
||||
//
|
||||
// As a related note, the new REST API version (which will come with breaking
|
||||
// changes) will probably default to return 200 + run details.
|
||||
err = client.REST(repo.RepoHost(), "POST", path, body, &response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create workflow dispatch event: %w", err)
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
out := opts.IO.Out
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(out, "%s Created workflow_dispatch event for %s at %s\n",
|
||||
fmt.Fprintf(opts.IO.Out, "%s Created workflow_dispatch event for %s at %s\n",
|
||||
cs.SuccessIcon(), cs.Cyan(workflow.Base()), cs.Bold(ref))
|
||||
|
||||
fmt.Fprintln(out)
|
||||
if response.HtmlURL != "" {
|
||||
fmt.Fprintln(opts.IO.Out, response.HtmlURL)
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "To see runs for this workflow, try: %s\n",
|
||||
fmt.Fprintln(opts.IO.Out)
|
||||
|
||||
if response.WorkflowRunID != 0 {
|
||||
fmt.Fprintf(opts.IO.Out, "To see the created workflow run, try: %s\n",
|
||||
cs.Boldf("gh run view %d", response.WorkflowRunID))
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "To see runs for this workflow, try: %s\n",
|
||||
cs.Boldf("gh run list --workflow=%q", workflow.Base()))
|
||||
} else {
|
||||
if response.HtmlURL != "" {
|
||||
fmt.Fprintln(opts.IO.Out, response.HtmlURL)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/workflow/shared"
|
||||
|
|
@ -394,6 +396,7 @@ jobs:
|
|||
run: echo "${{ github.event.inputs.message }} ${{ fromJSON('["", "🥳"]')[github.event.inputs.use-emoji == 'true'] }} ${{ github.event.inputs.name }}"`)
|
||||
encodedYAMLContentMissingChoiceIp := base64.StdEncoding.EncodeToString(yamlContentMissingChoiceIp)
|
||||
|
||||
// Old GitHub API servers return 204 No Content for successful workflow dispatches.
|
||||
stubs := func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/workflow.yml"),
|
||||
|
|
@ -406,6 +409,24 @@ jobs:
|
|||
httpmock.StatusStringResponse(204, "cool"))
|
||||
}
|
||||
|
||||
// Current GitHub API servers return 200 OK with run info for successful workflow dispatches,
|
||||
// if `return_run_details` is enabled in the request body.
|
||||
stubsWithRunInfo := func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/workflow.yml"),
|
||||
httpmock.JSONResponse(shared.Workflow{
|
||||
Path: ".github/workflows/workflow.yml",
|
||||
ID: 12345,
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/12345/dispatches"),
|
||||
httpmock.StatusJSONResponse(200, map[string]interface{}{
|
||||
"workflow_run_id": int64(6789),
|
||||
"run_url": "https://api.github.com/repos/OWNER/REPO/actions/runs/6789",
|
||||
"html_url": "https://github.com/OWNER/REPO/actions/runs/6789",
|
||||
}))
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *RunOptions
|
||||
|
|
@ -434,11 +455,14 @@ jobs:
|
|||
errOut: "could not parse provided JSON: unexpected end of JSON input",
|
||||
},
|
||||
{
|
||||
name: "good JSON",
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// To be deleted
|
||||
name: "good JSON without run info (204)",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
JSONInput: `{"name":"scully"}`,
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
|
|
@ -447,13 +471,44 @@ jobs:
|
|||
"ref": "trunk",
|
||||
},
|
||||
httpStubs: stubs,
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=\"workflow.yml\"\n",
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for workflow.yml at trunk
|
||||
|
||||
To see runs for this workflow, try: gh run list --workflow="workflow.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "nontty good JSON",
|
||||
name: "good JSON with run info",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
JSONInput: `{"name":"scully"}`,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
"name": "scully",
|
||||
},
|
||||
"ref": "trunk",
|
||||
"return_run_details": true,
|
||||
},
|
||||
httpStubs: stubsWithRunInfo,
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for workflow.yml at trunk
|
||||
https://github.com/OWNER/REPO/actions/runs/6789
|
||||
|
||||
To see the created workflow run, try: gh run view 6789
|
||||
To see runs for this workflow, try: gh run list --workflow="workflow.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// To be deleted
|
||||
name: "nontty good JSON without run info (204)",
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
JSONInput: `{"name":"scully"}`,
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
|
|
@ -464,11 +519,31 @@ jobs:
|
|||
httpStubs: stubs,
|
||||
},
|
||||
{
|
||||
name: "nontty good input fields",
|
||||
name: "nontty good JSON with run info",
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
JSONInput: `{"name":"scully"}`,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
"name": "scully",
|
||||
},
|
||||
"ref": "trunk",
|
||||
"return_run_details": true,
|
||||
},
|
||||
httpStubs: stubsWithRunInfo,
|
||||
wantOut: "https://github.com/OWNER/REPO/actions/runs/6789\n",
|
||||
},
|
||||
{
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// To be deleted
|
||||
name: "nontty good input fields without run info (204)",
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
RawFields: []string{`name=scully`},
|
||||
MagicFields: []string{`greeting=hey`},
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
|
|
@ -480,12 +555,34 @@ jobs:
|
|||
httpStubs: stubs,
|
||||
},
|
||||
{
|
||||
name: "respects ref",
|
||||
name: "nontty good input fields with run info",
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
RawFields: []string{`name=scully`},
|
||||
MagicFields: []string{`greeting=hey`},
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
"name": "scully",
|
||||
"greeting": "hey",
|
||||
},
|
||||
"ref": "trunk",
|
||||
"return_run_details": true,
|
||||
},
|
||||
httpStubs: stubsWithRunInfo,
|
||||
wantOut: "https://github.com/OWNER/REPO/actions/runs/6789\n",
|
||||
},
|
||||
{
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// To be deleted
|
||||
name: "respects ref, without run info (204)",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
JSONInput: `{"name":"scully"}`,
|
||||
Ref: "good-branch",
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
|
|
@ -494,7 +591,36 @@ jobs:
|
|||
"ref": "good-branch",
|
||||
},
|
||||
httpStubs: stubs,
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at good-branch\n\nTo see runs for this workflow, try: gh run list --workflow=\"workflow.yml\"\n",
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for workflow.yml at good-branch
|
||||
|
||||
To see runs for this workflow, try: gh run list --workflow="workflow.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "respects ref, with run info",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
JSONInput: `{"name":"scully"}`,
|
||||
Ref: "good-branch",
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
"name": "scully",
|
||||
},
|
||||
"ref": "good-branch",
|
||||
"return_run_details": true,
|
||||
},
|
||||
httpStubs: stubsWithRunInfo,
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for workflow.yml at good-branch
|
||||
https://github.com/OWNER/REPO/actions/runs/6789
|
||||
|
||||
To see the created workflow run, try: gh run view 6789
|
||||
To see runs for this workflow, try: gh run list --workflow="workflow.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
// TODO this test is somewhat silly; it's more of a placeholder in case I decide to handle the API error more elegantly
|
||||
|
|
@ -503,6 +629,7 @@ jobs:
|
|||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
JSONInput: `{"greeting":"hello there"}`,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -515,6 +642,13 @@ jobs:
|
|||
httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/12345/dispatches"),
|
||||
httpmock.StatusStringResponse(422, "missing something"))
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
"greeting": "hello there",
|
||||
},
|
||||
"ref": "trunk",
|
||||
"return_run_details": true,
|
||||
},
|
||||
wantErr: true,
|
||||
errOut: "could not create workflow dispatch event: HTTP 422 (https://api.github.com/repos/OWNER/REPO/actions/workflows/12345/dispatches)",
|
||||
},
|
||||
|
|
@ -523,6 +657,7 @@ jobs:
|
|||
tty: false,
|
||||
opts: &RunOptions{
|
||||
Selector: "workflow.yaml",
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -530,13 +665,19 @@ jobs:
|
|||
httpmock.StatusStringResponse(200, `{"id": 12345}`))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/12345/dispatches"),
|
||||
httpmock.StatusStringResponse(204, ""))
|
||||
httpmock.StatusJSONResponse(200, map[string]interface{}{
|
||||
"workflow_run_id": int64(6789),
|
||||
"run_url": "https://api.github.com/repos/OWNER/REPO/actions/runs/6789",
|
||||
"html_url": "https://github.com/OWNER/REPO/actions/runs/6789",
|
||||
}))
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{},
|
||||
"ref": "trunk",
|
||||
"inputs": map[string]interface{}{},
|
||||
"ref": "trunk",
|
||||
"return_run_details": true,
|
||||
},
|
||||
wantErr: false,
|
||||
wantOut: "https://github.com/OWNER/REPO/actions/runs/6789\n",
|
||||
},
|
||||
{
|
||||
// TODO this test is somewhat silly; it's more of a placeholder in case I decide to handle the API error more elegantly
|
||||
|
|
@ -544,6 +685,7 @@ jobs:
|
|||
opts: &RunOptions{
|
||||
Selector: "workflow.yml",
|
||||
RawFields: []string{`greeting="hello there"`},
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -563,7 +705,8 @@ jobs:
|
|||
name: "prompt, no workflows enabled",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Prompt: true,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -585,7 +728,8 @@ jobs:
|
|||
name: "prompt, no workflows",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Prompt: true,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -598,10 +742,13 @@ jobs:
|
|||
errOut: "could not fetch workflows for OWNER/REPO: no workflows are enabled",
|
||||
},
|
||||
{
|
||||
name: "prompt, minimal yaml",
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// To be deleted
|
||||
name: "prompt, minimal yaml, without run info (204)",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Prompt: true,
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -634,13 +781,71 @@ jobs:
|
|||
"inputs": map[string]interface{}{},
|
||||
"ref": "trunk",
|
||||
},
|
||||
wantOut: "✓ Created workflow_dispatch event for minimal.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=\"minimal.yml\"\n",
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for minimal.yml at trunk
|
||||
|
||||
To see runs for this workflow, try: gh run list --workflow="minimal.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "prompt",
|
||||
name: "prompt, minimal yaml, with run info",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Prompt: true,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
|
||||
httpmock.JSONResponse(shared.WorkflowsPayload{
|
||||
Workflows: []shared.Workflow{
|
||||
{
|
||||
Name: "minimal workflow",
|
||||
ID: 1,
|
||||
State: shared.Active,
|
||||
Path: ".github/workflows/minimal.yml",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/contents/.github/workflows/minimal.yml"),
|
||||
httpmock.JSONResponse(struct{ Content string }{
|
||||
Content: encodedNoInputsYAMLContent,
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/1/dispatches"),
|
||||
httpmock.StatusJSONResponse(200, map[string]interface{}{
|
||||
"workflow_run_id": int64(6789),
|
||||
"run_url": "https://api.github.com/repos/OWNER/REPO/actions/runs/6789",
|
||||
"html_url": "https://github.com/OWNER/REPO/actions/runs/6789",
|
||||
}))
|
||||
},
|
||||
promptStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Select a workflow", []string{"minimal workflow (minimal.yml)"}, func(_, _ string, opts []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{},
|
||||
"ref": "trunk",
|
||||
"return_run_details": true,
|
||||
},
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for minimal.yml at trunk
|
||||
https://github.com/OWNER/REPO/actions/runs/6789
|
||||
|
||||
To see the created workflow run, try: gh run view 6789
|
||||
To see runs for this workflow, try: gh run list --workflow="minimal.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// To be deleted
|
||||
name: "prompt without run info (204)",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -682,13 +887,80 @@ jobs:
|
|||
},
|
||||
"ref": "trunk",
|
||||
},
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=\"workflow.yml\"\n",
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for workflow.yml at trunk
|
||||
|
||||
To see runs for this workflow, try: gh run list --workflow="workflow.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "prompt, workflow choice input",
|
||||
name: "prompt with run info",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Prompt: true,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
|
||||
httpmock.JSONResponse(shared.WorkflowsPayload{
|
||||
Workflows: []shared.Workflow{
|
||||
{
|
||||
Name: "a workflow",
|
||||
ID: 12345,
|
||||
State: shared.Active,
|
||||
Path: ".github/workflows/workflow.yml",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/contents/.github/workflows/workflow.yml"),
|
||||
httpmock.JSONResponse(struct{ Content string }{
|
||||
Content: encodedYAMLContent,
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/12345/dispatches"),
|
||||
httpmock.StatusJSONResponse(200, map[string]interface{}{
|
||||
"workflow_run_id": int64(6789),
|
||||
"run_url": "https://api.github.com/repos/OWNER/REPO/actions/runs/6789",
|
||||
"html_url": "https://github.com/OWNER/REPO/actions/runs/6789",
|
||||
}))
|
||||
},
|
||||
promptStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Select a workflow", []string{"a workflow (workflow.yml)"}, func(_, _ string, opts []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
pm.RegisterInput("greeting", func(_, _ string) (string, error) {
|
||||
return "hi", nil
|
||||
})
|
||||
pm.RegisterInput("name (required)", func(_, _ string) (string, error) {
|
||||
return "scully", nil
|
||||
})
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
"name": "scully",
|
||||
"greeting": "hi",
|
||||
},
|
||||
"ref": "trunk",
|
||||
"return_run_details": true,
|
||||
},
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for workflow.yml at trunk
|
||||
https://github.com/OWNER/REPO/actions/runs/6789
|
||||
|
||||
To see the created workflow run, try: gh run view 6789
|
||||
To see runs for this workflow, try: gh run list --workflow="workflow.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
// TODO workflowDispatchRunDetailsCleanup
|
||||
// To be deleted
|
||||
name: "prompt, workflow choice input without run info (204)",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -731,13 +1003,79 @@ jobs:
|
|||
},
|
||||
"ref": "trunk",
|
||||
},
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=\"workflow.yml\"\n",
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for workflow.yml at trunk
|
||||
|
||||
To see runs for this workflow, try: gh run list --workflow="workflow.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "prompt, workflow choice input with run info",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
|
||||
httpmock.JSONResponse(shared.WorkflowsPayload{
|
||||
Workflows: []shared.Workflow{
|
||||
{
|
||||
Name: "choice inputs",
|
||||
ID: 12345,
|
||||
State: shared.Active,
|
||||
Path: ".github/workflows/workflow.yml",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/contents/.github/workflows/workflow.yml"),
|
||||
httpmock.JSONResponse(struct{ Content string }{
|
||||
Content: encodedYAMLContentChoiceIp,
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/12345/dispatches"),
|
||||
httpmock.StatusJSONResponse(200, map[string]interface{}{
|
||||
"workflow_run_id": int64(6789),
|
||||
"run_url": "https://api.github.com/repos/OWNER/REPO/actions/runs/6789",
|
||||
"html_url": "https://github.com/OWNER/REPO/actions/runs/6789",
|
||||
}))
|
||||
},
|
||||
promptStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Select a workflow", []string{"choice inputs (workflow.yml)"}, func(_, _ string, opts []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
pm.RegisterSelect("favourite-animal (required)", []string{"dog", "cat"}, func(_, _ string, opts []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
pm.RegisterSelect("name", []string{"monalisa", "cschleiden"}, func(_, _ string, opts []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
|
||||
},
|
||||
wantBody: map[string]interface{}{
|
||||
"inputs": map[string]interface{}{
|
||||
"name": "monalisa",
|
||||
"favourite-animal": "dog",
|
||||
},
|
||||
"ref": "trunk",
|
||||
"return_run_details": true,
|
||||
},
|
||||
wantOut: heredoc.Doc(`
|
||||
✓ Created workflow_dispatch event for workflow.yml at trunk
|
||||
https://github.com/OWNER/REPO/actions/runs/6789
|
||||
|
||||
To see the created workflow run, try: gh run view 6789
|
||||
To see runs for this workflow, try: gh run list --workflow="workflow.yml"
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "prompt, workflow choice missing input",
|
||||
tty: true,
|
||||
opts: &RunOptions{
|
||||
Prompt: true,
|
||||
Prompt: true,
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -757,9 +1095,6 @@ jobs:
|
|||
httpmock.JSONResponse(struct{ Content string }{
|
||||
Content: encodedYAMLContentMissingChoiceIp,
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/12345/dispatches"),
|
||||
httpmock.StatusStringResponse(204, "cool"))
|
||||
},
|
||||
promptStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Select a workflow", []string{"choice missing inputs (workflow.yml)"}, func(_, _ string, opts []string) (int, error) {
|
||||
|
|
@ -775,27 +1110,28 @@ jobs:
|
|||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
}
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return api.InitRepoHostname(&api.Repository{
|
||||
Name: "REPO",
|
||||
Owner: api.RepositoryOwner{Login: "OWNER"},
|
||||
DefaultBranchRef: api.BranchRef{Name: "trunk"},
|
||||
}, "github.com"), nil
|
||||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
}
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return api.InitRepoHostname(&api.Repository{
|
||||
Name: "REPO",
|
||||
Owner: api.RepositoryOwner{Login: "OWNER"},
|
||||
DefaultBranchRef: api.BranchRef{Name: "trunk"},
|
||||
}, "github.com"), nil
|
||||
}
|
||||
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
tt.opts.Prompter = pm
|
||||
if tt.promptStubs != nil {
|
||||
|
|
@ -810,7 +1146,6 @@ jobs:
|
|||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantOut, stdout.String())
|
||||
reg.Verify(t)
|
||||
|
||||
if len(reg.Requests) > 0 {
|
||||
lastRequest := reg.Requests[len(reg.Requests)-1]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue