From fa8b514bf180263a8e2a176c8723a017918bc7c8 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 21 Jun 2023 15:47:33 +0200 Subject: [PATCH] Early exit repo sync if merge-upstream requires workflow scope --- pkg/cmd/repo/sync/http.go | 7 +++++++ pkg/cmd/repo/sync/sync_test.go | 18 ++++++++++++++++++ pkg/httpmock/stub.go | 21 +++++++++++++++++++-- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/repo/sync/http.go b/pkg/cmd/repo/sync/http.go index d5cc0e282..61a1c3317 100644 --- a/pkg/cmd/repo/sync/http.go +++ b/pkg/cmd/repo/sync/http.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" @@ -33,6 +34,9 @@ type upstreamMergeErr struct{ error } var upstreamMergeUnavailableErr = upstreamMergeErr{errors.New("upstream merge API is unavailable")} +var missingWorkflowScopeRE = regexp.MustCompile("refusing to allow.*without `workflow` scope") +var missingWorkflowScopeErr = errors.New("Upstream commits contain workflow changes, which require the `workflow` scope to merge. To request it, run: gh auth refresh -s workflow") + func triggerUpstreamMerge(client *api.Client, repo ghrepo.Interface, branch string) (string, error) { var payload bytes.Buffer if err := json.NewEncoder(&payload).Encode(map[string]interface{}{ @@ -52,6 +56,9 @@ func triggerUpstreamMerge(client *api.Client, repo ghrepo.Interface, branch stri if errors.As(err, &httpErr) { switch httpErr.StatusCode { case http.StatusUnprocessableEntity, http.StatusConflict: + if missingWorkflowScopeRE.MatchString(httpErr.Message) { + return "", missingWorkflowScopeErr + } return "", upstreamMergeErr{errors.New(httpErr.Message)} case http.StatusNotFound: return "", upstreamMergeUnavailableErr diff --git a/pkg/cmd/repo/sync/sync_test.go b/pkg/cmd/repo/sync/sync_test.go index 90a216607..a532f3992 100644 --- a/pkg/cmd/repo/sync/sync_test.go +++ b/pkg/cmd/repo/sync/sync_test.go @@ -457,6 +457,24 @@ func Test_SyncRun(t *testing.T) { wantErr: true, errMsg: "trunk branch does not exist on OWNER/REPO-FORK repository", }, + { + name: "sync remote fork with missing workflow scope on token", + opts: &SyncOptions{ + DestArg: "FORKOWNER/REPO-FORK", + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(`{"data":{"repository":{"defaultBranchRef":{"name": "trunk"}}}}`)) + reg.Register( + httpmock.REST("POST", "repos/FORKOWNER/REPO-FORK/merge-upstream"), + httpmock.StatusJSONResponse(422, struct { + Message string `json:"message"` + }{Message: "refusing to allow an OAuth App to create or update workflow `.github/workflows/unimportant.yml` without `workflow` scope"})) + }, + wantErr: true, + errMsg: "Upstream commits contain workflow changes, which require the `workflow` scope to merge. To request it, run: gh auth refresh -s workflow", + }, } for _, tt := range tests { reg := &httpmock.Registry{} diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index b26d433fa..9332ac9dd 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -143,7 +143,20 @@ func StatusStringResponse(status int, body string) Responder { func JSONResponse(body interface{}) Responder { return func(req *http.Request) (*http.Response, error) { b, _ := json.Marshal(body) - return httpResponse(200, req, bytes.NewBuffer(b)), nil + header := http.Header{ + "Content-Type": []string{"application/json"}, + } + return httpResponseWithHeader(200, req, bytes.NewBuffer(b), header), nil + } +} + +func StatusJSONResponse(status int, body interface{}) Responder { + return func(req *http.Request) (*http.Response, error) { + b, _ := json.Marshal(body) + header := http.Header{ + "Content-Type": []string{"application/json"}, + } + return httpResponseWithHeader(status, req, bytes.NewBuffer(b), header), nil } } @@ -215,10 +228,14 @@ func ScopesResponder(scopes string) func(*http.Request) (*http.Response, error) } func httpResponse(status int, req *http.Request, body io.Reader) *http.Response { + return httpResponseWithHeader(status, req, body, http.Header{}) +} + +func httpResponseWithHeader(status int, req *http.Request, body io.Reader, header http.Header) *http.Response { return &http.Response{ StatusCode: status, Request: req, Body: io.NopCloser(body), - Header: http.Header{}, + Header: header, } }