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:
Babak K. Shandiz 2026-02-17 20:37:53 +00:00 committed by GitHub
commit 027adc7bf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 580 additions and 54 deletions

View file

@ -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

View file

@ -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"`

View file

@ -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)
})
}
}

View file

@ -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

View file

@ -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]