Repository: cli/cli (go)

Issue:
`gh secret` subcommands don't work anymore if called outside of Git repository
### Describe the bug

After upgrading to GitHub CLI `2.67.0` all `gh secret set` commands that are run outside of git repository fails with this message:

```
failed to run git: fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
```

Actions like [this](https://github.com/AnimMouse/setup-rclone/blob/main/update-config/action.yaml#L20) are now failing if they are run without using `actions/checkout`.

Related: #4688 #10209

Maybe related: #10352

### Affected version

`2.67.0` `2.66.1` `2.66.0`

### Steps to reproduce the behavior

1. Upgrade to GitHub CLI 2.67.0
2. Run `GH_REPO=AnimMouse/test-repo gh secret set test_secret`
3. See error:

```
failed to run git: fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
```

### Expected vs actual behavior

Earlier version of `gh secret` worked without checking out the repository.

```
GH_REPO=AnimMouse/test-repo gh secret set test_secret
? Paste your secret: 
```

Pull request:
Check `GH_REPO` too in addition to `--repo` for disambiguation
## Description

Fixes #10525

Commit pulled from @iamazeem's fork, with attribution maintained.

This is an alternative to https://github.com/cli/cli/pull/10535 because that one requires a bit more thought and I'd like this to go out in a release.

## Acceptance

**Given** my cwd is not a git repo
**And** I have `GH_REPO` set to a repo I have permissions on
**When** I run any of the `gh secret` commands
**Then** I am able to interact with secrets on that repo

Before:

```
➜  workspace GH_REPO=williammartin-test-org/test-repo gh secret list
failed to run git: fatal: not a git repository (or any of the parent directories): .git
```

After:

```
➜  workspace GH_REPO=williammartin-test-org/test-repo ~/workspace/cli/bin/gh secret list | cat
FOO     2025-01-09T16:39:07Z
```

Patch:
diff --git a/pkg/cmd/secret/delete/delete.go b/pkg/cmd/secret/delete/delete.go
index 84a5e7a83..1cbfbca9d 100644
--- a/pkg/cmd/secret/delete/delete.go
+++ b/pkg/cmd/secret/delete/delete.go
@@ -3,6 +3,7 @@ package delete
 import (
 	"fmt"
 	"net/http"
+	"os"
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
@@ -48,8 +49,9 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
 		RunE: func(cmd *cobra.Command, args []string) error {
 			// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
 			// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
-			opts.BaseRepo = f.BaseRepo
-			if !cmd.Flags().Changed("repo") {
+			if cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != "" {
+				opts.BaseRepo = f.BaseRepo
+			} else {
 				// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
 				// there might be multiple valid remotes.
 				opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)
diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go
index af3526386..c976bd60e 100644
--- a/pkg/cmd/secret/list/list.go
+++ b/pkg/cmd/secret/list/list.go
@@ -3,6 +3,7 @@ package list
 import (
 	"fmt"
 	"net/http"
+	"os"
 	"slices"
 	"strings"
 	"time"
@@ -69,8 +70,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
 		RunE: func(cmd *cobra.Command, args []string) error {
 			// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
 			// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
-			opts.BaseRepo = f.BaseRepo
-			if !cmd.Flags().Changed("repo") {
+			if cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != "" {
+				opts.BaseRepo = f.BaseRepo
+			} else {
 				// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
 				// there might be multiple valid remotes.
 				opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)
diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go
index 3c12b4192..89f11f1cf 100644
--- a/pkg/cmd/secret/set/set.go
+++ b/pkg/cmd/secret/set/set.go
@@ -106,8 +106,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
 		RunE: func(cmd *cobra.Command, args []string) error {
 			// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
 			// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
-			opts.BaseRepo = f.BaseRepo
-			if !cmd.Flags().Changed("repo") {
+			if cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != "" {
+				opts.BaseRepo = f.BaseRepo
+			} else {
 				// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
 				// there might be multiple valid remotes.
 				opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)


Test patch:
diff --git a/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar b/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar
index f4d1bbb4a..02dec06a0 100644
--- a/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar
+++ b/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar
@@ -34,3 +34,29 @@ stderr 'multiple remotes detected. please specify which repo to use by providing
 # Secret delete requires disambiguation
 ! exec gh secret delete 'TEST_SECRET_NAME'
 stderr 'multiple remotes detected. please specify which repo to use by providing the -R, --repo argument'
+
+# Move out of the fork repo to test whether secret commands work without local repository context
+cd ..
+
+# Secret set using --repo flag does not require disambiguation
+exec gh secret set 'TEST_SECRET_NAME' --body 'TEST_SECRET_VALUE' --repo ${ORG}/${REPO}-fork
+
+# Secret list using --repo flag does not require disambiguation
+exec gh secret list --repo ${ORG}/${REPO}-fork
+stdout 'TEST_SECRET_NAME'
+
+# Secret delete using --repo flag does not require disambiguation
+exec gh secret delete 'TEST_SECRET_NAME' --repo ${ORG}/${REPO}-fork
+
+# Setup GH_REPO for testing environment variable behavior
+env GH_REPO=${ORG}/${REPO}-fork
+
+# Secret set using GH_REPO env var does not require disambiguation
+exec gh secret set 'TEST_SECRET_NAME2' --body 'TEST_SECRET_VALUE2'
+
+# Secret list using GH_REPO env var does not require disambiguation
+exec gh secret list
+stdout 'TEST_SECRET_NAME2'
+
+# Secret delete using GH_REPO env var does not require disambiguation
+exec gh secret delete 'TEST_SECRET_NAME2'
diff --git a/pkg/cmd/secret/delete/delete_test.go b/pkg/cmd/secret/delete/delete_test.go
index 377879cf2..2f6cad431 100644
--- a/pkg/cmd/secret/delete/delete_test.go
+++ b/pkg/cmd/secret/delete/delete_test.go
@@ -144,6 +144,7 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
 	tests := []struct {
 		name          string
 		args          string
+		env           map[string]string
 		prompterStubs func(*prompter.MockPrompter)
 		wantRepo      ghrepo.Interface
 		wantErr       error
@@ -154,14 +155,22 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
 			wantRepo: ghrepo.New("owner", "repo"),
 		},
 		{
-			name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used",
+			name: "when GH_REPO env var is provided, the factory base repo func is used",
+			args: "SECRET_NAME",
+			env: map[string]string{
+				"GH_REPO": "owner/repo",
+			},
+			wantRepo: ghrepo.New("owner", "repo"),
+		},
+		{
+			name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
 			args: "SECRET_NAME",
 			wantErr: shared.AmbiguousBaseRepoError{
 				Remotes: remotes,
 			},
 		},
 		{
-			name: "when there is no repo flag provided, and can prompt, the base func resolving ambiguity is used",
+			name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
 			args: "SECRET_NAME",
 			prompterStubs: func(pm *prompter.MockPrompter) {
 				pm.RegisterSelect(
@@ -199,6 +208,10 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
 				},
 			}
 
+			for k, v := range tt.env {
+				t.Setenv(k, v)
+			}
+
 			argv, err := shlex.Split(tt.args)
 			assert.NoError(t, err)
 
diff --git a/pkg/cmd/secret/list/list_test.go b/pkg/cmd/secret/list/list_test.go
index 601de2572..f4ecde73e 100644
--- a/pkg/cmd/secret/list/list_test.go
+++ b/pkg/cmd/secret/list/list_test.go
@@ -124,6 +124,7 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) {
 	tests := []struct {
 		name          string
 		args          string
+		env           map[string]string
 		prompterStubs func(*prompter.MockPrompter)
 		wantRepo      ghrepo.Interface
 		wantErr       error
@@ -134,14 +135,21 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) {
 			wantRepo: ghrepo.New("owner", "repo"),
 		},
 		{
-			name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used",
+			name: "when GH_REPO env var is provided, the factory base repo func is used",
+			env: map[string]string{
+				"GH_REPO": "owner/repo",
+			},
+			wantRepo: ghrepo.New("owner", "repo"),
+		},
+		{
+			name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
 			args: "",
 			wantErr: shared.AmbiguousBaseRepoError{
 				Remotes: remotes,
 			},
 		},
 		{
-			name: "when there is no repo flag provided, and can prompt, the base func resolving ambiguity is used",
+			name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
 			args: "",
 			prompterStubs: func(pm *prompter.MockPrompter) {
 				pm.RegisterSelect(
@@ -179,6 +187,10 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) {
 				},
 			}
 
+			for k, v := range tt.env {
+				t.Setenv(k, v)
+			}
+
 			argv, err := shlex.Split(tt.args)
 			assert.NoError(t, err)
 
diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go
index 99c8c6cbe..22c7aaab8 100644
--- a/pkg/cmd/secret/set/set_test.go
+++ b/pkg/cmd/secret/set/set_test.go
@@ -243,6 +243,7 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
 	tests := []struct {
 		name          string
 		args          string
+		env           map[string]string
 		prompterStubs func(*prompter.MockPrompter)
 		wantRepo      ghrepo.Interface
 		wantErr       error
@@ -253,14 +254,22 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
 			wantRepo: ghrepo.New("owner", "repo"),
 		},
 		{
-			name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used",
+			name: "when GH_REPO env var is provided, the factory base repo func is used",
+			args: "SECRET_NAME",
+			env: map[string]string{
+				"GH_REPO": "owner/repo",
+			},
+			wantRepo: ghrepo.New("owner", "repo"),
+		},
+		{
+			name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
 			args: "SECRET_NAME",
 			wantErr: shared.AmbiguousBaseRepoError{
 				Remotes: remotes,
 			},
 		},
 		{
-			name: "when there is no repo flag provided, and can prompt, the base func resolving ambiguity is used",
+			name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
 			args: "SECRET_NAME",
 			prompterStubs: func(pm *prompter.MockPrompter) {
 				pm.RegisterSelect(
@@ -298,6 +307,10 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
 				},
 			}
 
+			for k, v := range tt.env {
+				t.Setenv(k, v)
+			}
+
 			argv, err := shlex.Split(tt.args)
 			assert.NoError(t, err)