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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions internal/featuredetection/detector_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions internal/featuredetection/feature_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Detector interface {
ProjectsV1() gh.ProjectsV1Support
SearchFeatures() (SearchFeatures, error)
ReleaseFeatures() (ReleaseFeatures, error)
ActionsFeatures() (ActionsFeatures, error)
}

type IssueFeatures struct {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand Down
68 changes: 68 additions & 0 deletions internal/featuredetection/feature_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
23 changes: 23 additions & 0 deletions pkg/cmd/gist/edit/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -58,6 +59,28 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
cmd := &cobra.Command{
Use: "edit {<id> | <url>} [<filename>]",
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")
Expand Down
72 changes: 63 additions & 9 deletions pkg/cmd/workflow/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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
}

path := fmt.Sprintf("repos/%s/%s/actions/workflows/%d/dispatches", url.PathEscape(repo.RepoOwner()), url.PathEscape(repo.RepoName()), workflow.ID)

requestByte, err := json.Marshal(map[string]interface{}{
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
Expand Down
Loading
Loading