Merge pull request #3833 from cristiand391/gh-run-cancel
Add `run cancel` command
This commit is contained in:
commit
06c06c87dc
3 changed files with 356 additions and 0 deletions
136
pkg/cmd/run/cancel/cancel.go
Normal file
136
pkg/cmd/run/cancel/cancel.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package cancel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/run/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CancelOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
Prompt bool
|
||||
|
||||
RunID string
|
||||
}
|
||||
|
||||
func NewCmdCancel(f *cmdutil.Factory, runF func(*CancelOptions) error) *cobra.Command {
|
||||
opts := &CancelOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "cancel [<run-id>]",
|
||||
Short: "Cancel a workflow run",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.RunID = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")}
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return runCancel(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCancel(opts *CancelOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create http client: %w", err)
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine base repo: %w", err)
|
||||
}
|
||||
|
||||
runID := opts.RunID
|
||||
var run *shared.Run
|
||||
|
||||
if opts.Prompt {
|
||||
runs, err := shared.GetRunsWithFilter(client, repo, 10, func(run shared.Run) bool {
|
||||
return run.Status != shared.Completed
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get runs: %w", err)
|
||||
}
|
||||
if len(runs) == 0 {
|
||||
return fmt.Errorf("found no in progress runs to cancel")
|
||||
}
|
||||
runID, err = shared.PromptForRun(cs, runs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO silly stopgap until dust settles and PromptForRun can just return a run
|
||||
for _, r := range runs {
|
||||
if fmt.Sprintf("%d", r.ID) == runID {
|
||||
run = &r
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
run, err = shared.GetRun(client, repo, runID)
|
||||
if err != nil {
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
if httpErr.StatusCode == http.StatusNotFound {
|
||||
err = fmt.Errorf("Could not find any workflow run with ID %s", opts.RunID)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = cancelWorkflowRun(client, repo, fmt.Sprintf("%d", run.ID))
|
||||
if err != nil {
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
if httpErr.StatusCode == http.StatusConflict {
|
||||
err = fmt.Errorf("Cannot cancel a workflow run that is completed")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s Request to cancel workflow submitted.\n", cs.SuccessIcon())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelWorkflowRun(client *api.Client, repo ghrepo.Interface, runID string) error {
|
||||
path := fmt.Sprintf("repos/%s/actions/runs/%s/cancel", ghrepo.FullName(repo), runID)
|
||||
|
||||
err := client.REST(repo.RepoHost(), "POST", path, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
218
pkg/cmd/run/cancel/cancel_test.go
Normal file
218
pkg/cmd/run/cancel/cancel_test.go
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
package cancel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/run/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdCancel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
tty bool
|
||||
wants CancelOptions
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "blank tty",
|
||||
tty: true,
|
||||
wants: CancelOptions{
|
||||
Prompt: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "blank nontty",
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "with arg",
|
||||
cli: "1234",
|
||||
wants: CancelOptions{
|
||||
RunID: "1234",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, _, _, _ := iostreams.Test()
|
||||
io.SetStdinTTY(tt.tty)
|
||||
io.SetStdoutTTY(tt.tty)
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *CancelOptions
|
||||
cmd := NewCmdCancel(f, func(opts *CancelOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.RunID, gotOpts.RunID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCancel(t *testing.T) {
|
||||
inProgressRun := shared.TestRun("more runs", 1234, shared.InProgress, "")
|
||||
completedRun := shared.TestRun("more runs", 4567, shared.Completed, shared.Failure)
|
||||
tests := []struct {
|
||||
name string
|
||||
httpStubs func(*httpmock.Registry)
|
||||
askStubs func(*prompt.AskStubber)
|
||||
opts *CancelOptions
|
||||
wantErr bool
|
||||
wantOut string
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "cancel run",
|
||||
opts: &CancelOptions{
|
||||
RunID: "1234",
|
||||
},
|
||||
wantErr: false,
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
|
||||
httpmock.JSONResponse(inProgressRun))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/cancel"),
|
||||
httpmock.StatusStringResponse(202, "{}"))
|
||||
},
|
||||
wantOut: "✓ Request to cancel workflow submitted.\n",
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
opts: &CancelOptions{
|
||||
RunID: "1234",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "Could not find any workflow run with ID 1234",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
|
||||
httpmock.StatusStringResponse(404, ""))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "completed",
|
||||
opts: &CancelOptions{
|
||||
RunID: "4567",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "Cannot cancel a workflow run that is completed",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/4567"),
|
||||
httpmock.JSONResponse(completedRun))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/4567/cancel"),
|
||||
httpmock.StatusStringResponse(409, ""),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompt, no in progress runs",
|
||||
opts: &CancelOptions{
|
||||
Prompt: true,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "found no in progress runs to cancel",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
|
||||
httpmock.JSONResponse(shared.RunsPayload{
|
||||
WorkflowRuns: []shared.Run{
|
||||
completedRun,
|
||||
},
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompt, cancel",
|
||||
opts: &CancelOptions{
|
||||
Prompt: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
|
||||
httpmock.JSONResponse(shared.RunsPayload{
|
||||
WorkflowRuns: []shared.Run{
|
||||
inProgressRun,
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/cancel"),
|
||||
httpmock.StatusStringResponse(202, "{}"))
|
||||
},
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubOne(0)
|
||||
},
|
||||
wantOut: "✓ Request to cancel workflow submitted.\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
tt.httpStubs(reg)
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
io, _, stdout, _ := iostreams.Test()
|
||||
io.SetStdoutTTY(true)
|
||||
io.SetStdinTTY(true)
|
||||
tt.opts.IO = io
|
||||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
}
|
||||
|
||||
as, teardown := prompt.InitAskStubber()
|
||||
defer teardown()
|
||||
if tt.askStubs != nil {
|
||||
tt.askStubs(as)
|
||||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := runCancel(tt.opts)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Equal(t, tt.errMsg, err.Error())
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantOut, stdout.String())
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package run
|
||||
|
||||
import (
|
||||
cmdCancel "github.com/cli/cli/v2/pkg/cmd/run/cancel"
|
||||
cmdDownload "github.com/cli/cli/v2/pkg/cmd/run/download"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/run/list"
|
||||
cmdRerun "github.com/cli/cli/v2/pkg/cmd/run/rerun"
|
||||
|
|
@ -26,6 +27,7 @@ func NewCmdRun(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdRerun.NewCmdRerun(f, nil))
|
||||
cmd.AddCommand(cmdDownload.NewCmdDownload(f, nil))
|
||||
cmd.AddCommand(cmdWatch.NewCmdWatch(f, nil))
|
||||
cmd.AddCommand(cmdCancel.NewCmdCancel(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue