From a0dea00fdd58f20fa06fe37ea0dc12a981d39238 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 11:53:33 +0000 Subject: [PATCH 01/15] fix(featuredetection): add `ActionsFeatures` to detect workflow dispatch features Signed-off-by: Babak K. Shandiz --- .../featuredetection/feature_detection.go | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index 2aff20d98fb..8100882fbb5 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -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 now + // supported, and setting it will cause error. + DispatchRunDetails bool +} + type detector struct { host string httpClient *http.Client @@ -393,6 +404,52 @@ 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. + + var dispatchRunDetailsSupported bool + + if !ghauth.IsEnterprise(d.host) { + dispatchRunDetailsSupported = true + } else { + 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) { + dispatchRunDetailsSupported = true + } + } + + return ActionsFeatures{ + DispatchRunDetails: dispatchRunDetailsSupported, + }, nil +} + func resolveEnterpriseVersion(httpClient *http.Client, host string) (*version.Version, error) { var metaResponse struct { InstalledVersion string `json:"installed_version"` From 33825477aefe2dc0e27e05776451f2bec64e9146 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 11:54:07 +0000 Subject: [PATCH 02/15] test(featuredetection): add tests for `ActionsFeatures` Signed-off-by: Babak K. Shandiz --- internal/featuredetection/detector_mock.go | 10 +++ .../feature_detection_test.go | 68 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/internal/featuredetection/detector_mock.go b/internal/featuredetection/detector_mock.go index b0ca81f4063..a2dd8c2f883 100644 --- a/internal/featuredetection/detector_mock.go +++ b/internal/featuredetection/detector_mock.go @@ -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 diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go index d3ee1a7e944..b5d268f5b56 100644 --- a/internal/featuredetection/feature_detection_test.go +++ b/internal/featuredetection/feature_detection_test.go @@ -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 not 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) + }) + } +} From e73bf113dd72d16c5f4342d4e811da0d03d034f2 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 12:56:01 +0000 Subject: [PATCH 03/15] feat(workflow run): retrieve workflow run if supported by the API Signed-off-by: Babak K. Shandiz --- pkg/cmd/workflow/run/run.go | 61 ++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 2acd1d4ccb8..5f9a4476494 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -10,9 +10,11 @@ import ( "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 +27,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 +67,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 + + When running non-interactively, 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 +265,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,20 +313,48 @@ func runRun(opts *RunOptions) error { } } - path := fmt.Sprintf("repos/%s/actions/workflows/%d/dispatches", - ghrepo.FullName(repo), workflow.ID) + var returnRunDetailsSupported bool + if features, err := opts.Detector.ActionsFeatures(); err == nil { + // If there's an error detecting features, we assume the feature is not supported. + returnRunDetailsSupported = features.DispatchRunDetails + } + + path := fmt.Sprintf("repos/%s/actions/workflows/%d/dispatches", ghrepo.FullName(repo), workflow.ID) - requestByte, err := json.Marshal(map[string]interface{}{ + requestBody := map[string]interface{}{ "ref": ref, "inputs": providedInputs, - }) + } + + if returnRunDetailsSupported { + 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) } @@ -327,10 +365,23 @@ func runRun(opts *RunOptions) error { fmt.Fprintf(out, "%s Created workflow_dispatch event for %s at %s\n", cs.SuccessIcon(), cs.Cyan(workflow.Base()), cs.Bold(ref)) + if response.HtmlURL != "" { + fmt.Fprintln(out, response.HtmlURL) + } + fmt.Fprintln(out) + if response.WorkflowRunID != 0 { + fmt.Fprintf(out, "To see the created workflow run, try: %s\n", + cs.Boldf("gh run view %d", response.WorkflowRunID)) + } + fmt.Fprintf(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 From bf2ff7c39b78bdc3c7eccb042f5796f3cccbee5d Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 12:57:01 +0000 Subject: [PATCH 04/15] test(workflow run): verify retrieval of workflow run details Signed-off-by: Babak K. Shandiz --- pkg/cmd/workflow/run/run_test.go | 425 +++++++++++++++++++++++++++---- 1 file changed, 380 insertions(+), 45 deletions(-) diff --git a/pkg/cmd/workflow/run/run_test.go b/pkg/cmd/workflow/run/run_test.go index b121a573d49..a4e44e5dabd 100644 --- a/pkg/cmd/workflow/run/run_test.go +++ b/pkg/cmd/workflow/run/run_test.go @@ -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] From b64dd58d8ba99796e5e97c748109bc8a421fb827 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 13:06:23 +0000 Subject: [PATCH 05/15] test(featuredetection): fix test case name Signed-off-by: Babak K. Shandiz --- internal/featuredetection/feature_detection_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go index b5d268f5b56..f4eeb5962fa 100644 --- a/internal/featuredetection/feature_detection_test.go +++ b/internal/featuredetection/feature_detection_test.go @@ -732,7 +732,7 @@ func TestActionsFeatures(t *testing.T) { }, }, { - name: "GHE 3.21, workflow dispatch run details not supported", + name: "GHE 3.21, workflow dispatch run details supported", hostname: "git.my.org", httpStubs: func(reg *httpmock.Registry) { reg.Register( From 61ab5e0b5db4ef6af0ffd6d7d0b09a8aacb1859c Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 13:07:24 +0000 Subject: [PATCH 06/15] docs(featuredetection): fix typo in comment Signed-off-by: Babak K. Shandiz --- internal/featuredetection/feature_detection.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index 8100882fbb5..2f7b6255aa4 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -104,8 +104,8 @@ type ActionsFeatures struct { // 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 now - // supported, and setting it will cause error. + // 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 } From 3e9fbbb2fa74cfd9396000e5c80689a6ad079f1d Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 21:53:17 +0000 Subject: [PATCH 07/15] refactor(workflow run): remove temp `out` Signed-off-by: Babak K. Shandiz --- pkg/cmd/workflow/run/run.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 5f9a4476494..0484b3c06d9 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -360,23 +360,22 @@ func runRun(opts *RunOptions) error { } 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)) if response.HtmlURL != "" { - fmt.Fprintln(out, response.HtmlURL) + fmt.Fprintln(opts.IO.Out, response.HtmlURL) } - fmt.Fprintln(out) + fmt.Fprintln(opts.IO.Out) if response.WorkflowRunID != 0 { - fmt.Fprintf(out, "To see the created workflow run, try: %s\n", + fmt.Fprintf(opts.IO.Out, "To see the created workflow run, try: %s\n", cs.Boldf("gh run view %d", response.WorkflowRunID)) } - fmt.Fprintf(out, "To see runs for this workflow, try: %s\n", + 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 != "" { From 36a85fd71febf4c96c0b1729c6f51311dbc1e198 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 21:55:25 +0000 Subject: [PATCH 08/15] fix(workflow run): apply `url.PathEscape` when compiling URL Signed-off-by: Babak K. Shandiz --- pkg/cmd/workflow/run/run.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 0484b3c06d9..294e403d421 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "reflect" "sort" "strings" @@ -319,7 +320,7 @@ func runRun(opts *RunOptions) error { returnRunDetailsSupported = features.DispatchRunDetails } - path := fmt.Sprintf("repos/%s/actions/workflows/%d/dispatches", ghrepo.FullName(repo), workflow.ID) + 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, From c47a10e58364d65d33297fb489ad7e8ae40fe9e4 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 22:02:18 +0000 Subject: [PATCH 09/15] docs(workflow run): add cleanup marker with explanation on future changes Signed-off-by: Babak K. Shandiz --- pkg/cmd/workflow/run/run.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 294e403d421..1d4844cdb9b 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -327,6 +327,10 @@ func runRun(opts *RunOptions) error { "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 returnRunDetailsSupported { requestBody["return_run_details"] = true } From ce016217d0511573848ba91d262c1ad6dbadd624 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 16 Feb 2026 22:49:42 +0000 Subject: [PATCH 10/15] docs(workflow run): improve help docs Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/workflow/run/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 1d4844cdb9b..ee78ed4cfcc 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -69,7 +69,7 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command - Via %[1]s-f/--raw-field%[1]s or %[1]s-F/--field%[1]s flags - As JSON, via standard input - When running non-interactively, the created workflow run URL will be returned if available. + 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 From 52eca968733ccbff720483a375bff2367ba6bad4 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 17 Feb 2026 11:46:27 +0000 Subject: [PATCH 11/15] refactor(featuredetection): remove temp in favour of early returns Signed-off-by: Babak K. Shandiz --- .../featuredetection/feature_detection.go | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index 2f7b6255aa4..b2fb6d65dea 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -425,28 +425,30 @@ func (d *detector) ActionsFeatures() (ActionsFeatures, error) { // going to be ignored/removed. So, once we are migrating to the new API version we should double check the status // of the API. - var dispatchRunDetailsSupported bool - if !ghauth.IsEnterprise(d.host) { - dispatchRunDetailsSupported = true - } else { - minSupportedVersion, err := version.NewVersion(enterpriseWorkflowDispatchRunDetailsSupport) - if err != nil { - return ActionsFeatures{}, err - } + return ActionsFeatures{ + DispatchRunDetails: true, + }, nil + } - hostVersion, err := resolveEnterpriseVersion(d.httpClient, d.host) - if err != nil { - return ActionsFeatures{}, err - } + minSupportedVersion, err := version.NewVersion(enterpriseWorkflowDispatchRunDetailsSupport) + if err != nil { + return ActionsFeatures{}, err + } - if hostVersion.GreaterThanOrEqual(minSupportedVersion) { - dispatchRunDetailsSupported = true - } + 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: dispatchRunDetailsSupported, + DispatchRunDetails: false, }, nil } From 31f3756089d177c0f2cd58faba6b74ebf960fc12 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 17 Feb 2026 11:48:22 +0000 Subject: [PATCH 12/15] fix(workflow run): bail out on feature detection error Signed-off-by: Babak K. Shandiz --- pkg/cmd/workflow/run/run.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index ee78ed4cfcc..386227bf16b 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -314,10 +314,9 @@ func runRun(opts *RunOptions) error { } } - var returnRunDetailsSupported bool - if features, err := opts.Detector.ActionsFeatures(); err == nil { - // If there's an error detecting features, we assume the feature is not supported. - returnRunDetailsSupported = features.DispatchRunDetails + features, err := opts.Detector.ActionsFeatures() + if err != nil { + return err } path := fmt.Sprintf("repos/%s/%s/actions/workflows/%d/dispatches", url.PathEscape(repo.RepoOwner()), url.PathEscape(repo.RepoName()), workflow.ID) @@ -331,7 +330,7 @@ func runRun(opts *RunOptions) error { // 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 returnRunDetailsSupported { + if features.DispatchRunDetails { requestBody["return_run_details"] = true } From e90343db35995b3155700589e0e4efca2487c9b0 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:57:52 -0700 Subject: [PATCH 13/15] Migrate PR triage workflows to shared workflows Replace prauto.yml and pr-help-wanted.yml with a single triage-pull-requests.yml that calls shared reusable workflows from desktop/gh-cli-and-desktop-shared-workflows: - triage-label-external-pr: labels external PRs with external,needs-triage - triage-close-from-default-branch: closes PRs opened from trunk - triage-pr-requirements: enforces body length + help-wanted issue linkage - triage-close-no-help-wanted: closes PRs labeled no-help-wanted-issue - triage-ready-for-review: removes needs-triage on ready-for-review label Also adds a daily schedule to auto-close PRs with unmet requirements after 7 days. Deletes: - prauto.yml - pr-help-wanted.yml - scripts/check-help-wanted.sh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-help-wanted.yml | 46 -------- .github/workflows/prauto.yml | 75 ------------- .../workflows/scripts/check-help-wanted.sh | 105 ------------------ .github/workflows/triage-pull-requests.yml | 59 ++++++++++ 4 files changed, 59 insertions(+), 226 deletions(-) delete mode 100644 .github/workflows/pr-help-wanted.yml delete mode 100644 .github/workflows/prauto.yml delete mode 100755 .github/workflows/scripts/check-help-wanted.sh create mode 100644 .github/workflows/triage-pull-requests.yml diff --git a/.github/workflows/pr-help-wanted.yml b/.github/workflows/pr-help-wanted.yml deleted file mode 100644 index b63d025bc6b..00000000000 --- a/.github/workflows/pr-help-wanted.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: PR Help Wanted Check -on: - pull_request_target: - types: [opened] - workflow_dispatch: - inputs: - pr_number: - description: "Pull Request number to check" - required: true - type: string - -permissions: - contents: none - issues: read - pull-requests: write - -jobs: - check-help-wanted: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Set PR variables for workflow_dispatch event - id: pr-vars-dispatch - if: github.event_name == 'workflow_dispatch' - env: - PR_NUMBER: ${{ github.event.inputs.pr_number }} - run: | - # We only need to construct the PR URL from the dispatch event input. - echo "pr_url=https://github.com/cli/cli/pull/${PR_NUMBER}" >> $GITHUB_OUTPUT - - - name: Check for issues without help-wanted label - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # These variables are optionally used in the check-help-wanted.sh - # script for additional checks; but they are not strictly necessary - # for the script to run. This is why we are okay with them being - # empty when the event is workflow_dispatch. - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} - PR_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} - PR_URL: ${{ github.event.pull_request.html_url || steps.pr-vars-dispatch.outputs.pr_url }} - run: | - # Run the script to check for issues without help-wanted label - bash .github/workflows/scripts/check-help-wanted.sh "${PR_URL}" diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml deleted file mode 100644 index 40dfee8465a..00000000000 --- a/.github/workflows/prauto.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: PR Automation -on: - pull_request_target: - types: [ready_for_review, opened, reopened] - -permissions: - contents: none - issues: write - pull-requests: write - -jobs: - pr-auto: - runs-on: ubuntu-latest - environment: cli-automation - steps: - - name: lint pr - env: - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} - PRBODY: ${{ github.event.pull_request.body }} - PRNUM: ${{ github.event.pull_request.number }} - PRHEAD: ${{ github.event.pull_request.head.label }} - PRAUTHOR: ${{ github.event.pull_request.user.login }} - PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} - if: "!github.event.pull_request.draft" - run: | - commentPR () { - gh pr comment $PRNUM -b "${1}" - } - - closePR () { - gh pr close $PRNUM - } - - colID () { - gh api graphql -f query='query($owner:String!, $repo:String!) { - repository(owner:$owner, name:$repo) { - project(number:1) { - columns(first:10) { nodes {id,name} } - } - } - }' -f owner="${GH_REPO%/*}" -f repo="${GH_REPO#*/}" \ - -q ".data.repository.project.columns.nodes[] | select(.name | startswith(\"$1\")) | .id" - } - - if [ "$PR_AUTHOR_TYPE" = "Bot" ] || gh api orgs/cli/public_members/$PRAUTHOR --silent 2>/dev/null - then - if [ "$PR_AUTHOR_TYPE" != "Bot" ] - then - gh pr edit $PRNUM --add-assignee $PRAUTHOR - fi - exit 0 - fi - - gh pr edit $PRNUM --add-label "external" - - if [ "$PRHEAD" = "cli:trunk" ] - then - closePR - exit 0 - fi - - if [ $(wc -c <<<"$PRBODY") -lt 10 ] - then - commentPR "Thanks for the pull request! We're a small team and it's helpful to have context around community submissions in order to review them appropriately. Our automation has closed this pull request since it does not have an adequate description. Please edit the body of this pull request to describe what this does, then reopen it." - closePR - exit 0 - fi - - if ! grep -Eq '(#|issues/)[0-9]+' <<<"$PRBODY" - then - commentPR "Hi! Thanks for the pull request. Please ensure that this change is linked to an issue by mentioning an issue number in the description of the pull request. If this pull request would close the issue, please put the word 'Fixes' before the issue number somewhere in the pull request body. If this is a tiny change like fixing a typo, feel free to ignore this message." - fi - - exit 0 diff --git a/.github/workflows/scripts/check-help-wanted.sh b/.github/workflows/scripts/check-help-wanted.sh deleted file mode 100755 index d713be14440..00000000000 --- a/.github/workflows/scripts/check-help-wanted.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash - -set -e - -PR_URL="$1" - -if [ -z "$PR_URL" ]; then - echo "Usage: $0 " - echo "" - echo "Check if the PR references any non-help-wanted issues and, if so, comment" - echo "on it explaining why the team might close/dismiss it." - exit 1 -fi - -# Skip if PR is from a bot or org member -if [ "$PR_AUTHOR_TYPE" = "Bot" ] || [ "$PR_AUTHOR_ASSOCIATION" = "MEMBER" ] || [ "$PR_AUTHOR_ASSOCIATION" = "OWNER" ]; then - echo "Skipping check for PR $PR_URL as it is from a bot ($PR_AUTHOR_TYPE) or an org member ($PR_AUTHOR_ASSOCIATION: MEMBER/OWNER)" - exit 0 -fi - -# Skip if PR is a draft -if [ "$(gh pr view "${PR_URL}" --json isDraft --jq '.isDraft')" != "false" ]; then - echo "Skipping check for PR $PR_URL as it is a draft" - exit 0 -fi - -# Extract PR number from URL for logging -PR_NUM="$(basename "$PR_URL")" - -# Extract cli/cli closing issues references from PR -CLOSING_ISSUES="$(gh pr view "$PR_URL" --json closingIssuesReferences --jq '.closingIssuesReferences[] | select(.repository.name == "cli" and .repository.owner.login == "cli") | .number')" - -if [ -z "$CLOSING_ISSUES" ]; then - echo "No closing issues found for PR #$PR_NUM" - exit 0 -fi - -# Check each closing issue for 'help-wanted' label -ISSUES_WITHOUT_HELP_WANTED=() - -for issue_num in $CLOSING_ISSUES; do - echo "Checking issue #$issue_num for 'help wanted' label..." - - # Get issue labels - LABELS=$(gh issue view "$issue_num" --json labels --jq '.labels[].name') - - # Skip if the issue has the gh-attestion or gh-codespace label - # This is because the codeowners for these commands may not be public - # cli org members, and so unless we authenticate with a PAT, we can't - # know who is an external contributor or not. - # So we skip these issues to avoid falsely writing a comment - # on each PR opened by these codeowners. - if echo "$LABELS" | grep -q -e "gh-attestation" -e "gh-codespace"; then - echo "Issue #$issue_num is skipped due to labels" - continue - fi - - # Check if 'help wanted' label exists - if ! echo "$LABELS" | grep -qE '^help wanted$'; then - ISSUES_WITHOUT_HELP_WANTED+=("$issue_num") - echo "Issue #$issue_num does not have 'help wanted' label" - else - echo "Issue #$issue_num has 'help wanted' label" - fi -done - -# If we found issues without 'help wanted' label, post a comment -if [ ${#ISSUES_WITHOUT_HELP_WANTED[@]} -gt 0 ]; then - echo "Found ${#ISSUES_WITHOUT_HELP_WANTED[@]} issues without 'help wanted' label" - - # Build issue list for comment - ISSUE_LIST="" - for issue_num in "${ISSUES_WITHOUT_HELP_WANTED[@]}"; do - ISSUE_LIST="$ISSUE_LIST- #$issue_num"$'\n' - done - - # Create comment message - gh pr comment "$PR_URL" --body-file - <- + github.event_name == 'pull_request_target' && + (github.event.action == 'opened' || github.event.action == 'reopened') + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-label-external-pr.yml@main + permissions: + issues: write + pull-requests: write + repository-projects: read + + close-from-default-branch: + if: >- + github.event_name == 'pull_request_target' && + github.event.action == 'opened' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-from-default-branch.yml@main + with: + default_branch: trunk + permissions: + pull-requests: write + + check-requirements: + if: >- + github.event_name == 'pull_request_target' && + (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'edited') + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-pr-requirements.yml@main + permissions: + issues: read + pull-requests: write + + close-unmet-requirements: + if: github.event_name == 'schedule' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-pr-requirements.yml@main + permissions: + issues: read + pull-requests: write + + close-no-help-wanted: + if: >- + github.event_name == 'pull_request_target' && + github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-no-help-wanted.yml@main + permissions: + pull-requests: write + + ready-for-review: + if: >- + github.event_name == 'pull_request_target' && + github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-ready-for-review.yml@main + permissions: + pull-requests: write From 5f66c0e8b7f66c71028280c869eaff49473e8327 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:07:02 -0700 Subject: [PATCH 14/15] Remove feedback issue template Delete the .github/ISSUE_TEMPLATE/feedback.md file --- .github/ISSUE_TEMPLATE/feedback.md | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feedback.md diff --git a/.github/ISSUE_TEMPLATE/feedback.md b/.github/ISSUE_TEMPLATE/feedback.md deleted file mode 100644 index 837c36632a5..00000000000 --- a/.github/ISSUE_TEMPLATE/feedback.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: "\U0001F4E3 Feedback" -about: Give us general feedback about the GitHub CLI -title: '' -labels: feedback -assignees: '' - ---- - -# CLI Feedback - -You can use this template to give us structured feedback or just wipe it and leave us a note. Thank you! - -## What have you loved? - -_eg "the nice colors"_ - -## What was confusing or gave you pause? - -_eg "it did something unexpected"_ - -## Are there features you'd like to see added? - -_eg "gh cli needs mini-games"_ - -## Anything else? - -_eg "have a nice day"_ From 3af535902d4f23e267229e07fd4c1a2cb23c5b0e Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:38:15 -0700 Subject: [PATCH 15/15] Add usage examples to gh gist edit command Add a cobra Example string to the gist edit command showing common operations: interactive selection, editing in default editor, editing a specific file, replacing file content, adding/removing files, and changing the description. Closes #8943 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/gist/edit/edit.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index 1773a8f8312..6f00c906cbd 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -12,6 +12,7 @@ import ( "sort" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/prompter" @@ -58,6 +59,28 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman cmd := &cobra.Command{ Use: "edit { | } []", Short: "Edit one of your gists", + Example: heredoc.Doc(` + # Select a gist to edit interactively + $ gh gist edit + + # Edit a gist file in the default editor + $ gh gist edit 1234567890abcdef1234567890abcdef + + # Edit a specific file in the gist + $ gh gist edit 1234567890abcdef1234567890abcdef --filename hello.py + + # Replace a gist file with content from a local file + $ gh gist edit 1234567890abcdef1234567890abcdef --filename hello.py hello.py + + # Add a new file to the gist + $ gh gist edit 1234567890abcdef1234567890abcdef --add newfile.py + + # Change the description of the gist + $ gh gist edit 1234567890abcdef1234567890abcdef --desc "new description" + + # Remove a file from the gist + $ gh gist edit 1234567890abcdef1234567890abcdef --remove hello.py + `), Args: func(cmd *cobra.Command, args []string) error { if len(args) > 2 { return cmdutil.FlagErrorf("too many arguments")