Repository: cli/cli
Pull request #10574 (2025-03-10 13:20:07)

[gh repo sync] Make missing workflow regexp aware of GitHub App
Fixes https://github.com/cli/cli/issues/10573
Follow up of https://github.com/cli/cli/pull/7612

The `missingWorkflowScopeRE` is defined to capture the error message when the `GH_TOKEN` does not have `workflow` scope in `gh repo sync <remote>`, but this is only intended for error messages for OAuth Apps and does not work with GitHub Apps.

In GitHub App, you will get the following error:

```
{
  "message": "refusing to allow a GitHub App to create or update workflow `.github/workflows/teamcity-pr-checks.yml` without `workflows` permission",
  "documentation_url": "https://docs.github.com/rest/branches/branches#sync-a-fork-branch-with-the-upstream-repository",
  "status": "422"
}
```

As you can see above, the existing regexp does not match the "`workflows` permission".

This change modifies the regexp to return the user-friendly error message when the `workflow` permission is missing, even in the case of a GitHub App.


jtmcg: Thanks for the fast turnaround on this, @wata727! I'm looking into testing this locally before giving it the 👍 

Patch:
diff --git a/pkg/cmd/repo/sync/http.go b/pkg/cmd/repo/sync/http.go
index 3e193daeb..27e9a6351 100644
--- a/pkg/cmd/repo/sync/http.go
+++ b/pkg/cmd/repo/sync/http.go
@@ -32,8 +32,8 @@ func latestCommit(client *api.Client, repo ghrepo.Interface, branch string) (com
 
 type upstreamMergeErr struct{ error }
 
-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")
+var missingWorkflowScopeRE = regexp.MustCompile("refusing to allow.*without `workflow(s)?` (scope|permission)")
+var missingWorkflowScopeErr = errors.New("Upstream commits contain workflow changes, which require the `workflow` scope or permission 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


Test patch:
diff --git a/pkg/cmd/repo/sync/sync_test.go b/pkg/cmd/repo/sync/sync_test.go
index 227e2a6a2..60e6ae392 100644
--- a/pkg/cmd/repo/sync/sync_test.go
+++ b/pkg/cmd/repo/sync/sync_test.go
@@ -470,7 +470,7 @@ func Test_SyncRun(t *testing.T) {
 			errMsg:  "trunk branch does not exist on OWNER/REPO-FORK repository",
 		},
 		{
-			name: "sync remote fork with missing workflow scope on token",
+			name: "sync remote fork with missing workflow scope on OAuth App token",
 			opts: &SyncOptions{
 				DestArg: "FORKOWNER/REPO-FORK",
 			},
@@ -485,7 +485,43 @@ func Test_SyncRun(t *testing.T) {
 					}{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",
+			errMsg:  "Upstream commits contain workflow changes, which require the `workflow` scope or permission to merge. To request it, run: gh auth refresh -s workflow",
+		},
+		{
+			name: "sync remote fork with missing workflow permission on GitHub App 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 a GitHub App to create or update workflow `.github/workflows/unimportant.yml` without `workflows` permission"}))
+			},
+			wantErr: true,
+			errMsg:  "Upstream commits contain workflow changes, which require the `workflow` scope or permission to merge. To request it, run: gh auth refresh -s workflow",
+		},
+		{
+			name: "sync remote fork with missing workflow scope on personal access 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 a Personal Access Token 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 or permission to merge. To request it, run: gh auth refresh -s workflow",
 		},
 	}
 	for _, tt := range tests {