From c5fc7e3ffbcb1c9e56a2eff5a0f9a9caaa004422 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Tue, 19 Aug 2025 22:37:25 +0200 Subject: [PATCH 01/87] Fix missing assertions --- pkg/cmd/auth/status/status_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 3f16baf46..8919dff47 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -78,6 +78,8 @@ func Test_NewCmdStatus(t *testing.T) { assert.NoError(t, err) assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) + assert.Equal(t, tt.wants.ShowToken, gotOpts.ShowToken) + assert.Equal(t, tt.wants.Active, gotOpts.Active) }) } } From 0091384495494a0004f237e2f965bb169c1cef1f Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Tue, 19 Aug 2025 22:49:32 +0200 Subject: [PATCH 02/87] Refactor entry to a single type --- pkg/cmd/auth/status/status.go | 206 ++++++++++++++-------------------- 1 file changed, 86 insertions(+), 120 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 73ee084e9..84bb00a35 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -19,105 +19,84 @@ import ( "github.com/spf13/cobra" ) -type validEntry struct { - active bool - host string - user string - token string - tokenSource string - gitProtocol string - scopes string +type authEntry struct { + State string `json:"state"` + Error string `json:"error"` + Active bool `json:"active"` + Host string `json:"host"` + Login string `json:"login"` + TokenSource string `json:"token_source"` + Token string `json:"token"` + Scopes string `json:"scopes"` + GitProtocol string `json:"git_protocol"` } -func (e validEntry) String(cs *iostreams.ColorScheme) string { +func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { var sb strings.Builder + switch e.State { + case "valid": + sb.WriteString( + fmt.Sprintf(" %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.Host, cs.Bold(e.Login), e.TokenSource), + ) + activeStr := fmt.Sprintf("%v", e.Active) + sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) + sb.WriteString(fmt.Sprintf(" - Git operations protocol: %s\n", cs.Bold(e.GitProtocol))) + sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(displayToken(e.Token, showToken)))) - sb.WriteString( - fmt.Sprintf(" %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.host, cs.Bold(e.user), e.tokenSource), - ) - activeStr := fmt.Sprintf("%v", e.active) - sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) - sb.WriteString(fmt.Sprintf(" - Git operations protocol: %s\n", cs.Bold(e.gitProtocol))) - sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(e.token))) - - if expectScopes(e.token) { - sb.WriteString(fmt.Sprintf(" - Token scopes: %s\n", cs.Bold(displayScopes(e.scopes)))) - if err := shared.HeaderHasMinimumScopes(e.scopes); err != nil { - var missingScopesError *shared.MissingScopesError - if errors.As(err, &missingScopesError) { - missingScopes := strings.Join(missingScopesError.MissingScopes, ",") - sb.WriteString(fmt.Sprintf(" %s Missing required token scopes: %s\n", - cs.WarningIcon(), - cs.Bold(displayScopes(missingScopes)))) - refreshInstructions := fmt.Sprintf("gh auth refresh -h %s", e.host) - sb.WriteString(fmt.Sprintf(" - To request missing scopes, run: %s\n", cs.Bold(refreshInstructions))) + if expectScopes(e.Token) { + sb.WriteString(fmt.Sprintf(" - Token scopes: %s\n", cs.Bold(displayScopes(e.Scopes)))) + if err := shared.HeaderHasMinimumScopes(e.Scopes); err != nil { + var missingScopesError *shared.MissingScopesError + if errors.As(err, &missingScopesError) { + missingScopes := strings.Join(missingScopesError.MissingScopes, ",") + sb.WriteString(fmt.Sprintf(" %s Missing required token scopes: %s\n", + cs.WarningIcon(), + cs.Bold(displayScopes(missingScopes)))) + refreshInstructions := fmt.Sprintf("gh auth refresh -h %s", e.Host) + sb.WriteString(fmt.Sprintf(" - To request missing scopes, run: %s\n", cs.Bold(refreshInstructions))) + } } } + + case "timeout": + if e.Login != "" { + sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) + } else { + sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource)) + } + activeStr := fmt.Sprintf("%v", e.Active) + sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) + + case "error": + if e.Login != "" { + sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) + } else { + sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource)) + } + activeStr := fmt.Sprintf("%v", e.Active) + sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) + sb.WriteString(fmt.Sprintf(" - The token in %s is invalid.\n", e.TokenSource)) + if authTokenWriteable(e.TokenSource) { + loginInstructions := fmt.Sprintf("gh auth login -h %s", e.Host) + logoutInstructions := fmt.Sprintf("gh auth logout -h %s -u %s", e.Host, e.Login) + sb.WriteString(fmt.Sprintf(" - To re-authenticate, run: %s\n", cs.Bold(loginInstructions))) + sb.WriteString(fmt.Sprintf(" - To forget about this account, run: %s\n", cs.Bold(logoutInstructions))) + } + } - - return sb.String() -} - -type invalidTokenEntry struct { - active bool - host string - user string - tokenSource string - tokenIsWriteable bool -} - -func (e invalidTokenEntry) String(cs *iostreams.ColorScheme) string { - var sb strings.Builder - - if e.user != "" { - sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.host, cs.Bold(e.user), e.tokenSource)) - } else { - sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s using token (%s)\n", cs.Red("X"), e.host, e.tokenSource)) - } - activeStr := fmt.Sprintf("%v", e.active) - sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) - sb.WriteString(fmt.Sprintf(" - The token in %s is invalid.\n", e.tokenSource)) - if e.tokenIsWriteable { - loginInstructions := fmt.Sprintf("gh auth login -h %s", e.host) - logoutInstructions := fmt.Sprintf("gh auth logout -h %s -u %s", e.host, e.user) - sb.WriteString(fmt.Sprintf(" - To re-authenticate, run: %s\n", cs.Bold(loginInstructions))) - sb.WriteString(fmt.Sprintf(" - To forget about this account, run: %s\n", cs.Bold(logoutInstructions))) - } - - return sb.String() -} - -type timeoutErrorEntry struct { - active bool - host string - user string - tokenSource string -} - -func (e timeoutErrorEntry) String(cs *iostreams.ColorScheme) string { - var sb strings.Builder - - if e.user != "" { - sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.host, cs.Bold(e.user), e.tokenSource)) - } else { - sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s using token (%s)\n", cs.Red("X"), e.host, e.tokenSource)) - } - activeStr := fmt.Sprintf("%v", e.active) - sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) - return sb.String() } type Entry interface { - String(cs *iostreams.ColorScheme) string + String(cs *iostreams.ColorScheme, showToken bool) string } type Entries []Entry -func (e Entries) Strings(cs *iostreams.ColorScheme) []string { +func (e Entries) Strings(cs *iostreams.ColorScheme, showToken bool) []string { var out []string for _, entry := range e { - out = append(out, entry.String(cs)) + out = append(out, entry.String(cs, showToken)) } return out } @@ -270,7 +249,7 @@ func statusRun(opts *StatusOptions) error { } prevEntry = true fmt.Fprintf(stream, "%s\n", cs.Bold(hostname)) - fmt.Fprintf(stream, "%s", strings.Join(entries.Strings(cs), "\n")) + fmt.Fprintf(stream, "%s", strings.Join(entries.Strings(cs, opts.ShowToken), "\n")) } return err @@ -314,31 +293,34 @@ type buildEntryOptions struct { username string } -func buildEntry(httpClient *http.Client, opts buildEntryOptions) Entry { - tokenIsWriteable := authTokenWriteable(opts.tokenSource) +func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { + + entry := authEntry{ + Active: opts.active, + Host: opts.hostname, + Login: opts.username, + TokenSource: opts.tokenSource, + Token: opts.token, + GitProtocol: opts.gitProtocol, + } if opts.tokenSource == "oauth_token" { // The go-gh function TokenForHost returns this value as source for tokens read from the // config file, but we want the file path instead. This attempts to reconstruct it. - opts.tokenSource = filepath.Join(config.ConfigDir(), "hosts.yml") + entry.TokenSource = filepath.Join(config.ConfigDir(), "hosts.yml") } // If token is not writeable, then it came from an environment variable and // we need to fetch the username as it won't be stored in the config. - if !tokenIsWriteable { + if !authTokenWriteable(opts.tokenSource) { // The httpClient will automatically use the correct token here as // the token from the environment variable take highest precedence. apiClient := api.NewClientFromHTTP(httpClient) var err error - opts.username, err = api.CurrentLoginName(apiClient, opts.hostname) + entry.Login, err = api.CurrentLoginName(apiClient, opts.hostname) if err != nil { - return invalidTokenEntry{ - active: opts.active, - host: opts.hostname, - user: opts.username, - tokenIsWriteable: tokenIsWriteable, - tokenSource: opts.tokenSource, - } + entry.State = "error" + return entry } } @@ -347,39 +329,23 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) Entry { if err != nil { var networkError net.Error if errors.As(err, &networkError) && networkError.Timeout() { - return timeoutErrorEntry{ - active: opts.active, - host: opts.hostname, - user: opts.username, - tokenSource: opts.tokenSource, - } + entry.State = "timeout" + return entry } - return invalidTokenEntry{ - active: opts.active, - host: opts.hostname, - user: opts.username, - tokenIsWriteable: tokenIsWriteable, - tokenSource: opts.tokenSource, - } + entry.State = "error" + return entry } - return validEntry{ - active: opts.active, - gitProtocol: opts.gitProtocol, - host: opts.hostname, - scopes: scopesHeader, - token: displayToken(opts.token, opts.showToken), - tokenSource: opts.tokenSource, - user: opts.username, - } + entry.State = "valid" + entry.Scopes = scopesHeader + return entry } func authTokenWriteable(src string) bool { return !strings.HasSuffix(src, "_TOKEN") } -func isValidEntry(entry Entry) bool { - _, ok := entry.(validEntry) - return ok +func isValidEntry(entry authEntry) bool { + return entry.State == "valid" } From fd19da8e55056b5f846203b26e422b089e6c875a Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:38:20 +0200 Subject: [PATCH 03/87] auth state enum --- pkg/cmd/auth/status/auth_state.go | 28 +++++++++++++++++++++++++ pkg/cmd/auth/status/status.go | 34 +++++++++++++++---------------- 2 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 pkg/cmd/auth/status/auth_state.go diff --git a/pkg/cmd/auth/status/auth_state.go b/pkg/cmd/auth/status/auth_state.go new file mode 100644 index 000000000..57edaa1a6 --- /dev/null +++ b/pkg/cmd/auth/status/auth_state.go @@ -0,0 +1,28 @@ +package status + +import "encoding/json" + +type AuthState int + +const ( + AuthStateSuccess AuthState = iota + AuthStateTimeout + AuthStateError +) + +func (s AuthState) String() string { + switch s { + case AuthStateSuccess: + return "success" + case AuthStateTimeout: + return "timeout" + case AuthStateError: + return "error" + default: + return "unknown" + } +} + +func (s AuthState) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 84bb00a35..555464230 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -20,21 +20,21 @@ import ( ) type authEntry struct { - State string `json:"state"` - Error string `json:"error"` - Active bool `json:"active"` - Host string `json:"host"` - Login string `json:"login"` - TokenSource string `json:"token_source"` - Token string `json:"token"` - Scopes string `json:"scopes"` - GitProtocol string `json:"git_protocol"` + State AuthState `json:"state"` + Error string `json:"error"` + Active bool `json:"active"` + Host string `json:"host"` + Login string `json:"login"` + TokenSource string `json:"token_source"` + Token string `json:"token"` + Scopes string `json:"scopes"` + GitProtocol string `json:"git_protocol"` } func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { var sb strings.Builder switch e.State { - case "valid": + case AuthStateSuccess: sb.WriteString( fmt.Sprintf(" %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.Host, cs.Bold(e.Login), e.TokenSource), ) @@ -58,7 +58,7 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { } } - case "timeout": + case AuthStateTimeout: if e.Login != "" { sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) } else { @@ -67,7 +67,7 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { activeStr := fmt.Sprintf("%v", e.Active) sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) - case "error": + case AuthStateError: if e.Login != "" { sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) } else { @@ -319,7 +319,7 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { var err error entry.Login, err = api.CurrentLoginName(apiClient, opts.hostname) if err != nil { - entry.State = "error" + entry.State = AuthStateError return entry } } @@ -329,15 +329,15 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { if err != nil { var networkError net.Error if errors.As(err, &networkError) && networkError.Timeout() { - entry.State = "timeout" + entry.State = AuthStateTimeout return entry } - entry.State = "error" + entry.State = AuthStateError return entry } - entry.State = "valid" + entry.State = AuthStateSuccess entry.Scopes = scopesHeader return entry } @@ -347,5 +347,5 @@ func authTokenWriteable(src string) bool { } func isValidEntry(entry authEntry) bool { - return entry.State == "valid" + return entry.State == AuthStateSuccess } From 8b553d66cc8579c2cbe89067df19a9d5f837cca1 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 00:27:31 +0200 Subject: [PATCH 04/87] json flags --- pkg/cmd/auth/status/status.go | 51 +++++++++- pkg/cmd/auth/status/status_test.go | 158 +++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 555464230..cc6f13d49 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -31,6 +31,18 @@ type authEntry struct { GitProtocol string `json:"git_protocol"` } +var authFields = []string{ + "state", + "error", + "active", + "host", + "login", + "token_source", + "token", + "scopes", + "git_protocol", +} + func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { var sb strings.Builder switch e.State { @@ -87,8 +99,13 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { return sb.String() } +func (e authEntry) ExportData(fields []string) map[string]interface{} { + return cmdutil.StructExportData(e, fields) +} + type Entry interface { String(cs *iostreams.ColorScheme, showToken bool) string + ExportData(fields []string) map[string]interface{} } type Entries []Entry @@ -101,10 +118,19 @@ func (e Entries) Strings(cs *iostreams.ColorScheme, showToken bool) []string { return out } +func (e Entries) ExportData(fields []string) []map[string]interface{} { + var out []map[string]interface{} + for _, entry := range e { + out = append(out, entry.ExportData(fields)) + } + return out +} + type StatusOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams Config func() (gh.Config, error) + Exporter cmdutil.Exporter Hostname string ShowToken bool @@ -142,8 +168,12 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co } cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "Check only a specific hostname's auth status") - cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "t", false, "Display the auth token") + // FIXME this conflicts with `--template`... temporary workaround + cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "z", false, "Display the auth token") cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only") + cmdutil.AddJSONFlags(cmd, &opts.Exporter, authFields) + + cmd.MarkFlagsMutuallyExclusive("show-token", "json") return cmd } @@ -165,12 +195,18 @@ func statusRun(opts *StatusOptions) error { if len(hostnames) == 0 { fmt.Fprintf(stderr, "You are not logged into any GitHub hosts. To log in, run: %s\n", cs.Bold("gh auth login")) + if opts.Exporter != nil { + opts.Exporter.Write(opts.IO, struct{}{}) + } return cmdutil.SilentError } if opts.Hostname != "" && !slices.Contains(hostnames, opts.Hostname) { fmt.Fprintf(stderr, "You are not logged into any accounts on %s\n", opts.Hostname) + if opts.Exporter != nil { + opts.Exporter.Write(opts.IO, struct{}{}) + } return cmdutil.SilentError } @@ -232,6 +268,19 @@ func statusRun(opts *StatusOptions) error { } } + if opts.Exporter != nil { + statusesForExport := make(map[string]interface{}) + + for _, hostname := range hostnames { + if len(statuses[hostname]) > 0 { + statusesForExport[hostname] = statuses[hostname].ExportData(opts.Exporter.Fields()) + } + } + + opts.Exporter.Write(opts.IO, statusesForExport) + return err + } + prevEntry := false for _, hostname := range hostnames { entries, ok := statuses[hostname] diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 8919dff47..84271a260 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -530,6 +530,158 @@ func Test_statusRun(t *testing.T) { - To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe-2 `), }, + { + name: "No tokens, with json flag", + opts: StatusOptions{ + Exporter: defaultJsonExporter(), + }, + wantErr: cmdutil.SilentError, + wantOut: "{}\n", + wantErrOut: "You are not logged into any GitHub hosts. To log in, run: gh auth login\n", + }, + { + name: "No token for the given --hostname, with json flag", + opts: StatusOptions{ + Hostname: "foo.com", + Exporter: defaultJsonExporter(), + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "gho_abc123", "https") + }, + wantErr: cmdutil.SilentError, + wantOut: "{}\n", + wantErrOut: "You are not logged into any accounts on foo.com\n", + }, + { + name: "All valid tokens, with json flag", + opts: StatusOptions{ + Exporter: defaultJsonExporter(), + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "gho_abc123", "https") + login(t, c, "github.com", "monalisa2", "gho_abc123", "https") + login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") + }, + httpStubs: func(reg *httpmock.Registry) { + // mocks for HeaderHasMinimumScopes api requests to github.com + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + + }, + wantOut: `{` + + `"ghe.io":[` + + `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + + `],` + + `"github.com":[` + + `{"active":true,"host":"github.com","login":"monalisa2","state":"success"},` + + `{"active":false,"host":"github.com","login":"monalisa","state":"success"}` + + "]}\n", + }, + { + name: "All valid tokens, with hostname and json flag", + opts: StatusOptions{ + Hostname: "github.com", + Exporter: defaultJsonExporter(), + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "gho_abc123", "https") + login(t, c, "github.com", "monalisa2", "gho_abc123", "https") + login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") + }, + httpStubs: func(reg *httpmock.Registry) { + // mocks for HeaderHasMinimumScopes api requests to github.com + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + + }, + wantOut: `{` + + `"github.com":[` + + `{"active":true,"host":"github.com","login":"monalisa2","state":"success"},` + + `{"active":false,"host":"github.com","login":"monalisa","state":"success"}` + + "]}\n", + }, + { + name: "All valid tokens, with active and json flag", + opts: StatusOptions{ + Active: true, + Exporter: defaultJsonExporter(), + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "gho_abc123", "https") + login(t, c, "github.com", "monalisa2", "gho_abc123", "https") + login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") + }, + httpStubs: func(reg *httpmock.Registry) { + // mocks for HeaderHasMinimumScopes api requests to github.com + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + }, + wantOut: `{` + + `"ghe.io":[` + + `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + + `],` + + `"github.com":[` + + `{"active":true,"host":"github.com","login":"monalisa2","state":"success"}` + + "]}\n", + }, + { + name: "bad token, with json flag", + opts: StatusOptions{ + Exporter: defaultJsonExporter(), + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") + }, + httpStubs: func(reg *httpmock.Registry) { + // mock for HeaderHasMinimumScopes api requests to a non-github.com host + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) + }, + wantErr: cmdutil.SilentError, + wantOut: `{"ghe.io":[{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"error"}]}` + "\n", + }, + { + name: "timeout error, with json flag", + opts: StatusOptions{ + Hostname: "github.com", + Exporter: defaultJsonExporter(), + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "abc123", "https") + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", ""), func(req *http.Request) (*http.Response, error) { + // timeout error + return nil, context.DeadlineExceeded + }) + }, + wantErr: cmdutil.SilentError, + wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","state":"timeout"}]}` + "\n", + }, + + // TODO: is MarkFlagsMutuallyExclusive ok? + // { + // name: "Both show token and json flags", + // opts: StatusOptions{ + // ShowToken: true, + // }, + // cfgStubs: func(t *testing.T, c gh.Config) { + // login(t, c, "github.com", "monalisa", "abc123", "https") + // }, + // jsonFields: []string{"login", "host", "state", "active"}, + // wantErr: cmdutil.SilentError, + // wantErrOut: "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", + // }, } for _, tt := range tests { @@ -581,3 +733,9 @@ func login(t *testing.T, c gh.Config, hostname, username, protocol, token string _, err := c.Authentication().Login(hostname, username, protocol, token, false) require.NoError(t, err) } + +func defaultJsonExporter() cmdutil.Exporter { + e := cmdutil.NewJSONExporter() + e.SetFields([]string{"login", "host", "state", "active"}) + return e +} From 3ffe199ef3e801eaac1775c02170fc47a7b2a2fa Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:04:09 +0200 Subject: [PATCH 05/87] do not fetch scope if not necessary --- pkg/cmd/auth/status/status.go | 82 +++++++++++++++++------------- pkg/cmd/auth/status/status_test.go | 65 +++++++++-------------- 2 files changed, 70 insertions(+), 77 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index cc6f13d49..95597ac53 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -25,10 +25,10 @@ type authEntry struct { Active bool `json:"active"` Host string `json:"host"` Login string `json:"login"` - TokenSource string `json:"token_source"` + TokenSource string `json:"tokenSource"` Token string `json:"token"` Scopes string `json:"scopes"` - GitProtocol string `json:"git_protocol"` + GitProtocol string `json:"gitProtocol"` } var authFields = []string{ @@ -37,10 +37,10 @@ var authFields = []string{ "active", "host", "login", - "token_source", + "tokenSource", "token", "scopes", - "git_protocol", + "gitProtocol", } func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { @@ -227,13 +227,14 @@ func statusRun(opts *StatusOptions) error { activeUser, _ = authCfg.ActiveUser(hostname) } entry := buildEntry(httpClient, buildEntryOptions{ - active: true, - gitProtocol: gitProtocol, - hostname: hostname, - showToken: opts.ShowToken, - token: activeUserToken, - tokenSource: activeUserTokenSource, - username: activeUser, + active: true, + gitProtocol: gitProtocol, + hostname: hostname, + showToken: opts.ShowToken, + token: activeUserToken, + tokenSource: activeUserTokenSource, + username: activeUser, + includeScope: opts.includeScope(), }) statuses[hostname] = append(statuses[hostname], entry) @@ -252,13 +253,14 @@ func statusRun(opts *StatusOptions) error { } token, tokenSource, _ := authCfg.TokenForUser(hostname, username) entry := buildEntry(httpClient, buildEntryOptions{ - active: false, - gitProtocol: gitProtocol, - hostname: hostname, - showToken: opts.ShowToken, - token: token, - tokenSource: tokenSource, - username: username, + active: false, + gitProtocol: gitProtocol, + hostname: hostname, + showToken: opts.ShowToken, + token: token, + tokenSource: tokenSource, + username: username, + includeScope: opts.includeScope(), }) statuses[hostname] = append(statuses[hostname], entry) @@ -333,13 +335,14 @@ func expectScopes(token string) bool { } type buildEntryOptions struct { - active bool - gitProtocol string - hostname string - showToken bool - token string - tokenSource string - username string + active bool + gitProtocol string + hostname string + showToken bool + token string + tokenSource string + username string + includeScope bool } func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { @@ -373,21 +376,23 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { } } - // Get scopes for token. - scopesHeader, err := shared.GetScopes(httpClient, opts.hostname, opts.token) - if err != nil { - var networkError net.Error - if errors.As(err, &networkError) && networkError.Timeout() { - entry.State = AuthStateTimeout + if opts.includeScope { + // Get scopes for token. + scopesHeader, err := shared.GetScopes(httpClient, opts.hostname, opts.token) + if err != nil { + var networkError net.Error + if errors.As(err, &networkError) && networkError.Timeout() { + entry.State = AuthStateTimeout + return entry + } + + entry.State = AuthStateError return entry } - - entry.State = AuthStateError - return entry + entry.Scopes = scopesHeader } entry.State = AuthStateSuccess - entry.Scopes = scopesHeader return entry } @@ -398,3 +403,10 @@ func authTokenWriteable(src string) bool { func isValidEntry(entry authEntry) bool { return entry.State == AuthStateSuccess } + +func (opts *StatusOptions) includeScope() bool { + if opts.Exporter == nil { + return true + } + return slices.Contains(opts.Exporter.Fields(), "scopes") +} diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 84271a260..64a4868f7 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -531,7 +531,7 @@ func Test_statusRun(t *testing.T) { `), }, { - name: "No tokens, with json flag", + name: "No tokens with json flag", opts: StatusOptions{ Exporter: defaultJsonExporter(), }, @@ -540,7 +540,7 @@ func Test_statusRun(t *testing.T) { wantErrOut: "You are not logged into any GitHub hosts. To log in, run: gh auth login\n", }, { - name: "No token for the given --hostname, with json flag", + name: "No token for the given --hostname with json flag", opts: StatusOptions{ Hostname: "foo.com", Exporter: defaultJsonExporter(), @@ -553,7 +553,7 @@ func Test_statusRun(t *testing.T) { wantErrOut: "You are not logged into any accounts on foo.com\n", }, { - name: "All valid tokens, with json flag", + name: "All valid tokens with json flag", opts: StatusOptions{ Exporter: defaultJsonExporter(), }, @@ -562,17 +562,6 @@ func Test_statusRun(t *testing.T) { login(t, c, "github.com", "monalisa2", "gho_abc123", "https") login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") }, - httpStubs: func(reg *httpmock.Registry) { - // mocks for HeaderHasMinimumScopes api requests to github.com - reg.Register( - httpmock.REST("GET", ""), - httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - reg.Register( - httpmock.REST("GET", ""), - httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - - }, wantOut: `{` + `"ghe.io":[` + `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + @@ -583,7 +572,7 @@ func Test_statusRun(t *testing.T) { "]}\n", }, { - name: "All valid tokens, with hostname and json flag", + name: "All valid tokens with hostname and json flag", opts: StatusOptions{ Hostname: "github.com", Exporter: defaultJsonExporter(), @@ -593,16 +582,6 @@ func Test_statusRun(t *testing.T) { login(t, c, "github.com", "monalisa2", "gho_abc123", "https") login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") }, - httpStubs: func(reg *httpmock.Registry) { - // mocks for HeaderHasMinimumScopes api requests to github.com - reg.Register( - httpmock.REST("GET", ""), - httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - reg.Register( - httpmock.REST("GET", ""), - httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - - }, wantOut: `{` + `"github.com":[` + `{"active":true,"host":"github.com","login":"monalisa2","state":"success"},` + @@ -610,7 +589,7 @@ func Test_statusRun(t *testing.T) { "]}\n", }, { - name: "All valid tokens, with active and json flag", + name: "All valid tokens with active and json flag", opts: StatusOptions{ Active: true, Exporter: defaultJsonExporter(), @@ -620,13 +599,6 @@ func Test_statusRun(t *testing.T) { login(t, c, "github.com", "monalisa2", "gho_abc123", "https") login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") }, - httpStubs: func(reg *httpmock.Registry) { - // mocks for HeaderHasMinimumScopes api requests to github.com - reg.Register( - httpmock.REST("GET", ""), - httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - }, wantOut: `{` + `"ghe.io":[` + `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + @@ -636,9 +608,9 @@ func Test_statusRun(t *testing.T) { "]}\n", }, { - name: "bad token, with json flag", + name: "bad token with json flag", opts: StatusOptions{ - Exporter: defaultJsonExporter(), + Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"scopes"}), }, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") @@ -648,13 +620,13 @@ func Test_statusRun(t *testing.T) { reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) }, wantErr: cmdutil.SilentError, - wantOut: `{"ghe.io":[{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"error"}]}` + "\n", + wantOut: `{"ghe.io":[{"active":true,"host":"ghe.io","login":"monalisa-ghe","scopes":"","state":"error"}]}` + "\n", }, { - name: "timeout error, with json flag", + name: "timeout error with json flag", opts: StatusOptions{ Hostname: "github.com", - Exporter: defaultJsonExporter(), + Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"scopes"}), }, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "abc123", "https") @@ -666,7 +638,7 @@ func Test_statusRun(t *testing.T) { }) }, wantErr: cmdutil.SilentError, - wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","state":"timeout"}]}` + "\n", + wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","scopes":"","state":"timeout"}]}` + "\n", }, // TODO: is MarkFlagsMutuallyExclusive ok? @@ -734,8 +706,17 @@ func login(t *testing.T, c gh.Config, hostname, username, protocol, token string require.NoError(t, err) } -func defaultJsonExporter() cmdutil.Exporter { - e := cmdutil.NewJSONExporter() - e.SetFields([]string{"login", "host", "state", "active"}) +type exporter interface { + cmdutil.Exporter + SetFields(fields []string) +} + +func addFieldsToExporter(e exporter, fields []string) exporter { + newFields := append(e.Fields(), fields...) + e.SetFields(newFields) return e } +func defaultJsonExporter() exporter { + return addFieldsToExporter(cmdutil.NewJSONExporter(), []string{"login", "host", "state", "active"}) + +} From 96755eec83ee971b5596d4edf5674bf37ccbafb7 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:05:20 +0200 Subject: [PATCH 06/87] handle -t conflict --- pkg/cmd/auth/status/status.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 95597ac53..9c9bfe67c 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -17,6 +17,7 @@ import ( "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type authEntry struct { @@ -168,10 +169,18 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co } cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "Check only a specific hostname's auth status") - // FIXME this conflicts with `--template`... temporary workaround - cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "z", false, "Display the auth token") + cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "t", false, "Display the auth token") cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only") - cmdutil.AddJSONFlags(cmd, &opts.Exporter, authFields) + + // Add JSON flags for exporting, but ignore the default `-t` abbreviation that conflicts with show-token. + tmpCmd := &cobra.Command{} + cmdutil.AddJSONFlags(tmpCmd, &opts.Exporter, authFields) + tmpCmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Name == "template" { + f.Shorthand = "" + } + cmd.Flags().AddFlag(f) + }) cmd.MarkFlagsMutuallyExclusive("show-token", "json") From 3d02e248c0921bddced141199f8fb240b97f426b Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:50:29 +0200 Subject: [PATCH 07/87] do not export authState --- pkg/cmd/auth/status/auth_state.go | 18 +++++++++--------- pkg/cmd/auth/status/status.go | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/cmd/auth/status/auth_state.go b/pkg/cmd/auth/status/auth_state.go index 57edaa1a6..0c7e47675 100644 --- a/pkg/cmd/auth/status/auth_state.go +++ b/pkg/cmd/auth/status/auth_state.go @@ -2,27 +2,27 @@ package status import "encoding/json" -type AuthState int +type authState int const ( - AuthStateSuccess AuthState = iota - AuthStateTimeout - AuthStateError + authStateSuccess authState = iota + authStateTimeout + authStateError ) -func (s AuthState) String() string { +func (s authState) String() string { switch s { - case AuthStateSuccess: + case authStateSuccess: return "success" - case AuthStateTimeout: + case authStateTimeout: return "timeout" - case AuthStateError: + case authStateError: return "error" default: return "unknown" } } -func (s AuthState) MarshalJSON() ([]byte, error) { +func (s authState) MarshalJSON() ([]byte, error) { return json.Marshal(s.String()) } diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 9c9bfe67c..11aad6baa 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -21,7 +21,7 @@ import ( ) type authEntry struct { - State AuthState `json:"state"` + State authState `json:"state"` Error string `json:"error"` Active bool `json:"active"` Host string `json:"host"` @@ -47,7 +47,7 @@ var authFields = []string{ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { var sb strings.Builder switch e.State { - case AuthStateSuccess: + case authStateSuccess: sb.WriteString( fmt.Sprintf(" %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.Host, cs.Bold(e.Login), e.TokenSource), ) @@ -71,7 +71,7 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { } } - case AuthStateTimeout: + case authStateTimeout: if e.Login != "" { sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) } else { @@ -80,7 +80,7 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { activeStr := fmt.Sprintf("%v", e.Active) sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) - case AuthStateError: + case authStateError: if e.Login != "" { sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) } else { @@ -380,7 +380,7 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { var err error entry.Login, err = api.CurrentLoginName(apiClient, opts.hostname) if err != nil { - entry.State = AuthStateError + entry.State = authStateError return entry } } @@ -391,17 +391,17 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { if err != nil { var networkError net.Error if errors.As(err, &networkError) && networkError.Timeout() { - entry.State = AuthStateTimeout + entry.State = authStateTimeout return entry } - entry.State = AuthStateError + entry.State = authStateError return entry } entry.Scopes = scopesHeader } - entry.State = AuthStateSuccess + entry.State = authStateSuccess return entry } @@ -410,7 +410,7 @@ func authTokenWriteable(src string) bool { } func isValidEntry(entry authEntry) bool { - return entry.State == AuthStateSuccess + return entry.State == authStateSuccess } func (opts *StatusOptions) includeScope() bool { From 6e6c09e6b14245eb2fe9078ce02de3148b344926 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:36:09 +0200 Subject: [PATCH 08/87] add ExpectCommandToSupportJSONFields --- pkg/cmd/auth/status/status_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 64a4868f7..595d8f07d 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -14,6 +14,7 @@ import ( "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/jsonfieldstest" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -84,6 +85,20 @@ func Test_NewCmdStatus(t *testing.T) { } } +func TestJSONFields(t *testing.T) { + jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdStatus, []string{ + "state", + "error", + "active", + "host", + "login", + "tokenSource", + "token", + "scopes", + "gitProtocol", + }) +} + func Test_statusRun(t *testing.T) { tests := []struct { name string From faa0e2c26b5dd33585bc3b89e5fc9232c29c81aa Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:00:30 +0200 Subject: [PATCH 09/87] flag duplicate check --- pkg/cmd/auth/status/status.go | 11 +------- pkg/cmdutil/json_flags.go | 13 +++++++-- pkg/cmdutil/json_flags_test.go | 48 ++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 11aad6baa..47d4106fb 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -17,7 +17,6 @@ import ( "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type authEntry struct { @@ -172,15 +171,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "t", false, "Display the auth token") cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only") - // Add JSON flags for exporting, but ignore the default `-t` abbreviation that conflicts with show-token. - tmpCmd := &cobra.Command{} - cmdutil.AddJSONFlags(tmpCmd, &opts.Exporter, authFields) - tmpCmd.Flags().VisitAll(func(f *pflag.Flag) { - if f.Name == "template" { - f.Shorthand = "" - } - cmd.Flags().AddFlag(f) - }) + cmdutil.AddJSONFlags(cmd, &opts.Exporter, authFields) cmd.MarkFlagsMutuallyExclusive("show-token", "json") diff --git a/pkg/cmdutil/json_flags.go b/pkg/cmdutil/json_flags.go index 596c2f216..29bb8c002 100644 --- a/pkg/cmdutil/json_flags.go +++ b/pkg/cmdutil/json_flags.go @@ -26,8 +26,8 @@ type JSONFlagError struct { func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { f := cmd.Flags() f.StringSlice("json", nil, "Output JSON with the specified `fields`") - f.StringP("jq", "q", "", "Filter JSON output using a jq `expression`") - f.StringP("template", "t", "", "Format JSON output using a Go template; see \"gh help formatting\"") + addStringFlagWithSafeShorthand(f, "jq", "q", "", "Filter JSON output using a jq `expression`") + addStringFlagWithSafeShorthand(f, "template", "t", "", "Format JSON output using a Go template; see \"gh help formatting\"") _ = cmd.RegisterFlagCompletionFunc("json", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var results []string @@ -94,6 +94,15 @@ func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { cmd.Annotations["help:json-fields"] = strings.Join(fields, ",") } +// addStringFlagWithSafeShorthand only adds the flag with shorthand if the shorthand is not already used by another flag. +func addStringFlagWithSafeShorthand(f *pflag.FlagSet, name, shorthand, value, usage string) { + if f.ShorthandLookup(shorthand) != nil { + f.String(name, value, usage) + } else { + f.StringP(name, shorthand, value, usage) + } +} + func checkJSONFlags(cmd *cobra.Command) (*jsonExporter, error) { f := cmd.Flags() jsonFlag := f.Lookup("json") diff --git a/pkg/cmdutil/json_flags_test.go b/pkg/cmdutil/json_flags_test.go index 63c63aa00..15f0b7642 100644 --- a/pkg/cmdutil/json_flags_test.go +++ b/pkg/cmdutil/json_flags_test.go @@ -119,6 +119,54 @@ func TestAddJSONFlags(t *testing.T) { } } +func TestAddJSONFlagsNoDuplicateShorthand(t *testing.T) { + tests := []struct { + name string + setFlags func(cmd *cobra.Command) + wantFlags map[string]string + }{ + { + name: "no conflicting flags", + setFlags: func(cmd *cobra.Command) { + cmd.Flags().StringP("web", "w", "", "") + }, + wantFlags: map[string]string{ + "web": "w", + "jq": "q", + "template": "t", + "json": "", + }, + }, + { + name: "conflicting flags", + setFlags: func(cmd *cobra.Command) { + cmd.Flags().StringP("token", "t", "", "") + }, + wantFlags: map[string]string{ + "token": "t", + "jq": "q", + "template": "", // no shorthand + "json": "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + cmd := &cobra.Command{Run: func(*cobra.Command, []string) {}} + tt.setFlags(cmd) + + AddJSONFlags(cmd, nil, []string{}) + + for f, shorthand := range tt.wantFlags { + flag := cmd.Flags().Lookup(f) + require.NotNil(t, flag) + require.Equal(t, shorthand, flag.Shorthand) + } + }) + } +} + // TestAddJSONFlagsSetsAnnotations asserts that `AddJSONFlags` function adds the // appropriate annotation to the command, which could later be used by doc // generator functions. From c937e0275d22b185dcc4d719fce98ea0a1993269 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:02:41 +0200 Subject: [PATCH 10/87] mutually exclusive flags --- pkg/cmd/auth/status/status.go | 7 +++++-- pkg/cmd/auth/status/status_test.go | 26 ++++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 47d4106fb..9b876b8c7 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -173,8 +173,6 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co cmdutil.AddJSONFlags(cmd, &opts.Exporter, authFields) - cmd.MarkFlagsMutuallyExclusive("show-token", "json") - return cmd } @@ -189,6 +187,11 @@ func statusRun(opts *StatusOptions) error { stdout := opts.IO.Out cs := opts.IO.ColorScheme() + if opts.ShowToken && opts.Exporter != nil { + fmt.Fprintf(stderr, "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.") + return cmdutil.SilentError + } + statuses := make(map[string]Entries) hostnames := authCfg.Hosts() diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 595d8f07d..09b7b9379 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -655,20 +655,18 @@ func Test_statusRun(t *testing.T) { wantErr: cmdutil.SilentError, wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","scopes":"","state":"timeout"}]}` + "\n", }, - - // TODO: is MarkFlagsMutuallyExclusive ok? - // { - // name: "Both show token and json flags", - // opts: StatusOptions{ - // ShowToken: true, - // }, - // cfgStubs: func(t *testing.T, c gh.Config) { - // login(t, c, "github.com", "monalisa", "abc123", "https") - // }, - // jsonFields: []string{"login", "host", "state", "active"}, - // wantErr: cmdutil.SilentError, - // wantErrOut: "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", - // }, + { + name: "Both show token and json flags", + opts: StatusOptions{ + ShowToken: true, + Exporter: defaultJsonExporter(), + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "abc123", "https") + }, + wantErr: cmdutil.SilentError, + wantErrOut: "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", + }, } for _, tt := range tests { From 085f31fed843675c5c01cb18f25f86a2aa437071 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:07:57 +0200 Subject: [PATCH 11/87] revert showToken change --- pkg/cmd/auth/status/status.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 9b876b8c7..52cebccbe 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -43,7 +43,7 @@ var authFields = []string{ "gitProtocol", } -func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { +func (e authEntry) String(cs *iostreams.ColorScheme) string { var sb strings.Builder switch e.State { case authStateSuccess: @@ -53,7 +53,7 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { activeStr := fmt.Sprintf("%v", e.Active) sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) sb.WriteString(fmt.Sprintf(" - Git operations protocol: %s\n", cs.Bold(e.GitProtocol))) - sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(displayToken(e.Token, showToken)))) + sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(e.Token))) if expectScopes(e.Token) { sb.WriteString(fmt.Sprintf(" - Token scopes: %s\n", cs.Bold(displayScopes(e.Scopes)))) @@ -104,16 +104,16 @@ func (e authEntry) ExportData(fields []string) map[string]interface{} { } type Entry interface { - String(cs *iostreams.ColorScheme, showToken bool) string + String(cs *iostreams.ColorScheme) string ExportData(fields []string) map[string]interface{} } type Entries []Entry -func (e Entries) Strings(cs *iostreams.ColorScheme, showToken bool) []string { +func (e Entries) Strings(cs *iostreams.ColorScheme) []string { var out []string for _, entry := range e { - out = append(out, entry.String(cs, showToken)) + out = append(out, entry.String(cs)) } return out } @@ -303,7 +303,7 @@ func statusRun(opts *StatusOptions) error { } prevEntry = true fmt.Fprintf(stream, "%s\n", cs.Bold(hostname)) - fmt.Fprintf(stream, "%s", strings.Join(entries.Strings(cs, opts.ShowToken), "\n")) + fmt.Fprintf(stream, "%s", strings.Join(entries.Strings(cs), "\n")) } return err @@ -355,7 +355,7 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { Host: opts.hostname, Login: opts.username, TokenSource: opts.tokenSource, - Token: opts.token, + Token: displayToken(opts.token, opts.showToken), GitProtocol: opts.gitProtocol, } From 48bc79a291d6da7e05a555d5f1a95ee91075d410 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:26:27 +0200 Subject: [PATCH 12/87] fix exit code --- pkg/cmd/auth/status/status.go | 6 ++++-- pkg/cmd/auth/status/status_test.go | 5 ----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 52cebccbe..014f3e7b6 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -189,7 +189,7 @@ func statusRun(opts *StatusOptions) error { if opts.ShowToken && opts.Exporter != nil { fmt.Fprintf(stderr, "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.") - return cmdutil.SilentError + return nil } statuses := make(map[string]Entries) @@ -200,6 +200,7 @@ func statusRun(opts *StatusOptions) error { "You are not logged into any GitHub hosts. To log in, run: %s\n", cs.Bold("gh auth login")) if opts.Exporter != nil { opts.Exporter.Write(opts.IO, struct{}{}) + return nil } return cmdutil.SilentError } @@ -209,6 +210,7 @@ func statusRun(opts *StatusOptions) error { "You are not logged into any accounts on %s\n", opts.Hostname) if opts.Exporter != nil { opts.Exporter.Write(opts.IO, struct{}{}) + return nil } return cmdutil.SilentError } @@ -283,7 +285,7 @@ func statusRun(opts *StatusOptions) error { } opts.Exporter.Write(opts.IO, statusesForExport) - return err + return nil } prevEntry := false diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 09b7b9379..567748d82 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -550,7 +550,6 @@ func Test_statusRun(t *testing.T) { opts: StatusOptions{ Exporter: defaultJsonExporter(), }, - wantErr: cmdutil.SilentError, wantOut: "{}\n", wantErrOut: "You are not logged into any GitHub hosts. To log in, run: gh auth login\n", }, @@ -563,7 +562,6 @@ func Test_statusRun(t *testing.T) { cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") }, - wantErr: cmdutil.SilentError, wantOut: "{}\n", wantErrOut: "You are not logged into any accounts on foo.com\n", }, @@ -634,7 +632,6 @@ func Test_statusRun(t *testing.T) { // mock for HeaderHasMinimumScopes api requests to a non-github.com host reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) }, - wantErr: cmdutil.SilentError, wantOut: `{"ghe.io":[{"active":true,"host":"ghe.io","login":"monalisa-ghe","scopes":"","state":"error"}]}` + "\n", }, { @@ -652,7 +649,6 @@ func Test_statusRun(t *testing.T) { return nil, context.DeadlineExceeded }) }, - wantErr: cmdutil.SilentError, wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","scopes":"","state":"timeout"}]}` + "\n", }, { @@ -664,7 +660,6 @@ func Test_statusRun(t *testing.T) { cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "abc123", "https") }, - wantErr: cmdutil.SilentError, wantErrOut: "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", }, } From 78675e73e1fbd5d0cdf065e2a6b7db518c78dbae Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 20 Aug 2025 21:01:31 +0200 Subject: [PATCH 13/87] fix show token when using json --- pkg/cmd/auth/status/status.go | 9 ++++++--- pkg/cmd/auth/status/status_test.go | 11 +++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 014f3e7b6..417a0773c 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -187,9 +187,12 @@ func statusRun(opts *StatusOptions) error { stdout := opts.IO.Out cs := opts.IO.ColorScheme() - if opts.ShowToken && opts.Exporter != nil { - fmt.Fprintf(stderr, "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.") - return nil + if opts.Exporter != nil { + if opts.ShowToken { + fmt.Fprintf(stderr, "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.") + return nil + } + opts.ShowToken = true } statuses := make(map[string]Entries) diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 567748d82..43d552394 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -651,6 +651,17 @@ func Test_statusRun(t *testing.T) { }, wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","scopes":"","state":"timeout"}]}` + "\n", }, + { + name: "token is not masked with json flag", + opts: StatusOptions{ + Hostname: "github.com", + Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"token"}), + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "abc123", "https") + }, + wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","state":"success","token":"abc123"}]}` + "\n", + }, { name: "Both show token and json flags", opts: StatusOptions{ From 325743e78b160af9256037a50dc43d151a4203ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:02:11 +0000 Subject: [PATCH 14/87] chore(deps): bump actions/attest-build-provenance from 2.4.0 to 3.0.0 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.4.0 to 3.0.0. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/e8998f949152b193b063cb0ec769d69d929409be...977bb373ede98d70efdf65b84cb5f73e068dcc2a) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 1510ac22a..ab45fee19 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -309,7 +309,7 @@ jobs: rpmsign --addsign dist/*.rpm - name: Attest release artifacts if: inputs.environment == 'production' - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 with: subject-path: "dist/gh_*" - name: Run createrepo From 45ecc5ece97e4bbd790270cc46661a81ec0bcaf1 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:56:26 +0200 Subject: [PATCH 15/87] introduce AddJSONFlagsWithoutShorthand --- pkg/cmd/auth/status/status.go | 3 ++- pkg/cmdutil/json_flags.go | 43 ++++++++++++++++++++++++---------- pkg/cmdutil/json_flags_test.go | 20 ++++------------ 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 417a0773c..bed9299b8 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -171,7 +171,8 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "t", false, "Display the auth token") cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only") - cmdutil.AddJSONFlags(cmd, &opts.Exporter, authFields) + // the json flags are intentionally not given a shorthand to avoid conflict with -t/--show-token + cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authFields) return cmd } diff --git a/pkg/cmdutil/json_flags.go b/pkg/cmdutil/json_flags.go index 29bb8c002..95e990585 100644 --- a/pkg/cmdutil/json_flags.go +++ b/pkg/cmdutil/json_flags.go @@ -24,10 +24,38 @@ type JSONFlagError struct { } func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { - f := cmd.Flags() + f := createFlags() + + f.VisitAll(func(flag *pflag.Flag) { + if flag.Name == "jq" { + flag.Shorthand = "q" + } + if flag.Name == "template" { + flag.Shorthand = "t" + } + cmd.Flags().AddFlag(flag) + }) + + setupJsonFlags(cmd, exportTarget, fields) +} + +func AddJSONFlagsWithoutShorthand(cmd *cobra.Command, exportTarget *Exporter, fields []string) { + f := createFlags() + f.VisitAll(func(flag *pflag.Flag) { + cmd.Flags().AddFlag(flag) + }) + setupJsonFlags(cmd, exportTarget, fields) +} + +func createFlags() *pflag.FlagSet { + f := pflag.NewFlagSet("", pflag.ContinueOnError) f.StringSlice("json", nil, "Output JSON with the specified `fields`") - addStringFlagWithSafeShorthand(f, "jq", "q", "", "Filter JSON output using a jq `expression`") - addStringFlagWithSafeShorthand(f, "template", "t", "", "Format JSON output using a Go template; see \"gh help formatting\"") + f.String("jq", "", "Filter JSON output using a jq `expression`") + f.String("template", "", "Format JSON output using a Go template; see \"gh help formatting\"") + return f +} + +func setupJsonFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { _ = cmd.RegisterFlagCompletionFunc("json", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var results []string @@ -94,15 +122,6 @@ func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { cmd.Annotations["help:json-fields"] = strings.Join(fields, ",") } -// addStringFlagWithSafeShorthand only adds the flag with shorthand if the shorthand is not already used by another flag. -func addStringFlagWithSafeShorthand(f *pflag.FlagSet, name, shorthand, value, usage string) { - if f.ShorthandLookup(shorthand) != nil { - f.String(name, value, usage) - } else { - f.StringP(name, shorthand, value, usage) - } -} - func checkJSONFlags(cmd *cobra.Command) (*jsonExporter, error) { f := cmd.Flags() jsonFlag := f.Lookup("json") diff --git a/pkg/cmdutil/json_flags_test.go b/pkg/cmdutil/json_flags_test.go index 15f0b7642..ee089960b 100644 --- a/pkg/cmdutil/json_flags_test.go +++ b/pkg/cmdutil/json_flags_test.go @@ -119,7 +119,7 @@ func TestAddJSONFlags(t *testing.T) { } } -func TestAddJSONFlagsNoDuplicateShorthand(t *testing.T) { +func TestAddJSONFlagsWithoutShorthand(t *testing.T) { tests := []struct { name string setFlags func(cmd *cobra.Command) @@ -129,23 +129,13 @@ func TestAddJSONFlagsNoDuplicateShorthand(t *testing.T) { name: "no conflicting flags", setFlags: func(cmd *cobra.Command) { cmd.Flags().StringP("web", "w", "", "") - }, - wantFlags: map[string]string{ - "web": "w", - "jq": "q", - "template": "t", - "json": "", - }, - }, - { - name: "conflicting flags", - setFlags: func(cmd *cobra.Command) { cmd.Flags().StringP("token", "t", "", "") }, wantFlags: map[string]string{ + "web": "w", "token": "t", - "jq": "q", - "template": "", // no shorthand + "jq": "", + "template": "", "json": "", }, }, @@ -156,7 +146,7 @@ func TestAddJSONFlagsNoDuplicateShorthand(t *testing.T) { cmd := &cobra.Command{Run: func(*cobra.Command, []string) {}} tt.setFlags(cmd) - AddJSONFlags(cmd, nil, []string{}) + AddJSONFlagsWithoutShorthand(cmd, nil, []string{}) for f, shorthand := range tt.wantFlags { flag := cmd.Flags().Lookup(f) From a9cc63b8a376f93eba39cb08e172fdecdc982b03 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sun, 31 Aug 2025 18:09:46 +0200 Subject: [PATCH 16/87] refactor without VisitAll --- pkg/cmdutil/json_flags.go | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/pkg/cmdutil/json_flags.go b/pkg/cmdutil/json_flags.go index 95e990585..579d38552 100644 --- a/pkg/cmdutil/json_flags.go +++ b/pkg/cmdutil/json_flags.go @@ -24,35 +24,31 @@ type JSONFlagError struct { } func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { - f := createFlags() - - f.VisitAll(func(flag *pflag.Flag) { - if flag.Name == "jq" { - flag.Shorthand = "q" - } - if flag.Name == "template" { - flag.Shorthand = "t" - } - cmd.Flags().AddFlag(flag) - }) + f := cmd.Flags() + addJsonFlag(f) + addJqFlag(f, "q") + addTemplateFlag(f, "t") setupJsonFlags(cmd, exportTarget, fields) } func AddJSONFlagsWithoutShorthand(cmd *cobra.Command, exportTarget *Exporter, fields []string) { - f := createFlags() - f.VisitAll(func(flag *pflag.Flag) { - cmd.Flags().AddFlag(flag) - }) + f := cmd.Flags() + addJsonFlag(f) + addJqFlag(f, "") + addTemplateFlag(f, "") + setupJsonFlags(cmd, exportTarget, fields) } -func createFlags() *pflag.FlagSet { - f := pflag.NewFlagSet("", pflag.ContinueOnError) +func addJsonFlag(f *pflag.FlagSet) { f.StringSlice("json", nil, "Output JSON with the specified `fields`") - f.String("jq", "", "Filter JSON output using a jq `expression`") - f.String("template", "", "Format JSON output using a Go template; see \"gh help formatting\"") - return f +} +func addJqFlag(f *pflag.FlagSet, shorthand string) { + f.StringP("jq", shorthand, "", "Filter JSON output using a jq `expression`") +} +func addTemplateFlag(f *pflag.FlagSet, shorthand string) { + f.StringP("template", shorthand, "", "Format JSON output using a Go template; see \"gh help formatting\"") } func setupJsonFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { From b38e12ef616ed7c969c38554bd51f38341760bf5 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:23:42 +0200 Subject: [PATCH 17/87] move flag validation to RunE --- pkg/cmd/auth/status/status.go | 11 +++++---- pkg/cmd/auth/status/status_test.go | 36 +++++++++++++++--------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index bed9299b8..07ff8bb1e 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -159,6 +159,13 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co To change the active account for a host, see %[1]sgh auth switch%[1]s. `, "`"), RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.MutuallyExclusive( + "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", + opts.Exporter != nil, + opts.ShowToken, + ); err != nil { + return err + } if runF != nil { return runF(opts) } @@ -189,10 +196,6 @@ func statusRun(opts *StatusOptions) error { cs := opts.IO.ColorScheme() if opts.Exporter != nil { - if opts.ShowToken { - fmt.Fprintf(stderr, "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.") - return nil - } opts.ShowToken = true } diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 43d552394..126e7fa7e 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -22,9 +22,11 @@ import ( func Test_NewCmdStatus(t *testing.T) { tests := []struct { - name string - cli string - wants StatusOptions + name string + cli string + wants StatusOptions + wantErr error + wantErrOut string }{ { name: "no arguments", @@ -52,6 +54,11 @@ func Test_NewCmdStatus(t *testing.T) { Active: true, }, }, + { + name: "both --show-token and --json flags", + cli: "--show-token --json state,token", + wantErr: cmdutil.FlagErrorf("`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`."), + }, } for _, tt := range tests { @@ -76,11 +83,15 @@ func Test_NewCmdStatus(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() - assert.NoError(t, err) + if tt.wantErr == nil { + assert.NoError(t, err) - assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) - assert.Equal(t, tt.wants.ShowToken, gotOpts.ShowToken) - assert.Equal(t, tt.wants.Active, gotOpts.Active) + assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) + assert.Equal(t, tt.wants.ShowToken, gotOpts.ShowToken) + assert.Equal(t, tt.wants.Active, gotOpts.Active) + } else { + assert.Equal(t, tt.wantErr, err) + } }) } } @@ -662,17 +673,6 @@ func Test_statusRun(t *testing.T) { }, wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","state":"success","token":"abc123"}]}` + "\n", }, - { - name: "Both show token and json flags", - opts: StatusOptions{ - ShowToken: true, - Exporter: defaultJsonExporter(), - }, - cfgStubs: func(t *testing.T, c gh.Config) { - login(t, c, "github.com", "monalisa", "abc123", "https") - }, - wantErrOut: "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", - }, } for _, tt := range tests { From 60088e0e7de64c56fb113957dba3f832617afd9b Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:27:15 +0200 Subject: [PATCH 18/87] move displayToken to String method --- pkg/cmd/auth/status/status.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 07ff8bb1e..3487d8f7e 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -29,6 +29,8 @@ type authEntry struct { Token string `json:"token"` Scopes string `json:"scopes"` GitProtocol string `json:"gitProtocol"` + + showToken bool } var authFields = []string{ @@ -53,7 +55,7 @@ func (e authEntry) String(cs *iostreams.ColorScheme) string { activeStr := fmt.Sprintf("%v", e.Active) sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) sb.WriteString(fmt.Sprintf(" - Git operations protocol: %s\n", cs.Bold(e.GitProtocol))) - sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(e.Token))) + sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(e.displayToken()))) if expectScopes(e.Token) { sb.WriteString(fmt.Sprintf(" - Token scopes: %s\n", cs.Bold(displayScopes(e.Scopes)))) @@ -318,17 +320,17 @@ func statusRun(opts *StatusOptions) error { return err } -func displayToken(token string, printRaw bool) string { - if printRaw { - return token +func (e authEntry) displayToken() string { + if e.showToken { + return e.Token } - if idx := strings.LastIndexByte(token, '_'); idx > -1 { - prefix := token[0 : idx+1] - return prefix + strings.Repeat("*", len(token)-len(prefix)) + if idx := strings.LastIndexByte(e.Token, '_'); idx > -1 { + prefix := e.Token[0 : idx+1] + return prefix + strings.Repeat("*", len(e.Token)-len(prefix)) } - return strings.Repeat("*", len(token)) + return strings.Repeat("*", len(e.Token)) } func displayScopes(scopes string) string { @@ -364,8 +366,10 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { Host: opts.hostname, Login: opts.username, TokenSource: opts.tokenSource, - Token: displayToken(opts.token, opts.showToken), + Token: opts.token, GitProtocol: opts.gitProtocol, + + showToken: opts.showToken, } if opts.tokenSource == "oauth_token" { From 54bf8432f62c8844b7e505e7e75c4f5f179aca99 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:31:57 +0200 Subject: [PATCH 19/87] do not mutate opts.ShowToken --- pkg/cmd/auth/status/status.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 3487d8f7e..07e11d204 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -197,8 +197,9 @@ func statusRun(opts *StatusOptions) error { stdout := opts.IO.Out cs := opts.IO.ColorScheme() - if opts.Exporter != nil { - opts.ShowToken = true + showToken := opts.ShowToken + if opts.Exporter != nil && slices.Contains(opts.Exporter.Fields(), "token") { + showToken = true } statuses := make(map[string]Entries) @@ -244,7 +245,7 @@ func statusRun(opts *StatusOptions) error { active: true, gitProtocol: gitProtocol, hostname: hostname, - showToken: opts.ShowToken, + showToken: showToken, token: activeUserToken, tokenSource: activeUserTokenSource, username: activeUser, @@ -270,7 +271,7 @@ func statusRun(opts *StatusOptions) error { active: false, gitProtocol: gitProtocol, hostname: hostname, - showToken: opts.ShowToken, + showToken: showToken, token: token, tokenSource: tokenSource, username: username, From 5abb467e693bb0fca955b8df1504ae8b38098dd8 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:50:06 +0200 Subject: [PATCH 20/87] remove includeScope --- pkg/cmd/auth/status/status.go | 74 +++++++++++++----------------- pkg/cmd/auth/status/status_test.go | 39 ++++++++++++++++ 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 07e11d204..d80b5e61f 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -242,14 +242,13 @@ func statusRun(opts *StatusOptions) error { activeUser, _ = authCfg.ActiveUser(hostname) } entry := buildEntry(httpClient, buildEntryOptions{ - active: true, - gitProtocol: gitProtocol, - hostname: hostname, - showToken: showToken, - token: activeUserToken, - tokenSource: activeUserTokenSource, - username: activeUser, - includeScope: opts.includeScope(), + active: true, + gitProtocol: gitProtocol, + hostname: hostname, + showToken: showToken, + token: activeUserToken, + tokenSource: activeUserTokenSource, + username: activeUser, }) statuses[hostname] = append(statuses[hostname], entry) @@ -268,14 +267,13 @@ func statusRun(opts *StatusOptions) error { } token, tokenSource, _ := authCfg.TokenForUser(hostname, username) entry := buildEntry(httpClient, buildEntryOptions{ - active: false, - gitProtocol: gitProtocol, - hostname: hostname, - showToken: showToken, - token: token, - tokenSource: tokenSource, - username: username, - includeScope: opts.includeScope(), + active: false, + gitProtocol: gitProtocol, + hostname: hostname, + showToken: showToken, + token: token, + tokenSource: tokenSource, + username: username, }) statuses[hostname] = append(statuses[hostname], entry) @@ -350,14 +348,13 @@ func expectScopes(token string) bool { } type buildEntryOptions struct { - active bool - gitProtocol string - hostname string - showToken bool - token string - tokenSource string - username string - includeScope bool + active bool + gitProtocol string + hostname string + showToken bool + token string + tokenSource string + username string } func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { @@ -393,21 +390,19 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { } } - if opts.includeScope { - // Get scopes for token. - scopesHeader, err := shared.GetScopes(httpClient, opts.hostname, opts.token) - if err != nil { - var networkError net.Error - if errors.As(err, &networkError) && networkError.Timeout() { - entry.State = authStateTimeout - return entry - } - - entry.State = authStateError + // Get scopes for token. + scopesHeader, err := shared.GetScopes(httpClient, opts.hostname, opts.token) + if err != nil { + var networkError net.Error + if errors.As(err, &networkError) && networkError.Timeout() { + entry.State = authStateTimeout return entry } - entry.Scopes = scopesHeader + + entry.State = authStateError + return entry } + entry.Scopes = scopesHeader entry.State = authStateSuccess return entry @@ -420,10 +415,3 @@ func authTokenWriteable(src string) bool { func isValidEntry(entry authEntry) bool { return entry.State == authStateSuccess } - -func (opts *StatusOptions) includeScope() bool { - if opts.Exporter == nil { - return true - } - return slices.Contains(opts.Exporter.Fields(), "scopes") -} diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 126e7fa7e..f9c5fb9dd 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -586,6 +586,21 @@ func Test_statusRun(t *testing.T) { login(t, c, "github.com", "monalisa2", "gho_abc123", "https") login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") }, + httpStubs: func(reg *httpmock.Registry) { + // mock for HeaderHasMinimumScopes api requests to github.com + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + + // mock for HeaderHasMinimumScopes api requests to a non-github.com host + reg.Register( + httpmock.REST("GET", "api/v3/"), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + + }, wantOut: `{` + `"ghe.io":[` + `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + @@ -606,6 +621,15 @@ func Test_statusRun(t *testing.T) { login(t, c, "github.com", "monalisa2", "gho_abc123", "https") login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") }, + httpStubs: func(reg *httpmock.Registry) { + // mocks for HeaderHasMinimumScopes api requests to github.com + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + }, wantOut: `{` + `"github.com":[` + `{"active":true,"host":"github.com","login":"monalisa2","state":"success"},` + @@ -623,6 +647,15 @@ func Test_statusRun(t *testing.T) { login(t, c, "github.com", "monalisa2", "gho_abc123", "https") login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") }, + httpStubs: func(reg *httpmock.Registry) { + // mocks for HeaderHasMinimumScopes api requests to github.com + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + reg.Register( + httpmock.REST("GET", "api/v3/"), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + }, wantOut: `{` + `"ghe.io":[` + `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + @@ -671,6 +704,12 @@ func Test_statusRun(t *testing.T) { cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "abc123", "https") }, + httpStubs: func(reg *httpmock.Registry) { + // mocks for HeaderHasMinimumScopes api requests to github.com + reg.Register( + httpmock.REST("GET", ""), + httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) + }, wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","state":"success","token":"abc123"}]}` + "\n", }, } From a69f7a6b53606134951a57c8b989f9e30ea4dc04 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:14:59 +0200 Subject: [PATCH 21/87] simplify exporter usage --- pkg/cmd/auth/status/status.go | 42 +++++++---------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index d80b5e61f..c72dc3341 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -105,29 +105,6 @@ func (e authEntry) ExportData(fields []string) map[string]interface{} { return cmdutil.StructExportData(e, fields) } -type Entry interface { - String(cs *iostreams.ColorScheme) string - ExportData(fields []string) map[string]interface{} -} - -type Entries []Entry - -func (e Entries) Strings(cs *iostreams.ColorScheme) []string { - var out []string - for _, entry := range e { - out = append(out, entry.String(cs)) - } - return out -} - -func (e Entries) ExportData(fields []string) []map[string]interface{} { - var out []map[string]interface{} - for _, entry := range e { - out = append(out, entry.ExportData(fields)) - } - return out -} - type StatusOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams @@ -202,7 +179,7 @@ func statusRun(opts *StatusOptions) error { showToken = true } - statuses := make(map[string]Entries) + statuses := make(map[string][]authEntry) hostnames := authCfg.Hosts() if len(hostnames) == 0 { @@ -284,15 +261,7 @@ func statusRun(opts *StatusOptions) error { } if opts.Exporter != nil { - statusesForExport := make(map[string]interface{}) - - for _, hostname := range hostnames { - if len(statuses[hostname]) > 0 { - statusesForExport[hostname] = statuses[hostname].ExportData(opts.Exporter.Fields()) - } - } - - opts.Exporter.Write(opts.IO, statusesForExport) + opts.Exporter.Write(opts.IO, statuses) return nil } @@ -313,7 +282,12 @@ func statusRun(opts *StatusOptions) error { } prevEntry = true fmt.Fprintf(stream, "%s\n", cs.Bold(hostname)) - fmt.Fprintf(stream, "%s", strings.Join(entries.Strings(cs), "\n")) + for i, entry := range entries { + fmt.Fprintf(stream, "%s", entry.String(cs)) + if i < len(entries)-1 { + fmt.Fprint(stream, "\n") + } + } } return err From 5ae0410bd239edfe4c266fa21a1856f2e3b3ef32 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:25:20 +0200 Subject: [PATCH 22/87] add examples --- pkg/cmd/auth/status/status.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index c72dc3341..37f97aaa4 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -137,6 +137,19 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co To change the active account for a host, see %[1]sgh auth switch%[1]s. `, "`"), + Example: heredoc.Doc(` + # Show authentication status for all accounts on all hosts + $ gh auth status + + # Show authentication status for active accounts on a specific host + $ gh auth status --hostname github.example.com --active + + # Show the authentication status with json output + $ gh auth status --json active,token,host,login + + # Gets the access token of the github.com active account + $ gh auth status -a --json token --jq '.["github.com"][0].token' + `), RunE: func(cmd *cobra.Command, args []string) error { if err := cmdutil.MutuallyExclusive( "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", From e2df8ac1cc9a677346ee0a3c6af86c61b453423e Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:33:27 +0200 Subject: [PATCH 23/87] address copilot comment on parameter order --- pkg/cmd/auth/status/status_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index f9c5fb9dd..f5f280cbb 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -758,9 +758,9 @@ func Test_statusRun(t *testing.T) { } } -func login(t *testing.T, c gh.Config, hostname, username, protocol, token string) { +func login(t *testing.T, c gh.Config, hostname, username, token, protocol string) { t.Helper() - _, err := c.Authentication().Login(hostname, username, protocol, token, false) + _, err := c.Authentication().Login(hostname, username, token, protocol, false) require.NoError(t, err) } From 3cdd3599873a9c536aeeea1e4e992a20fe74374b Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Tue, 16 Sep 2025 21:56:18 +0200 Subject: [PATCH 24/87] remove showToken from authEntry --- pkg/cmd/auth/status/status.go | 27 ++++++++++++--------------- pkg/cmd/auth/status/status_test.go | 9 ++++----- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 37f97aaa4..2acdb1687 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -29,8 +29,6 @@ type authEntry struct { Token string `json:"token"` Scopes string `json:"scopes"` GitProtocol string `json:"gitProtocol"` - - showToken bool } var authFields = []string{ @@ -45,7 +43,7 @@ var authFields = []string{ "gitProtocol", } -func (e authEntry) String(cs *iostreams.ColorScheme) string { +func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { var sb strings.Builder switch e.State { case authStateSuccess: @@ -55,7 +53,7 @@ func (e authEntry) String(cs *iostreams.ColorScheme) string { activeStr := fmt.Sprintf("%v", e.Active) sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) sb.WriteString(fmt.Sprintf(" - Git operations protocol: %s\n", cs.Bold(e.GitProtocol))) - sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(e.displayToken()))) + sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(displayToken(e.Token, showToken)))) if expectScopes(e.Token) { sb.WriteString(fmt.Sprintf(" - Token scopes: %s\n", cs.Bold(displayScopes(e.Scopes)))) @@ -199,6 +197,7 @@ func statusRun(opts *StatusOptions) error { fmt.Fprintf(stderr, "You are not logged into any GitHub hosts. To log in, run: %s\n", cs.Bold("gh auth login")) if opts.Exporter != nil { + // In machine-friendly mode, we always exit with no error. opts.Exporter.Write(opts.IO, struct{}{}) return nil } @@ -209,6 +208,7 @@ func statusRun(opts *StatusOptions) error { fmt.Fprintf(stderr, "You are not logged into any accounts on %s\n", opts.Hostname) if opts.Exporter != nil { + // In machine-friendly mode, we always exit with no error. opts.Exporter.Write(opts.IO, struct{}{}) return nil } @@ -296,7 +296,7 @@ func statusRun(opts *StatusOptions) error { prevEntry = true fmt.Fprintf(stream, "%s\n", cs.Bold(hostname)) for i, entry := range entries { - fmt.Fprintf(stream, "%s", entry.String(cs)) + fmt.Fprintf(stream, "%s", entry.String(cs, showToken)) if i < len(entries)-1 { fmt.Fprint(stream, "\n") } @@ -306,17 +306,17 @@ func statusRun(opts *StatusOptions) error { return err } -func (e authEntry) displayToken() string { - if e.showToken { - return e.Token +func displayToken(token string, printRaw bool) string { + if printRaw { + return token } - if idx := strings.LastIndexByte(e.Token, '_'); idx > -1 { - prefix := e.Token[0 : idx+1] - return prefix + strings.Repeat("*", len(e.Token)-len(prefix)) + if idx := strings.LastIndexByte(token, '_'); idx > -1 { + prefix := token[0 : idx+1] + return prefix + strings.Repeat("*", len(token)-len(prefix)) } - return strings.Repeat("*", len(e.Token)) + return strings.Repeat("*", len(token)) } func displayScopes(scopes string) string { @@ -345,7 +345,6 @@ type buildEntryOptions struct { } func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { - entry := authEntry{ Active: opts.active, Host: opts.hostname, @@ -353,8 +352,6 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { TokenSource: opts.tokenSource, Token: opts.token, GitProtocol: opts.gitProtocol, - - showToken: opts.showToken, } if opts.tokenSource == "oauth_token" { diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index f5f280cbb..ffb2186af 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -22,11 +22,10 @@ import ( func Test_NewCmdStatus(t *testing.T) { tests := []struct { - name string - cli string - wants StatusOptions - wantErr error - wantErrOut string + name string + cli string + wants StatusOptions + wantErr error }{ { name: "no arguments", From 5bb76e832b835eadd2ae7e46a65004522be6b181 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:00:07 +0200 Subject: [PATCH 25/87] examples --- pkg/cmd/auth/status/status.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 2acdb1687..520116bcb 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -136,17 +136,23 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co To change the active account for a host, see %[1]sgh auth switch%[1]s. `, "`"), Example: heredoc.Doc(` - # Show authentication status for all accounts on all hosts + # Display authentication status for all accounts on all hosts $ gh auth status - # Show authentication status for active accounts on a specific host - $ gh auth status --hostname github.example.com --active + # Display authentication status for the active account on a specific host + $ gh auth status --active --hostname github.example.com - # Show the authentication status with json output - $ gh auth status --json active,token,host,login + # Display tokens in plain text + $ gh auth status --show-token - # Gets the access token of the github.com active account - $ gh auth status -a --json token --jq '.["github.com"][0].token' + # Format authentication status as JSON + $ gh auth status --json active,login,host + + # Include plain text token in JSON output + $ gh auth status --json token,login + + # Format output as a flat JSON array + $ gh auth status --json active,host,login --jq add `), RunE: func(cmd *cobra.Command, args []string) error { if err := cmdutil.MutuallyExclusive( From 4449af614c0597f61a45d7e2bb924abf521c2464 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:00:41 +0200 Subject: [PATCH 26/87] fix error missing in json output --- pkg/cmd/auth/status/status.go | 49 ++++++++++++++---------------- pkg/cmd/auth/status/status_test.go | 41 ++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 31 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 520116bcb..d59fa5c9c 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -45,8 +45,7 @@ var authFields = []string{ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { var sb strings.Builder - switch e.State { - case authStateSuccess: + if e.State == authStateSuccess { sb.WriteString( fmt.Sprintf(" %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.Host, cs.Bold(e.Login), e.TokenSource), ) @@ -69,24 +68,18 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { } } } + return sb.String() + } - case authStateTimeout: - if e.Login != "" { - sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) - } else { - sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource)) - } - activeStr := fmt.Sprintf("%v", e.Active) - sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) + if e.Login != "" { + sb.WriteString(fmt.Sprintf(" %s %s to %s account %s (%s)\n", cs.Red("X"), e.Error, e.Host, cs.Bold(e.Login), e.TokenSource)) + } else { + sb.WriteString(fmt.Sprintf(" %s %s to %s using token (%s)\n", cs.Red("X"), e.Error, e.Host, e.TokenSource)) + } + activeStr := fmt.Sprintf("%v", e.Active) + sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) - case authStateError: - if e.Login != "" { - sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) - } else { - sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource)) - } - activeStr := fmt.Sprintf("%v", e.Active) - sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) + if e.State == authStateError { sb.WriteString(fmt.Sprintf(" - The token in %s is invalid.\n", e.TokenSource)) if authTokenWriteable(e.TokenSource) { loginInstructions := fmt.Sprintf("gh auth login -h %s", e.Host) @@ -94,7 +87,6 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { sb.WriteString(fmt.Sprintf(" - To re-authenticate, run: %s\n", cs.Bold(loginInstructions))) sb.WriteString(fmt.Sprintf(" - To forget about this account, run: %s\n", cs.Bold(logoutInstructions))) } - } return sb.String() } @@ -351,24 +343,24 @@ type buildEntryOptions struct { } func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { + tokenSource := opts.tokenSource + if tokenSource == "oauth_token" { + // The go-gh function TokenForHost returns this value as source for tokens read from the + // config file, but we want the file path instead. This attempts to reconstruct it. + tokenSource = filepath.Join(config.ConfigDir(), "hosts.yml") + } entry := authEntry{ Active: opts.active, Host: opts.hostname, Login: opts.username, - TokenSource: opts.tokenSource, + TokenSource: tokenSource, Token: opts.token, GitProtocol: opts.gitProtocol, } - if opts.tokenSource == "oauth_token" { - // The go-gh function TokenForHost returns this value as source for tokens read from the - // config file, but we want the file path instead. This attempts to reconstruct it. - entry.TokenSource = filepath.Join(config.ConfigDir(), "hosts.yml") - } - // If token is not writeable, then it came from an environment variable and // we need to fetch the username as it won't be stored in the config. - if !authTokenWriteable(opts.tokenSource) { + if !authTokenWriteable(tokenSource) { // The httpClient will automatically use the correct token here as // the token from the environment variable take highest precedence. apiClient := api.NewClientFromHTTP(httpClient) @@ -376,6 +368,7 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { entry.Login, err = api.CurrentLoginName(apiClient, opts.hostname) if err != nil { entry.State = authStateError + entry.Error = "Failed to log in" return entry } } @@ -386,10 +379,12 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { var networkError net.Error if errors.As(err, &networkError) && networkError.Timeout() { entry.State = authStateTimeout + entry.Error = "Timeout trying to log in" return entry } entry.State = authStateError + entry.Error = "Failed to log in" return entry } entry.Scopes = scopesHeader diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index ffb2186af..300ed0043 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -663,10 +663,29 @@ func Test_statusRun(t *testing.T) { `{"active":true,"host":"github.com","login":"monalisa2","state":"success"}` + "]}\n", }, + { + name: "token from env with json flag", + opts: StatusOptions{ + Exporter: defaultJsonExporter(), + }, + env: map[string]string{"GH_TOKEN": "gho_abc123"}, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", ""), + httpmock.ScopesResponder("")) + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"monalisa"}}}`)) + }, + wantOut: `{` + + `"github.com":[` + + `{"active":true,"host":"github.com","login":"monalisa","state":"success"}` + + "]}\n", + }, { name: "bad token with json flag", opts: StatusOptions{ - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"scopes"}), + Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), }, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") @@ -675,13 +694,27 @@ func Test_statusRun(t *testing.T) { // mock for HeaderHasMinimumScopes api requests to a non-github.com host reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) }, - wantOut: `{"ghe.io":[{"active":true,"host":"ghe.io","login":"monalisa-ghe","scopes":"","state":"error"}]}` + "\n", + wantOut: `{"ghe.io":[{"active":true,"error":"Failed to log in","host":"ghe.io","login":"monalisa-ghe","state":"error"}]}` + "\n", + }, + { + name: "bad token from env with json flag", + opts: StatusOptions{ + Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), + }, + env: map[string]string{"GH_TOKEN": "gho_abc123"}, + httpStubs: func(reg *httpmock.Registry) { + // mock for HeaderHasMinimumScopes api requests to a non-github.com host + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StatusStringResponse(400, "no bueno")) + }, + wantOut: `{"github.com":[{"active":true,"error":"Failed to log in","host":"github.com","login":"","state":"error"}]}` + "\n", }, { name: "timeout error with json flag", opts: StatusOptions{ Hostname: "github.com", - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"scopes"}), + Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), }, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "abc123", "https") @@ -692,7 +725,7 @@ func Test_statusRun(t *testing.T) { return nil, context.DeadlineExceeded }) }, - wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","scopes":"","state":"timeout"}]}` + "\n", + wantOut: `{"github.com":[{"active":true,"error":"Timeout trying to log in","host":"github.com","login":"monalisa","state":"timeout"}]}` + "\n", }, { name: "token is not masked with json flag", From 914531e6f12df3904137da9c5d61e7f86bb160d9 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:07:11 +0200 Subject: [PATCH 27/87] fixup! examples --- pkg/cmd/auth/status/status.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index d59fa5c9c..45f66d9c4 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -133,8 +133,8 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co # Display authentication status for the active account on a specific host $ gh auth status --active --hostname github.example.com - - # Display tokens in plain text + + # Display tokens in plain text $ gh auth status --show-token # Format authentication status as JSON From 5fddcef0a8f257257fa41fc0916405cca270c4e8 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 23 Sep 2025 15:24:37 +0100 Subject: [PATCH 28/87] fix(auth status): return JSON entries under `hosts` Signed-off-by: Babak K. Shandiz --- pkg/cmd/auth/status/auth_state.go | 28 ----- pkg/cmd/auth/status/status.go | 188 +++++++++++++++-------------- pkg/cmd/auth/status/status_test.go | 170 ++++++++++---------------- 3 files changed, 161 insertions(+), 225 deletions(-) delete mode 100644 pkg/cmd/auth/status/auth_state.go diff --git a/pkg/cmd/auth/status/auth_state.go b/pkg/cmd/auth/status/auth_state.go deleted file mode 100644 index 0c7e47675..000000000 --- a/pkg/cmd/auth/status/auth_state.go +++ /dev/null @@ -1,28 +0,0 @@ -package status - -import "encoding/json" - -type authState int - -const ( - authStateSuccess authState = iota - authStateTimeout - authStateError -) - -func (s authState) String() string { - switch s { - case authStateSuccess: - return "success" - case authStateTimeout: - return "timeout" - case authStateError: - return "error" - default: - return "unknown" - } -} - -func (s authState) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 45f66d9c4..9869d9c79 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -19,40 +19,56 @@ import ( "github.com/spf13/cobra" ) +type authEntryState string + +const ( + authEntryStateSuccess = "success" + authEntryStateTimeout = "timeout" + authEntryStateError = "error" +) + type authEntry struct { - State authState `json:"state"` - Error string `json:"error"` - Active bool `json:"active"` - Host string `json:"host"` - Login string `json:"login"` - TokenSource string `json:"tokenSource"` - Token string `json:"token"` - Scopes string `json:"scopes"` - GitProtocol string `json:"gitProtocol"` + State authEntryState `json:"state"` + Error string `json:"error,omitempty"` + Active bool `json:"active"` + Host string `json:"host"` + Login string `json:"login"` + TokenSource string `json:"tokenSource"` + Token string `json:"token,omitempty"` + Scopes string `json:"scopes,omitempty"` + GitProtocol string `json:"gitProtocol"` } -var authFields = []string{ - "state", - "error", - "active", - "host", - "login", - "tokenSource", - "token", - "scopes", - "gitProtocol", +type authStatus struct { + Hosts map[string][]authEntry `json:"hosts"` } -func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { +func newAuthStatus() *authStatus { + return &authStatus{ + Hosts: make(map[string][]authEntry), + } +} + +var authStatusFields = []string{ + "hosts", +} + +func (a authStatus) ExportData(fields []string) map[string]interface{} { + return cmdutil.StructExportData(a, fields) +} + +func (e authEntry) String(cs *iostreams.ColorScheme) string { var sb strings.Builder - if e.State == authStateSuccess { + + switch e.State { + case authEntryStateSuccess: sb.WriteString( fmt.Sprintf(" %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.Host, cs.Bold(e.Login), e.TokenSource), ) activeStr := fmt.Sprintf("%v", e.Active) sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) sb.WriteString(fmt.Sprintf(" - Git operations protocol: %s\n", cs.Bold(e.GitProtocol))) - sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(displayToken(e.Token, showToken)))) + sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(e.Token))) if expectScopes(e.Token) { sb.WriteString(fmt.Sprintf(" - Token scopes: %s\n", cs.Bold(displayScopes(e.Scopes)))) @@ -68,18 +84,15 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { } } } - return sb.String() - } - if e.Login != "" { - sb.WriteString(fmt.Sprintf(" %s %s to %s account %s (%s)\n", cs.Red("X"), e.Error, e.Host, cs.Bold(e.Login), e.TokenSource)) - } else { - sb.WriteString(fmt.Sprintf(" %s %s to %s using token (%s)\n", cs.Red("X"), e.Error, e.Host, e.TokenSource)) - } - activeStr := fmt.Sprintf("%v", e.Active) - sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) - - if e.State == authStateError { + case authEntryStateError: + if e.Login != "" { + sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) + } else { + sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource)) + } + activeStr := fmt.Sprintf("%v", e.Active) + sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) sb.WriteString(fmt.Sprintf(" - The token in %s is invalid.\n", e.TokenSource)) if authTokenWriteable(e.TokenSource) { loginInstructions := fmt.Sprintf("gh auth login -h %s", e.Host) @@ -87,12 +100,18 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { sb.WriteString(fmt.Sprintf(" - To re-authenticate, run: %s\n", cs.Bold(loginInstructions))) sb.WriteString(fmt.Sprintf(" - To forget about this account, run: %s\n", cs.Bold(logoutInstructions))) } - } - return sb.String() -} -func (e authEntry) ExportData(fields []string) map[string]interface{} { - return cmdutil.StructExportData(e, fields) + case authEntryStateTimeout: + if e.Login != "" { + sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) + } else { + sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource)) + } + activeStr := fmt.Sprintf("%v", e.Active) + sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) + } + + return sb.String() } type StatusOptions struct { @@ -138,22 +157,15 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co $ gh auth status --show-token # Format authentication status as JSON - $ gh auth status --json active,login,host + $ gh auth status --json hosts # Include plain text token in JSON output - $ gh auth status --json token,login + $ gh auth status --json hosts --show-token - # Format output as a flat JSON array - $ gh auth status --json active,host,login --jq add + # Format hosts as a flat JSON array + $ gh auth status --json hosts --jq '.hosts | add' `), RunE: func(cmd *cobra.Command, args []string) error { - if err := cmdutil.MutuallyExclusive( - "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", - opts.Exporter != nil, - opts.ShowToken, - ); err != nil { - return err - } if runF != nil { return runF(opts) } @@ -167,7 +179,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only") // the json flags are intentionally not given a shorthand to avoid conflict with -t/--show-token - cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authFields) + cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authStatusFields) return cmd } @@ -183,20 +195,13 @@ func statusRun(opts *StatusOptions) error { stdout := opts.IO.Out cs := opts.IO.ColorScheme() - showToken := opts.ShowToken - if opts.Exporter != nil && slices.Contains(opts.Exporter.Fields(), "token") { - showToken = true - } - - statuses := make(map[string][]authEntry) - hostnames := authCfg.Hosts() if len(hostnames) == 0 { fmt.Fprintf(stderr, "You are not logged into any GitHub hosts. To log in, run: %s\n", cs.Bold("gh auth login")) if opts.Exporter != nil { // In machine-friendly mode, we always exit with no error. - opts.Exporter.Write(opts.IO, struct{}{}) + opts.Exporter.Write(opts.IO, newAuthStatus()) return nil } return cmdutil.SilentError @@ -207,7 +212,7 @@ func statusRun(opts *StatusOptions) error { "You are not logged into any accounts on %s\n", opts.Hostname) if opts.Exporter != nil { // In machine-friendly mode, we always exit with no error. - opts.Exporter.Write(opts.IO, struct{}{}) + opts.Exporter.Write(opts.IO, newAuthStatus()) return nil } return cmdutil.SilentError @@ -218,6 +223,9 @@ func statusRun(opts *StatusOptions) error { return err } + var finalErr error + statuses := newAuthStatus() + for _, hostname := range hostnames { if opts.Hostname != "" && opts.Hostname != hostname { continue @@ -233,15 +241,14 @@ func statusRun(opts *StatusOptions) error { active: true, gitProtocol: gitProtocol, hostname: hostname, - showToken: showToken, token: activeUserToken, tokenSource: activeUserTokenSource, username: activeUser, }) - statuses[hostname] = append(statuses[hostname], entry) + statuses.Hosts[hostname] = append(statuses.Hosts[hostname], entry) - if err == nil && !isValidEntry(entry) { - err = cmdutil.SilentError + if finalErr == nil && entry.State != authEntryStateSuccess { + finalErr = cmdutil.SilentError } if opts.Active { @@ -258,33 +265,46 @@ func statusRun(opts *StatusOptions) error { active: false, gitProtocol: gitProtocol, hostname: hostname, - showToken: showToken, token: token, tokenSource: tokenSource, username: username, }) - statuses[hostname] = append(statuses[hostname], entry) + statuses.Hosts[hostname] = append(statuses.Hosts[hostname], entry) - if err == nil && !isValidEntry(entry) { - err = cmdutil.SilentError + if finalErr == nil && entry.State != authEntryStateSuccess { + finalErr = cmdutil.SilentError + } + } + } + + if !opts.ShowToken { + for _, host := range statuses.Hosts { + for i := range host { + if opts.Exporter != nil { + // In machine-readable we just drop the token + host[i].Token = "" + } else { + host[i].Token = maskToken(host[i].Token) + } } } } if opts.Exporter != nil { + // In machine-friendly mode, we always exit with no error. opts.Exporter.Write(opts.IO, statuses) return nil } prevEntry := false for _, hostname := range hostnames { - entries, ok := statuses[hostname] + entries, ok := statuses.Hosts[hostname] if !ok { continue } stream := stdout - if err != nil { + if finalErr != nil { stream = stderr } @@ -294,26 +314,21 @@ func statusRun(opts *StatusOptions) error { prevEntry = true fmt.Fprintf(stream, "%s\n", cs.Bold(hostname)) for i, entry := range entries { - fmt.Fprintf(stream, "%s", entry.String(cs, showToken)) + fmt.Fprintf(stream, "%s", entry.String(cs)) if i < len(entries)-1 { fmt.Fprint(stream, "\n") } } } - return err + return finalErr } -func displayToken(token string, printRaw bool) string { - if printRaw { - return token - } - +func maskToken(token string) string { if idx := strings.LastIndexByte(token, '_'); idx > -1 { prefix := token[0 : idx+1] return prefix + strings.Repeat("*", len(token)-len(prefix)) } - return strings.Repeat("*", len(token)) } @@ -336,7 +351,6 @@ type buildEntryOptions struct { active bool gitProtocol string hostname string - showToken bool token string tokenSource string username string @@ -367,8 +381,8 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { var err error entry.Login, err = api.CurrentLoginName(apiClient, opts.hostname) if err != nil { - entry.State = authStateError - entry.Error = "Failed to log in" + entry.State = authEntryStateError + entry.Error = err.Error() return entry } } @@ -378,25 +392,21 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { if err != nil { var networkError net.Error if errors.As(err, &networkError) && networkError.Timeout() { - entry.State = authStateTimeout - entry.Error = "Timeout trying to log in" + entry.State = authEntryStateTimeout + entry.Error = err.Error() return entry } - entry.State = authStateError - entry.Error = "Failed to log in" + entry.State = authEntryStateError + entry.Error = err.Error() return entry } entry.Scopes = scopesHeader - entry.State = authStateSuccess + entry.State = authEntryStateSuccess return entry } func authTokenWriteable(src string) bool { return !strings.HasSuffix(src, "_TOKEN") } - -func isValidEntry(entry authEntry) bool { - return entry.State == authStateSuccess -} diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 300ed0043..71b7ea4e2 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -22,10 +22,9 @@ import ( func Test_NewCmdStatus(t *testing.T) { tests := []struct { - name string - cli string - wants StatusOptions - wantErr error + name string + cli string + wants StatusOptions }{ { name: "no arguments", @@ -53,11 +52,6 @@ func Test_NewCmdStatus(t *testing.T) { Active: true, }, }, - { - name: "both --show-token and --json flags", - cli: "--show-token --json state,token", - wantErr: cmdutil.FlagErrorf("`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`."), - }, } for _, tt := range tests { @@ -82,30 +76,18 @@ func Test_NewCmdStatus(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() - if tt.wantErr == nil { - assert.NoError(t, err) + assert.NoError(t, err) - assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) - assert.Equal(t, tt.wants.ShowToken, gotOpts.ShowToken) - assert.Equal(t, tt.wants.Active, gotOpts.Active) - } else { - assert.Equal(t, tt.wantErr, err) - } + assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) + assert.Equal(t, tt.wants.ShowToken, gotOpts.ShowToken) + assert.Equal(t, tt.wants.Active, gotOpts.Active) }) } } func TestJSONFields(t *testing.T) { jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdStatus, []string{ - "state", - "error", - "active", - "host", - "login", - "tokenSource", - "token", - "scopes", - "gitProtocol", + "hosts", }) } @@ -113,6 +95,7 @@ func Test_statusRun(t *testing.T) { tests := []struct { name string opts StatusOptions + jsonFields []string env map[string]string httpStubs func(*httpmock.Registry) cfgStubs func(*testing.T, gh.Config) @@ -556,30 +539,30 @@ func Test_statusRun(t *testing.T) { `), }, { - name: "No tokens with json flag", - opts: StatusOptions{ - Exporter: defaultJsonExporter(), - }, - wantOut: "{}\n", + name: "json, no tokens", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, + wantOut: "{\"hosts\":{}}\n", wantErrOut: "You are not logged into any GitHub hosts. To log in, run: gh auth login\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "No token for the given --hostname with json flag", + name: "json, no token for given --hostname", opts: StatusOptions{ Hostname: "foo.com", - Exporter: defaultJsonExporter(), }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") }, - wantOut: "{}\n", + wantOut: "{\"hosts\":{}}\n", wantErrOut: "You are not logged into any accounts on foo.com\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "All valid tokens with json flag", - opts: StatusOptions{ - Exporter: defaultJsonExporter(), - }, + name: "json, all valid tokens", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") login(t, c, "github.com", "monalisa2", "gho_abc123", "https") @@ -598,23 +581,15 @@ func Test_statusRun(t *testing.T) { reg.Register( httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - }, - wantOut: `{` + - `"ghe.io":[` + - `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + - `],` + - `"github.com":[` + - `{"active":true,"host":"github.com","login":"monalisa2","state":"success"},` + - `{"active":false,"host":"github.com","login":"monalisa","state":"success"}` + - "]}\n", + wantOut: `{"hosts":{"ghe.io":[{"state":"success","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}],"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"},{"state":"success","active":false,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n", }, { - name: "All valid tokens with hostname and json flag", + name: "json, all valid tokens with hostname", opts: StatusOptions{ Hostname: "github.com", - Exporter: defaultJsonExporter(), }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") login(t, c, "github.com", "monalisa2", "gho_abc123", "https") @@ -629,18 +604,14 @@ func Test_statusRun(t *testing.T) { httpmock.REST("GET", ""), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) }, - wantOut: `{` + - `"github.com":[` + - `{"active":true,"host":"github.com","login":"monalisa2","state":"success"},` + - `{"active":false,"host":"github.com","login":"monalisa","state":"success"}` + - "]}\n", + wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"},{"state":"success","active":false,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n", }, { - name: "All valid tokens with active and json flag", + name: "json, all valid tokens with active", opts: StatusOptions{ - Active: true, - Exporter: defaultJsonExporter(), + Active: true, }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") login(t, c, "github.com", "monalisa2", "gho_abc123", "https") @@ -655,20 +626,13 @@ func Test_statusRun(t *testing.T) { httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) }, - wantOut: `{` + - `"ghe.io":[` + - `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + - `],` + - `"github.com":[` + - `{"active":true,"host":"github.com","login":"monalisa2","state":"success"}` + - "]}\n", + wantOut: `{"hosts":{"ghe.io":[{"state":"success","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}],"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n", }, { - name: "token from env with json flag", - opts: StatusOptions{ - Exporter: defaultJsonExporter(), - }, - env: map[string]string{"GH_TOKEN": "gho_abc123"}, + name: "json, token from env", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, + env: map[string]string{"GH_TOKEN": "gho_abc123"}, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", ""), @@ -677,16 +641,12 @@ func Test_statusRun(t *testing.T) { httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"monalisa"}}}`)) }, - wantOut: `{` + - `"github.com":[` + - `{"active":true,"host":"github.com","login":"monalisa","state":"success"}` + - "]}\n", + wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_TOKEN","gitProtocol":"https"}]}}` + "\n", }, { - name: "bad token with json flag", - opts: StatusOptions{ - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), - }, + name: "json, bad token", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") }, @@ -694,28 +654,29 @@ func Test_statusRun(t *testing.T) { // mock for HeaderHasMinimumScopes api requests to a non-github.com host reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) }, - wantOut: `{"ghe.io":[{"active":true,"error":"Failed to log in","host":"ghe.io","login":"monalisa-ghe","state":"error"}]}` + "\n", + wantOut: `{"hosts":{"ghe.io":[{"state":"error","error":"HTTP 400 (https://ghe.io/api/v3/)","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","gitProtocol":"https"}]}}` + "\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "bad token from env with json flag", - opts: StatusOptions{ - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), - }, - env: map[string]string{"GH_TOKEN": "gho_abc123"}, + name: "json, bad token from env", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, + env: map[string]string{"GH_TOKEN": "gho_abc123"}, httpStubs: func(reg *httpmock.Registry) { // mock for HeaderHasMinimumScopes api requests to a non-github.com host reg.Register( httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StatusStringResponse(400, "no bueno")) + httpmock.StatusStringResponse(400, `no bueno`)) }, - wantOut: `{"github.com":[{"active":true,"error":"Failed to log in","host":"github.com","login":"","state":"error"}]}` + "\n", + wantOut: `{"hosts":{"github.com":[{"state":"error","error":"non-200 OK status code: body: \"no bueno\"","active":true,"host":"github.com","login":"","tokenSource":"GH_TOKEN","gitProtocol":"https"}]}}` + "\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "timeout error with json flag", + name: "json, timeout error", opts: StatusOptions{ Hostname: "github.com", - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "abc123", "https") }, @@ -725,14 +686,16 @@ func Test_statusRun(t *testing.T) { return nil, context.DeadlineExceeded }) }, - wantOut: `{"github.com":[{"active":true,"error":"Timeout trying to log in","host":"github.com","login":"monalisa","state":"timeout"}]}` + "\n", + wantOut: `{"hosts":{"github.com":[{"state":"timeout","error":"Get \"https://api.github.com/\": context deadline exceeded","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","gitProtocol":"https"}]}}` + "\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "token is not masked with json flag", + name: "json, with show token", opts: StatusOptions{ - Hostname: "github.com", - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"token"}), + Hostname: "github.com", + ShowToken: true, }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "abc123", "https") }, @@ -742,18 +705,18 @@ func Test_statusRun(t *testing.T) { httpmock.REST("GET", ""), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) }, - wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","state":"success","token":"abc123"}]}` + "\n", + wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","token":"abc123","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() - ios.SetStdinTTY(true) ios.SetStderrTTY(true) ios.SetStdoutTTY(true) tt.opts.IO = ios + cfg, _ := config.NewIsolatedTestConfig(t) if tt.cfgStubs != nil { tt.cfgStubs(t, cfg) @@ -771,6 +734,12 @@ func Test_statusRun(t *testing.T) { tt.httpStubs(reg) } + if tt.jsonFields != nil { + jsonExporter := cmdutil.NewJSONExporter() + jsonExporter.SetFields(tt.jsonFields) + tt.opts.Exporter = jsonExporter + } + for k, v := range tt.env { t.Setenv(k, v) } @@ -795,18 +764,3 @@ func login(t *testing.T, c gh.Config, hostname, username, token, protocol string _, err := c.Authentication().Login(hostname, username, token, protocol, false) require.NoError(t, err) } - -type exporter interface { - cmdutil.Exporter - SetFields(fields []string) -} - -func addFieldsToExporter(e exporter, fields []string) exporter { - newFields := append(e.Fields(), fields...) - e.SetFields(newFields) - return e -} -func defaultJsonExporter() exporter { - return addFieldsToExporter(cmdutil.NewJSONExporter(), []string{"login", "host", "state", "active"}) - -} From e31136a6770b86857582e214edf25403e83c28a5 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 23 Sep 2025 15:42:25 +0100 Subject: [PATCH 29/87] docs(auth status): explain `--json` will always exit with zero Signed-off-by: Babak K. Shandiz --- pkg/cmd/auth/status/status.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 9869d9c79..348b9531d 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -141,8 +141,10 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co For each host, the authentication state of each known account is tested and any issues are included in the output. Each host section will indicate the active account, which will be used when targeting that host. + If an account on any host (or only the one given via %[1]s--hostname%[1]s) has authentication issues, - the command will exit with 1 and output to stderr. + the command will exit with 1 and output to stderr. Note that when using the %[1]s--json%[1]s option, the command + will always exit with zero regardless of any authentication issues, unless there is a fatal error. To change the active account for a host, see %[1]sgh auth switch%[1]s. `, "`"), From c1969f052340c461e18781ba8fa77ab5fefe69ec Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Tue, 23 Sep 2025 16:45:26 -0400 Subject: [PATCH 30/87] remove hidden value for release verify cmd --- pkg/cmd/release/verify-asset/verify_asset.go | 3 +-- pkg/cmd/release/verify/verify.go | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/release/verify-asset/verify_asset.go b/pkg/cmd/release/verify-asset/verify_asset.go index cad436eaa..6bd975f72 100644 --- a/pkg/cmd/release/verify-asset/verify_asset.go +++ b/pkg/cmd/release/verify-asset/verify_asset.go @@ -55,8 +55,7 @@ func NewCmdVerifyAsset(f *cmdutil.Factory, runF func(*VerifyAssetConfig) error) * The asset's digest matches the subject in the attestation * The attestation is associated with the specified release `), - Hidden: true, - Args: cobra.MaximumNArgs(2), + Args: cobra.MaximumNArgs(2), Example: heredoc.Doc(` # Verify an asset from the latest release $ gh release verify-asset ./dist/my-asset.zip diff --git a/pkg/cmd/release/verify/verify.go b/pkg/cmd/release/verify/verify.go index f0b92677a..99cf54e48 100644 --- a/pkg/cmd/release/verify/verify.go +++ b/pkg/cmd/release/verify/verify.go @@ -41,10 +41,9 @@ func NewCmdVerify(f *cmdutil.Factory, runF func(config *VerifyConfig) error) *co opts := &VerifyOptions{} cmd := &cobra.Command{ - Use: "verify []", - Short: "Verify the attestation for a GitHub Release.", - Hidden: true, - Args: cobra.MaximumNArgs(1), + Use: "verify []", + Short: "Verify the attestation for a GitHub Release.", + Args: cobra.MaximumNArgs(1), Long: heredoc.Doc(` Verify that a GitHub Release is accompanied by a valid cryptographically signed attestation. From f5cf156af117d004352914b6a6db079ee765880e Mon Sep 17 00:00:00 2001 From: juejinyuxitu Date: Wed, 24 Sep 2025 23:11:17 +0800 Subject: [PATCH 31/87] refactor: use strings.FieldsFuncSeq to reduce memory allocations Signed-off-by: juejinyuxitu --- pkg/cmd/gist/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index a4fd245f6..9dd2bfba5 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -250,7 +250,7 @@ func printContent(io *iostreams.IOStreams, gists []shared.Gist, filter *regexp.R } if file.Content != "" { - for _, line := range strings.FieldsFunc(file.Content, split) { + for line := range strings.FieldsFuncSeq(file.Content, split) { if filter.MatchString(line) { if line, err = highlightMatch(line, filter, &matched, normal, cs.Highlight); err != nil { return err From a2034545cc2611b419e8438340378056deb88b5a Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Wed, 24 Sep 2025 14:04:28 -0400 Subject: [PATCH 32/87] update the description --- pkg/cmd/release/verify-asset/verify_asset.go | 12 +++--------- pkg/cmd/release/verify/verify.go | 10 +++------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/release/verify-asset/verify_asset.go b/pkg/cmd/release/verify-asset/verify_asset.go index 6bd975f72..43cdec990 100644 --- a/pkg/cmd/release/verify-asset/verify_asset.go +++ b/pkg/cmd/release/verify-asset/verify_asset.go @@ -40,20 +40,14 @@ func NewCmdVerifyAsset(f *cmdutil.Factory, runF func(*VerifyAssetConfig) error) cmd := &cobra.Command{ Use: "verify-asset [] ", - Short: "Verify that a given asset originated from a specific GitHub Release.", + Short: "Verify that a given asset originated from a release", Long: heredoc.Doc(` Verify that a given asset file originated from a specific GitHub Release using cryptographically signed attestations. - ## Understanding Verification - An attestation is a claim made by GitHub regarding a release and its assets. - ## What This Command Does - - This command checks that the asset you provide matches an attestation produced by GitHub for a particular release. - It ensures the asset's integrity by validating: - * The asset's digest matches the subject in the attestation - * The attestation is associated with the specified release + This command checks that the asset you provide matches a valid attestation for the specified release (or the latest release, if no tag is given). + It ensures the asset's integrity by validating that the asset's digest matches the subject in the attestation and that the attestation is associated with the release. `), Args: cobra.MaximumNArgs(2), Example: heredoc.Doc(` diff --git a/pkg/cmd/release/verify/verify.go b/pkg/cmd/release/verify/verify.go index 99cf54e48..2654977f7 100644 --- a/pkg/cmd/release/verify/verify.go +++ b/pkg/cmd/release/verify/verify.go @@ -42,19 +42,15 @@ func NewCmdVerify(f *cmdutil.Factory, runF func(config *VerifyConfig) error) *co cmd := &cobra.Command{ Use: "verify []", - Short: "Verify the attestation for a GitHub Release.", + Short: "Verify the attestation for a release", Args: cobra.MaximumNArgs(1), Long: heredoc.Doc(` Verify that a GitHub Release is accompanied by a valid cryptographically signed attestation. - ## Understanding Verification - An attestation is a claim made by GitHub regarding a release and its assets. - ## What This Command Does - - This command checks that the specified release (or the latest release, if no tag is given) has a valid attestation. - It fetches the attestation for the release and prints out metadata about all assets referenced in the attestation, including their digests. + This command checks that the specified release (or the latest release, if no tag is given) has a valid attestation. + It fetches the attestation for the release and prints metadata about all assets referenced in the attestation, including their digests. `), Example: heredoc.Doc(` # Verify the latest release From 38d6a83e35e055af4cfbccbf9ad3abadfab2a58f Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 25 Sep 2025 10:46:44 +0100 Subject: [PATCH 33/87] test(auth status): correctly replace JSON-escaped paths Signed-off-by: Babak K. Shandiz --- pkg/cmd/auth/status/status_test.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 71b7ea4e2..4246b1e86 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -3,6 +3,7 @@ package status import ( "bytes" "context" + "encoding/json" "net/http" "path/filepath" "strings" @@ -750,8 +751,9 @@ func Test_statusRun(t *testing.T) { } else { require.NoError(t, err) } - output := strings.ReplaceAll(stdout.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/") - errorOutput := strings.ReplaceAll(stderr.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/") + + output := replaceAll(stdout.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/") + errorOutput := replaceAll(stderr.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/") require.Equal(t, tt.wantErrOut, errorOutput) require.Equal(t, tt.wantOut, output) @@ -764,3 +766,19 @@ func login(t *testing.T, c gh.Config, hostname, username, token, protocol string _, err := c.Authentication().Login(hostname, username, token, protocol, false) require.NoError(t, err) } + +// replaceAll replaces all instances of old with new in s, as well as all instances +// of the JSON-escaped version of old with the JSON-escaped version of new. +// This is because when the test is run on Windows the paths will have backslashes +// escaped in JSON and a simple strings.ReplaceAll won't catch them. +func replaceAll(s string, old string, new string) string { + jsonEscapedOld, _ := json.Marshal(old) + jsonEscapedOld = jsonEscapedOld[1 : len(jsonEscapedOld)-1] + + jsonEscapedNew, _ := json.Marshal(new) + jsonEscapedNew = jsonEscapedNew[1 : len(jsonEscapedNew)-1] + + replaced := strings.ReplaceAll(s, string(jsonEscapedOld), string(jsonEscapedNew)) + replaced = strings.ReplaceAll(replaced, old, new) + return replaced +} From 0e904d5ef5b6cdd54ff9c0b82598327a5aebae24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:13:42 +0000 Subject: [PATCH 34/87] chore(deps): Bump github.com/sigstore/sigstore-go from 1.1.0 to 1.1.3 Bumps [github.com/sigstore/sigstore-go](https://github.com/sigstore/sigstore-go) from 1.1.0 to 1.1.3. - [Release notes](https://github.com/sigstore/sigstore-go/releases) - [Commits](https://github.com/sigstore/sigstore-go/compare/v1.1.0...v1.1.3) --- updated-dependencies: - dependency-name: github.com/sigstore/sigstore-go dependency-version: 1.1.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 79 +++++++++-------- go.sum | 273 +++++++++++++++++++++++++-------------------------------- 2 files changed, 163 insertions(+), 189 deletions(-) diff --git a/go.mod b/go.mod index 518cae19c..986f2dcc6 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/atotto/clipboard v0.1.4 github.com/briandowns/spinner v1.23.2 github.com/cenkalti/backoff/v4 v4.3.0 - github.com/cenkalti/backoff/v5 v5.0.2 + github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/glamour v0.10.0 github.com/charmbracelet/huh v0.7.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 @@ -24,7 +24,7 @@ require ( github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea github.com/distribution/reference v0.6.0 github.com/gabriel-vasile/mimetype v1.4.9 - github.com/gdamore/tcell/v2 v2.8.1 + github.com/gdamore/tcell/v2 v2.9.0 github.com/golang/snappy v1.0.0 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.6 @@ -46,20 +46,20 @@ require ( github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 github.com/sigstore/protobuf-specs v0.5.0 - github.com/sigstore/sigstore-go v1.1.0 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.7 - github.com/stretchr/testify v1.11.0 - github.com/theupdateframework/go-tuf/v2 v2.1.1 + github.com/sigstore/sigstore-go v1.1.3 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + github.com/theupdateframework/go-tuf/v2 v2.2.0 github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/yuin/goldmark v1.7.13 github.com/zalando/go-keyring v0.2.6 - golang.org/x/crypto v0.41.0 - golang.org/x/sync v0.16.0 - golang.org/x/term v0.34.0 - golang.org/x/text v0.28.0 + golang.org/x/crypto v0.42.0 + golang.org/x/sync v0.17.0 + golang.org/x/term v0.35.0 + golang.org/x/text v0.29.0 google.golang.org/grpc v1.75.0 - google.golang.org/protobuf v1.36.8 + google.golang.org/protobuf v1.36.9 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -67,15 +67,15 @@ require ( require ( al.essio.dev/pkg/shellescape v1.6.0 // indirect cel.dev/expr v0.24.0 // indirect - cloud.google.com/go v0.121.4 // indirect + cloud.google.com/go v0.121.6 // indirect cloud.google.com/go/auth v0.16.5 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.8.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect - cloud.google.com/go/spanner v1.82.0 // indirect - cloud.google.com/go/storage v1.55.0 // indirect + cloud.google.com/go/spanner v1.84.1 // indirect + cloud.google.com/go/storage v1.56.1 // indirect dario.cat/mergo v1.0.2 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect @@ -86,7 +86,6 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/alecthomas/chroma/v2 v2.19.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/avast/retry-go/v4 v4.6.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect @@ -120,8 +119,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect - github.com/globocom/go-buffer v1.2.2 // indirect - github.com/go-chi/chi/v5 v5.2.2 // indirect + github.com/go-chi/chi/v5 v5.2.3 // indirect github.com/go-jose/go-jose/v4 v4.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -133,7 +131,18 @@ require ( github.com/go-openapi/runtime v0.28.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect @@ -144,7 +153,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -187,10 +196,10 @@ require ( github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect - github.com/sigstore/rekor v1.4.1 // indirect - github.com/sigstore/rekor-tiles v0.1.7-0.20250624231741-98cd4a77300f // indirect - github.com/sigstore/sigstore v1.9.5 // indirect - github.com/sigstore/timestamp-authority v1.2.8 // indirect + github.com/sigstore/rekor v1.4.2 // indirect + github.com/sigstore/rekor-tiles v0.1.11 // indirect + github.com/sigstore/sigstore v1.9.6-0.20250729224751-181c5d3339b3 // indirect + github.com/sigstore/timestamp-authority v1.2.9 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect @@ -204,7 +213,7 @@ require ( github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect - github.com/transparency-dev/tessera v0.2.1-0.20250610150926-8ee4e93b2823 // indirect + github.com/transparency-dev/tessera v1.0.0-rc3 // indirect github.com/vbatts/tar-split v0.12.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect @@ -213,26 +222,26 @@ require ( go.mongodb.org/mongo-driver v1.17.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/sdk v1.37.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/mod v0.27.0 // indirect + golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/sys v0.36.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.35.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/api v0.248.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect k8s.io/klog/v2 v2.130.1 // indirect ) diff --git a/go.sum b/go.sum index 7ad0f6745..6ebd3d295 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs= -cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s= +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -532,8 +532,8 @@ cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+ cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/spanner v1.82.0 h1:w9uO8RqEoBooBLX4nqV1RtgudyU2ZX780KTLRgeVg60= -cloud.google.com/go/spanner v1.82.0/go.mod h1:BzybQHFQ/NqGxvE/M+/iU29xgutJf7Q85/4U9RWMto0= +cloud.google.com/go/spanner v1.84.1 h1:ShH4Y3YeDtmHa55dFiSS3YtQ0dmCuP0okfAoHp/d68w= +cloud.google.com/go/spanner v1.84.1/go.mod h1:3GMEIjOcXINJSvb42H3M6TdlGCDzaCFpiiNQpjHPlCM= cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= @@ -551,8 +551,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= -cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/storage v1.56.1 h1:n6gy+yLnHn0hTwBFzNn8zJ1kqWfR91wzdM8hjRF4wP0= +cloud.google.com/go/storage v1.56.1/go.mod h1:C9xuCZgFl3buo2HZU/1FncgvvOgTAs/rnh4gF4lMg0s= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= @@ -686,36 +686,34 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= -github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU= -github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= -github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4= -github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I= -github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM= -github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc= +github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= +github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/config v1.31.3 h1:RIb3yr/+PZ18YYNe6MDiG/3jVoJrPmdoCARwNkMGvco= +github.com/aws/aws-sdk-go-v2/config v1.31.3/go.mod h1:jjgx1n7x0FAKl6TnakqrpkHWWKcX3xfWtdnIJs5K9CE= +github.com/aws/aws-sdk-go-v2/credentials v1.18.7 h1:zqg4OMrKj+t5HlswDApgvAHjxKtlduKS7KicXB+7RLg= +github.com/aws/aws-sdk-go-v2/credentials v1.18.7/go.mod h1:/4M5OidTskkgkv+nCIfC9/tbiQ/c8qTox9QcUDV0cgc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= github.com/aws/aws-sdk-go-v2/service/kms v1.44.0 h1:Z95XCqqSnwXr0AY7PgsiOUBhUG2GoDM5getw6RfD1Lg= github.com/aws/aws-sdk-go-v2/service/kms v1.44.0/go.mod h1:DqcSngL7jJeU1fOzh5Ll5rSvX/MlMV6OZlE4mVdFAQc= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.0 h1:Bnr+fXrlrPEoR1MAFrHVsge3M/WoK4n23VNhRM7TPHI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.0/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -736,8 +734,8 @@ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= @@ -815,6 +813,8 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -882,21 +882,17 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= -github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= -github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= +github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys= +github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/globocom/go-buffer v1.2.2 h1:ICgtlUe5GIYIZFdAVj57+5WYBR4DA56cX+PYZDhGDwc= -github.com/globocom/go-buffer v1.2.2/go.mod h1:kY1ALQS0ChiiThmWhsFoT5CYSiuad0t3keIew5LsWdM= -github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= @@ -930,8 +926,30 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= @@ -1083,8 +1101,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vb github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 h1:+epNPbD5EqgpEMm5wrl4Hqts3jZt8+kYaqUisuuIGTk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -1122,7 +1140,6 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= @@ -1240,20 +1257,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= -github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -1279,15 +1284,15 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -1295,7 +1300,6 @@ github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kA github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= @@ -1318,9 +1322,10 @@ github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGq github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= -github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g= github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= @@ -1333,14 +1338,14 @@ github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZV github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= -github.com/sigstore/rekor v1.4.1 h1:KK3McuHnptIE9mdNlrc9qh/OVE0AXf4rnScMxJE6xH4= -github.com/sigstore/rekor v1.4.1/go.mod h1:/McBsz/vrtfi4EInxSIk/MGbDXzgv2+1FQUg1R/uSnE= -github.com/sigstore/rekor-tiles v0.1.7-0.20250624231741-98cd4a77300f h1:zaqWahYAlVouSm5qwCH+2vZ3eenZFBwzzuBz/IZyy5c= -github.com/sigstore/rekor-tiles v0.1.7-0.20250624231741-98cd4a77300f/go.mod h1:1Epq0PQ73v5Z276rAY241JyaP8gtD64I6sgYIECHPvc= -github.com/sigstore/sigstore v1.9.5 h1:Wm1LT9yF4LhQdEMy5A2JeGRHTrAWGjT3ubE5JUSrGVU= -github.com/sigstore/sigstore v1.9.5/go.mod h1:VtxgvGqCmEZN9X2zhFSOkfXxvKUjpy8RpUW39oCtoII= -github.com/sigstore/sigstore-go v1.1.0 h1:NBfyvL/LiBIplnIZAtC7GtDZ7qj82A/GTpn0+5WV7BM= -github.com/sigstore/sigstore-go v1.1.0/go.mod h1:97lDVpZVBCTFX114KPAManEsShVe934KyaVhZGhPVBM= +github.com/sigstore/rekor v1.4.2 h1:Lx2xby7loviFYdg2C9pB1mESk2QU/LqcYSGsqqZwmg8= +github.com/sigstore/rekor v1.4.2/go.mod h1:nX/OYaLqpTeCOuMEt7ELE0+5cVjZWFnFKM+cZ+3hQRA= +github.com/sigstore/rekor-tiles v0.1.11 h1:0NAJ2EhD1r6DH95FUuDTqUDd+c31LSKzoXGW5ZCzFq0= +github.com/sigstore/rekor-tiles v0.1.11/go.mod h1:eGIeqASh52pgWpmp/j5KZDjmKdVwob7eTYskVVRCu5k= +github.com/sigstore/sigstore v1.9.6-0.20250729224751-181c5d3339b3 h1:IEhSeWfhTd0kaBpHUXniWU2Tl5K5OUACN69mi1WGd+8= +github.com/sigstore/sigstore v1.9.6-0.20250729224751-181c5d3339b3/go.mod h1:JuqyPRJYnkNl6OTnQiG503EUnKih4P5EV6FUw+1B0iA= +github.com/sigstore/sigstore-go v1.1.3 h1:5lKcbXZa5JC7wb/UVywyCulccfYTUju1D5h4tkn+fXE= +github.com/sigstore/sigstore-go v1.1.3/go.mod h1:3jKC4IDh7TEVtCSJCjx0lpq5YfJbDJmfp65WsMvY2mg= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.5 h1:qp2VFyKuFQvTGmZwk5Q7m5nE4NwnF9tHwkyz0gtWAck= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.5/go.mod h1:DKlQjjr+GsWljEYPycI0Sf8URLCk4EbGA9qYjF47j4g= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.5 h1:CRZcdYn5AOptStsLRAAACudAVmb1qUbhMlzrvm7ju3o= @@ -1349,10 +1354,12 @@ github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.6-0.20250729224751-181c5 github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.6-0.20250729224751-181c5d3339b3/go.mod h1:tRtJzSZ48MXJV9bmS8pkb3mP36PCad/Cs+BmVJ3Z4O4= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.5 h1:S2ukEfN1orLKw2wEQIUHDDlzk0YcylhcheeZ5TGk8LI= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.5/go.mod h1:m7sQxVJmDa+rsmS1m6biQxaLX83pzNS7ThUEyjOqkCU= -github.com/sigstore/timestamp-authority v1.2.8 h1:BEV3fkphwU4zBp3allFAhCqQb99HkiyCXB853RIwuEE= -github.com/sigstore/timestamp-authority v1.2.8/go.mod h1:G2/0hAZmLPnevEwT1S9IvtNHUm9Ktzvso6xuRhl94ZY= +github.com/sigstore/timestamp-authority v1.2.9 h1:L9Fj070/EbMC8qUk8BchkrYCS1BT5i93Bl6McwydkFs= +github.com/sigstore/timestamp-authority v1.2.9/go.mod h1:QyRnZchz4o+xdHyK5rvCWacCHxWmpX+mgvJwB1OXcLY= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -1363,11 +1370,11 @@ github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= @@ -1389,14 +1396,14 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= -github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= -github.com/theupdateframework/go-tuf/v2 v2.1.1 h1:OWcoHItwsGO+7m0wLa7FDWPR4oB1cj0zOr1kosE4G+I= -github.com/theupdateframework/go-tuf/v2 v2.1.1/go.mod h1:V675cQGhZONR0OGQ8r1feO0uwtsTBYPDWHzAAPn5rjE= +github.com/theupdateframework/go-tuf/v2 v2.2.0 h1:Hmb+Azgd7IKOZeNJFT2C91y+YZ+F+TeloSIvQIaXCQw= +github.com/theupdateframework/go-tuf/v2 v2.2.0/go.mod h1:CubcJiJlBHQ2YkA5j9hlBO4B+tHFlLjRbWCJCT7EIKU= github.com/thlib/go-timezone-local v0.0.6 h1:Ii3QJ4FhosL/+eCZl6Hsdr4DDU4tfevNoV83yAEo2tU= github.com/thlib/go-timezone-local v0.0.6/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= @@ -1413,8 +1420,8 @@ github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26 h1:YTbkeF github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26/go.mod h1:ODywn0gGarHMMdSkWT56ULoK8Hk71luOyRseKek9COw= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= -github.com/transparency-dev/tessera v0.2.1-0.20250610150926-8ee4e93b2823 h1:s3p7wNrK/mnKI2bdp9PrQd9eBVxo1i5rU6O5hKkN0zc= -github.com/transparency-dev/tessera v0.2.1-0.20250610150926-8ee4e93b2823/go.mod h1:Jv2IDwG1q8QNXZTaI1X6QX8s96WlJn73ka2hT1n4N5c= +github.com/transparency-dev/tessera v1.0.0-rc3 h1:v385KqMekDUKI3ZVJHCHE5MAz8LBrWsEKa6OzYLrz0k= +github.com/transparency-dev/tessera v1.0.0-rc3/go.mod h1:aaLlvG/sEPMzT96iIF4hua6Z9pLzkfDtkbaUAR4IL8I= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -1453,24 +1460,24 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -1493,11 +1500,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1557,14 +1561,10 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1585,7 +1585,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -1621,10 +1620,6 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1674,14 +1669,9 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1691,10 +1681,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1709,7 +1696,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1766,14 +1752,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1782,13 +1762,8 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1805,12 +1780,9 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1880,10 +1852,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2102,8 +2072,8 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 h1:mVXdvnmR3S3BQOqHECm9NGMjYiRtEvDYcqAqedTXY6s= -google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I= +google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -2167,22 +2137,17 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 87468f40db49324d61eac3c91e55d050aa04212c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:50:11 -0600 Subject: [PATCH 35/87] Refactor PR reviewer editing to use REST API and optimize team fetch Switches pull request reviewer add/remove operations from GraphQL to the REST API, enabling separate add and remove calls for reviewers and teams. Refactors reviewer editing logic to avoid fetching organization teams unless required for interactive editing, improving performance for non-interactive flows. Updates tests and supporting code to reflect the new reviewer management and metadata fetching behavior. --- api/queries_pr.go | 60 ++++++-- pkg/cmd/pr/edit/edit.go | 84 +++++++++--- pkg/cmd/pr/edit/edit_test.go | 249 ++++++++++++++++++++++++++-------- pkg/cmd/pr/shared/editable.go | 55 ++------ 4 files changed, 325 insertions(+), 123 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index b3373a903..0d0fed03d 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -1,6 +1,8 @@ package api import ( + "bytes" + "encoding/json" "fmt" "net/http" "net/url" @@ -629,17 +631,55 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter return pr, nil } -func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error { - var mutation struct { - RequestReviews struct { - PullRequest struct { - ID string - } - } `graphql:"requestReviews(input: $input)"` +// AddPullRequestReviews updates the requested reviewers on a pull request using the REST API. +func AddPullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, users, teams []string) error { + if len(users) == 0 && len(teams) == 0 { + return nil } - variables := map[string]interface{}{"input": params} - err := client.Mutate(repo.RepoHost(), "PullRequestUpdateRequestReviews", &mutation, variables) - return err + + path := fmt.Sprintf("repos/%s/%s/pulls/%d/requested_reviewers", repo.RepoOwner(), repo.RepoName(), prNumber) + body := struct { + Reviewers []string `json:"reviewers,omitempty"` + TeamReviewers []string `json:"team_reviewers,omitempty"` + }{ + Reviewers: users, + TeamReviewers: teams, + } + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(body); err != nil { + return err + } + // The endpoint responds with the updated pull request object; we don't need it here. + return client.REST(repo.RepoHost(), "POST", path, buf, nil) +} + +// RemovePullRequestReviews removes requested reviewers from a pull request using the REST API. +func RemovePullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, users, teams []string) error { + if len(users) == 0 && len(teams) == 0 { + return nil + } + + if users == nil { + users = []string{} + } + if teams == nil { + teams = []string{} + } + + path := fmt.Sprintf("repos/%s/%s/pulls/%d/requested_reviewers", repo.RepoOwner(), repo.RepoName(), prNumber) + body := struct { + Reviewers []string `json:"reviewers"` + TeamReviewers []string `json:"team_reviewers"` + }{ + Reviewers: users, + TeamReviewers: teams, + } + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(body); err != nil { + return err + } + // The endpoint responds with the updated pull request object; we don't need it here. + return client.REST(repo.RepoHost(), "DELETE", path, buf, nil) } func UpdatePullRequestBranch(client *Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestBranchInput) error { diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index b196546cd..de65f1c72 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -3,6 +3,8 @@ package edit import ( "fmt" "net/http" + "slices" + "strings" "time" "github.com/MakeNowJust/heredoc" @@ -13,6 +15,7 @@ import ( shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/set" "github.com/shurcooL/githubv4" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -237,7 +240,7 @@ func editRun(opts *EditOptions) error { findOptions := shared.FindOptions{ Selector: opts.SelectorArg, - Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"}, + Fields: []string{"id", "author", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"}, Detector: opts.Detector, } @@ -298,6 +301,18 @@ func editRun(opts *EditOptions) error { } if opts.Interactive { + // Remove PR author from reviewer options + // as it is not a valid option for a reviewer. + // The REST API will return an error if we + // attempt to add the PR author as a reviewer. + // However, the GraphQL API will silently ignore it. + if editable.Reviewers.Edited { + s := set.NewStringSet() + s.AddValues(editable.Reviewers.Options) + s.Remove(pr.Author.Login) + editable.Reviewers.Options = s.ToSlice() + } + editorCommand, err := opts.EditorRetriever.Retrieve() if err != nil { return err @@ -309,7 +324,7 @@ func editRun(opts *EditOptions) error { } opts.IO.StartProgressIndicator() - err = updatePullRequest(httpClient, repo, pr.ID, editable) + err = updatePullRequest(httpClient, repo, pr.ID, pr.Number, editable) opts.IO.StopProgressIndicator() if err != nil { return err @@ -320,36 +335,71 @@ func editRun(opts *EditOptions) error { return nil } -func updatePullRequest(httpClient *http.Client, repo ghrepo.Interface, id string, editable shared.Editable) error { +func updatePullRequest(httpClient *http.Client, repo ghrepo.Interface, id string, number int, editable shared.Editable) error { var wg errgroup.Group wg.Go(func() error { return shared.UpdateIssue(httpClient, repo, id, true, editable) }) if editable.Reviewers.Edited { wg.Go(func() error { - return updatePullRequestReviews(httpClient, repo, id, editable) + return updatePullRequestReviews(httpClient, repo, number, editable) }) } return wg.Wait() } -func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, id string, editable shared.Editable) error { - userIds, teamIds, err := editable.ReviewerIds() - if err != nil { - return err - } - if userIds == nil && teamIds == nil { +func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, number int, editable shared.Editable) error { + if !editable.Reviewers.Edited { return nil } - union := githubv4.Boolean(false) - reviewsRequestParams := githubv4.RequestReviewsInput{ - PullRequestID: id, - Union: &union, - UserIDs: ghIds(userIds), - TeamIDs: ghIds(teamIds), + + // Rebuild the Value slice from non-interactive flag input. + if len(editable.Reviewers.Add) != 0 || len(editable.Reviewers.Remove) != 0 { + s := set.NewStringSet() + s.AddValues(editable.Reviewers.Add) + s.AddValues(editable.Reviewers.Default) + s.RemoveValues(editable.Reviewers.Remove) + editable.Reviewers.Value = s.ToSlice() } + + var addUsers []string + var addTeams []string + for _, r := range editable.Reviewers.Value { + if strings.ContainsRune(r, '/') { + teamSlug := strings.Split(r, "/")[1] + addTeams = append(addTeams, teamSlug) + } else { + addUsers = append(addUsers, r) + } + } + + // Reviewers in Default but not in the Value have been removed interactively. + var toRemove []string + for _, r := range editable.Reviewers.Default { + if !slices.Contains(editable.Reviewers.Value, r) { + toRemove = append(toRemove, r) + } + } + var removeUsers []string + var removeTeams []string + for _, r := range toRemove { + if strings.ContainsRune(r, '/') { + teamSlug := strings.Split(r, "/")[1] + removeTeams = append(removeTeams, teamSlug) + } else { + removeUsers = append(removeUsers, r) + } + } + client := api.NewClientFromHTTP(httpClient) - return api.UpdatePullRequestReviews(client, repo, reviewsRequestParams) + wg := errgroup.Group{} + wg.Go(func() error { + return api.AddPullRequestReviews(client, repo, number, addUsers, addTeams) + }) + wg.Go(func() error { + return api.RemovePullRequestReviews(client, repo, number, removeUsers, removeTeams) + }) + return wg.Wait() } type Surveyor interface { diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index bb468a307..803f506bc 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -354,7 +354,7 @@ func Test_editRun(t *testing.T) { tests := []struct { name string input *EditOptions - httpStubs func(*httpmock.Registry) + httpStubs func(*httpmock.Registry, *testing.T) stdout string stderr string }{ @@ -411,11 +411,11 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry) { - mockRepoMetadata(reg, false) + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true, teamReviewers: false, assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestUpdateActorAssignees(reg) - mockPullRequestReviewersUpdate(reg) + mockPullRequestAddReviewers(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) }, @@ -469,8 +469,8 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry) { - mockRepoMetadata(reg, true) + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + mockRepoMetadata(reg, mockRepoMetadataOptions{assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestUpdateActorAssignees(reg) mockPullRequestUpdateLabels(reg) @@ -483,8 +483,19 @@ func Test_editRun(t *testing.T) { input: &EditOptions{ Detector: &fd.EnabledDetectorMock{}, SelectorArg: "123", - Finder: shared.NewMockFinder("123", &api.PullRequest{ + Finder: shared.NewMockFinder("123", &api.PullRequest{ // include existing reviewers so removal logic triggers URL: "https://github.com/OWNER/REPO/pull/123", + ReviewRequests: api.ReviewRequests{Nodes: []struct{ RequestedReviewer api.RequestedReviewer }{ + {RequestedReviewer: api.RequestedReviewer{TypeName: "Team", Slug: "core", Organization: struct { + Login string `json:"login"` + }{Login: "OWNER"}}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "Team", Slug: "external", Organization: struct { + Login string `json:"login"` + }{Login: "OWNER"}}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "User", Login: "monalisa"}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "User", Login: "hubot"}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "User", Login: "dependabot"}}, + }}, }, ghrepo.New("OWNER", "REPO")), Interactive: false, Editable: shared.Editable{ @@ -501,8 +512,9 @@ func Test_editRun(t *testing.T) { Edited: true, }, Reviewers: shared.EditableSlice{ - Remove: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot", "dependabot"}, - Edited: true, + Default: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot", "dependabot"}, + Remove: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot", "dependabot"}, + Edited: true, }, Assignees: shared.EditableAssignees{ EditableSlice: shared.EditableSlice{ @@ -530,16 +542,107 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry) { - mockRepoMetadata(reg, false) + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true, teamReviewers: false, assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) - mockPullRequestReviewersUpdate(reg) + mockPullRequestRemoveReviewers(reg) mockPullRequestUpdateLabels(reg) mockPullRequestUpdateActorAssignees(reg) mockProjectV2ItemUpdate(reg) }, stdout: "https://github.com/OWNER/REPO/pull/123\n", }, + // Conditional team fetching cases + { + name: "non-interactive add only user reviewers skips team fetch", + input: &EditOptions{ + Detector: &fd.EnabledDetectorMock{}, + SelectorArg: "123", + Finder: shared.NewMockFinder("123", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO")), + Interactive: false, + Editable: shared.Editable{ + Reviewers: shared.EditableSlice{Add: []string{"monalisa", "hubot"}, Edited: true}, + }, + Fetcher: testFetcher{}, + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + // reviewers only (users), no team reviewers fetched + mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) + // explicitly assert that no OrganizationTeamList query occurs + reg.Exclude(t, httpmock.GraphQL(`query OrganizationTeamList\b`)) + mockPullRequestUpdate(reg) + mockPullRequestAddReviewers(reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123\n", + }, + { + name: "non-interactive add contains team reviewers skips team fetch", + input: &EditOptions{ + Detector: &fd.EnabledDetectorMock{}, + SelectorArg: "123", + Finder: shared.NewMockFinder("123", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO")), + Interactive: false, + Editable: shared.Editable{ + Reviewers: shared.EditableSlice{Add: []string{"monalisa", "OWNER/core"}, Edited: true}, + }, + Fetcher: testFetcher{}, + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + // reviewer add includes team but non-interactive Add/Remove provided -> no team fetch + mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) + reg.Exclude(t, httpmock.GraphQL(`query OrganizationTeamList\b`)) + mockPullRequestUpdate(reg) + mockPullRequestAddReviewers(reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123\n", + }, + { + name: "non-interactive reviewers remove contains team skips team fetch", + input: &EditOptions{ + Detector: &fd.EnabledDetectorMock{}, + SelectorArg: "123", + Finder: shared.NewMockFinder("123", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123", ReviewRequests: api.ReviewRequests{Nodes: []struct{ RequestedReviewer api.RequestedReviewer }{ + {RequestedReviewer: api.RequestedReviewer{TypeName: "Team", Slug: "core", Organization: struct { + Login string `json:"login"` + }{Login: "OWNER"}}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "User", Login: "monalisa"}}, + }}}, ghrepo.New("OWNER", "REPO")), + Interactive: false, + Editable: shared.Editable{ + Reviewers: shared.EditableSlice{Remove: []string{"monalisa", "OWNER/core"}, Edited: true}, + }, + Fetcher: testFetcher{}, + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) + reg.Exclude(t, httpmock.GraphQL(`query OrganizationTeamList\b`)) + mockPullRequestUpdate(reg) + mockPullRequestRemoveReviewers(reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123\n", + }, + { + name: "non-interactive mutate reviewers with no change to existing team reviewers skips team fetch", + input: &EditOptions{ + Detector: &fd.EnabledDetectorMock{}, + SelectorArg: "123", + Finder: shared.NewMockFinder("123", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO")), + Interactive: false, + Editable: shared.Editable{ + Reviewers: shared.EditableSlice{Add: []string{"monalisa"}, Remove: []string{"hubot"}, Default: []string{"OWNER/core"}, Edited: true}, + }, + Fetcher: testFetcher{}, + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + // reviewers only (users), no team reviewers fetched + mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) + // explicitly assert that no OrganizationTeamList query occurs + reg.Exclude(t, httpmock.GraphQL(`query OrganizationTeamList\b`)) + mockPullRequestUpdate(reg) + mockPullRequestAddReviewers(reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123\n", + }, { name: "interactive", input: &EditOptions{ @@ -576,11 +679,11 @@ func Test_editRun(t *testing.T) { Fetcher: testFetcher{}, EditorRetriever: testEditorRetriever{}, }, - httpStubs: func(reg *httpmock.Registry) { - mockRepoMetadata(reg, false) + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true, teamReviewers: true, assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestUpdateActorAssignees(reg) - mockPullRequestReviewersUpdate(reg) + mockPullRequestAddReviewers(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) }, @@ -620,8 +723,9 @@ func Test_editRun(t *testing.T) { Fetcher: testFetcher{}, EditorRetriever: testEditorRetriever{}, }, - httpStubs: func(reg *httpmock.Registry) { - mockRepoMetadata(reg, true) + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + // interactive but reviewers not chosen; need everything except reviewers/teams + mockRepoMetadata(reg, mockRepoMetadataOptions{assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestUpdateActorAssignees(reg) mockPullRequestUpdateLabels(reg) @@ -634,8 +738,19 @@ func Test_editRun(t *testing.T) { input: &EditOptions{ Detector: &fd.EnabledDetectorMock{}, SelectorArg: "123", - Finder: shared.NewMockFinder("123", &api.PullRequest{ + Finder: shared.NewMockFinder("123", &api.PullRequest{ // include existing reviewers URL: "https://github.com/OWNER/REPO/pull/123", + ReviewRequests: api.ReviewRequests{Nodes: []struct{ RequestedReviewer api.RequestedReviewer }{ + {RequestedReviewer: api.RequestedReviewer{TypeName: "Team", Slug: "core", Organization: struct { + Login string `json:"login"` + }{Login: "OWNER"}}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "Team", Slug: "external", Organization: struct { + Login string `json:"login"` + }{Login: "OWNER"}}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "User", Login: "monalisa"}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "User", Login: "hubot"}}, + {RequestedReviewer: api.RequestedReviewer{TypeName: "User", Login: "dependabot"}}, + }}, }, ghrepo.New("OWNER", "REPO")), Interactive: true, Surveyor: testSurveyor{ @@ -665,10 +780,10 @@ func Test_editRun(t *testing.T) { Fetcher: testFetcher{}, EditorRetriever: testEditorRetriever{}, }, - httpStubs: func(reg *httpmock.Registry) { - mockRepoMetadata(reg, false) + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true, teamReviewers: true, assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) - mockPullRequestReviewersUpdate(reg) + mockPullRequestRemoveReviewers(reg) mockPullRequestUpdateActorAssignees(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) @@ -712,7 +827,7 @@ func Test_editRun(t *testing.T) { Fetcher: testFetcher{}, EditorRetriever: testEditorRetriever{}, }, - httpStubs: func(reg *httpmock.Registry) { + httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryAssignableActors\b`), httpmock.StringResponse(` @@ -759,7 +874,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry) { + httpStubs: func(reg *httpmock.Registry, t *testing.T) { // Notice there is no call to mockReplaceActorsForAssignable() // and no GraphQL call to RepositoryAssignableActors below. reg.Register( @@ -787,7 +902,7 @@ func Test_editRun(t *testing.T) { reg := &httpmock.Registry{} defer reg.Verify(t) - tt.httpStubs(reg) + tt.httpStubs(reg, t) httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } @@ -804,10 +919,21 @@ func Test_editRun(t *testing.T) { } } -func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { - reg.Register( - httpmock.GraphQL(`query RepositoryAssignableActors\b`), - httpmock.StringResponse(` +type mockRepoMetadataOptions struct { + reviewers bool + teamReviewers bool // reviewers must also be true for this to have an effect. + assignees bool + labels bool + projects bool // includes both legacy (v1) and v2 + milestones bool +} + +func mockRepoMetadata(reg *httpmock.Registry, opt mockRepoMetadataOptions) { + // Assignable actors (users/bots) are fetched when reviewers OR assignees edited with ActorAssignees enabled. + if opt.reviewers || opt.assignees { + reg.Register( + httpmock.GraphQL(`query RepositoryAssignableActors\b`), + httpmock.StringResponse(` { "data": { "repository": { "suggestedActors": { "nodes": [ { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, @@ -816,9 +942,11 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) - reg.Register( - httpmock.GraphQL(`query RepositoryLabelList\b`), - httpmock.StringResponse(` + } + if opt.labels { + reg.Register( + httpmock.GraphQL(`query RepositoryLabelList\b`), + httpmock.StringResponse(` { "data": { "repository": { "labels": { "nodes": [ { "name": "feature", "id": "FEATUREID" }, @@ -829,9 +957,11 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) - reg.Register( - httpmock.GraphQL(`query RepositoryMilestoneList\b`), - httpmock.StringResponse(` + } + if opt.milestones { + reg.Register( + httpmock.GraphQL(`query RepositoryMilestoneList\b`), + httpmock.StringResponse(` { "data": { "repository": { "milestones": { "nodes": [ { "title": "GA", "id": "GAID" }, @@ -840,9 +970,11 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) - reg.Register( - httpmock.GraphQL(`query RepositoryProjectList\b`), - httpmock.StringResponse(` + } + if opt.projects { + reg.Register( + httpmock.GraphQL(`query RepositoryProjectList\b`), + httpmock.StringResponse(` { "data": { "repository": { "projects": { "nodes": [ { "name": "Cleanup", "id": "CLEANUPID" }, @@ -851,9 +983,9 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) - reg.Register( - httpmock.GraphQL(`query OrganizationProjectList\b`), - httpmock.StringResponse(` + reg.Register( + httpmock.GraphQL(`query OrganizationProjectList\b`), + httpmock.StringResponse(` { "data": { "organization": { "projects": { "nodes": [ { "name": "Triage", "id": "TRIAGEID" } @@ -861,9 +993,9 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) - reg.Register( - httpmock.GraphQL(`query RepositoryProjectV2List\b`), - httpmock.StringResponse(` + reg.Register( + httpmock.GraphQL(`query RepositoryProjectV2List\b`), + httpmock.StringResponse(` { "data": { "repository": { "projectsV2": { "nodes": [ { "title": "CleanupV2", "id": "CLEANUPV2ID" }, @@ -872,9 +1004,9 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) - reg.Register( - httpmock.GraphQL(`query OrganizationProjectV2List\b`), - httpmock.StringResponse(` + reg.Register( + httpmock.GraphQL(`query OrganizationProjectV2List\b`), + httpmock.StringResponse(` { "data": { "organization": { "projectsV2": { "nodes": [ { "title": "TriageV2", "id": "TRIAGEV2ID" } @@ -882,9 +1014,9 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) - reg.Register( - httpmock.GraphQL(`query UserProjectV2List\b`), - httpmock.StringResponse(` + reg.Register( + httpmock.GraphQL(`query UserProjectV2List\b`), + httpmock.StringResponse(` { "data": { "viewer": { "projectsV2": { "nodes": [ { "title": "MonalisaV2", "id": "MONALISAV2ID" } @@ -892,7 +1024,8 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) - if !skipReviewers { + } + if opt.teamReviewers && opt.reviewers { // teams only relevant if reviewers edited reg.Register( httpmock.GraphQL(`query OrganizationTeamList\b`), httpmock.StringResponse(` @@ -904,11 +1037,13 @@ func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { "pageInfo": { "hasNextPage": false } } } } } `)) + } + if opt.reviewers { // Current user fetched only when reviewers requested reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(` - { "data": { "viewer": { "login": "monalisa" } } } - `)) + { "data": { "viewer": { "login": "monalisa" } } } + `)) } } @@ -927,9 +1062,15 @@ func mockPullRequestUpdateActorAssignees(reg *httpmock.Registry) { ) } -func mockPullRequestReviewersUpdate(reg *httpmock.Registry) { +func mockPullRequestAddReviewers(reg *httpmock.Registry) { reg.Register( - httpmock.GraphQL(`mutation PullRequestUpdateRequestReviews\b`), + httpmock.REST("POST", "repos/OWNER/REPO/pulls/0/requested_reviewers"), + httpmock.StringResponse(`{}`)) +} + +func mockPullRequestRemoveReviewers(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("DELETE", "repos/OWNER/REPO/pulls/0/requested_reviewers"), httpmock.StringResponse(`{}`)) } diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index 9adbeb47c..7d0805a80 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -2,7 +2,6 @@ package shared import ( "fmt" - "strings" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" @@ -78,37 +77,6 @@ func (e Editable) BodyValue() *string { return &e.Body.Value } -func (e Editable) ReviewerIds() (*[]string, *[]string, error) { - if !e.Reviewers.Edited { - return nil, nil, nil - } - if len(e.Reviewers.Add) != 0 || len(e.Reviewers.Remove) != 0 { - s := set.NewStringSet() - s.AddValues(e.Reviewers.Default) - s.AddValues(e.Reviewers.Add) - s.RemoveValues(e.Reviewers.Remove) - e.Reviewers.Value = s.ToSlice() - } - var userReviewers []string - var teamReviewers []string - for _, r := range e.Reviewers.Value { - if strings.ContainsRune(r, '/') { - teamReviewers = append(teamReviewers, r) - } else { - userReviewers = append(userReviewers, r) - } - } - userIds, err := e.Metadata.MembersToIDs(userReviewers) - if err != nil { - return nil, nil, err - } - teamIds, err := e.Metadata.TeamsToIDs(teamReviewers) - if err != nil { - return nil, nil, err - } - return &userIds, &teamIds, nil -} - func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]string, error) { if !e.Assignees.Edited { return nil, nil @@ -428,17 +396,20 @@ func FieldsToEditSurvey(p EditPrompter, editable *Editable) error { } func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error { + // Determine whether to fetch organization teams. + // Interactive reviewer editing (Edited true, but no Add/Remove slices) still needs + // team data for selection UI. For non-interactive flows, we never need to fetch teams. + teamReviewers := false + if editable.Reviewers.Edited { + // This is likely an interactive flow since edited is set but no mutations to + // Add/Remove slices, so we need to load the teams. + if len(editable.Reviewers.Add) == 0 && len(editable.Reviewers.Remove) == 0 { + teamReviewers = true + } + } input := api.RepoMetadataInput{ - Reviewers: editable.Reviewers.Edited, - // TeamReviewers is always true if Reviewers is true because - // this is the existing `pr edit` behavior. This means - // always fetch teams. - // TODO: evaluate whether this can follow the same logic as - // `pr create` to conditionally fetch teams if a reviewer contains - // a slash. - // See https://github.com/cli/cli/blob/449920b40fc8a5015d1578ca10a301aa385a1914/pkg/cmd/pr/shared/params.go#L67-L71 - // See https://github.com/cli/cli/issues/11360 - TeamReviewers: editable.Reviewers.Edited, + Reviewers: editable.Reviewers.Edited, + TeamReviewers: teamReviewers, Assignees: editable.Assignees.Edited, ActorAssignees: editable.Assignees.ActorAssignees, Labels: editable.Labels.Edited, From ccfc2c3045485e3372f35eff6278bb9ea188e391 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 1 Oct 2025 18:44:24 +0200 Subject: [PATCH 36/87] fix(cache): report correct deleted count for key and key+ref deletions Previously, deleting caches by key or key+ref could misreport the number of deleted entries, especially when multiple caches matched the criteria. This change ensures the actual number of deleted caches is reported by consuming the API response's total_count field. --- pkg/cmd/cache/delete/delete.go | 45 ++++++++++++++++++--------- pkg/cmd/cache/delete/delete_test.go | 48 +++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index 58281d831..3f17d5d31 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -164,33 +164,27 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface cs := opts.IO.ColorScheme() repoName := ghrepo.FullName(repo) opts.IO.StartProgressIndicator() - base := fmt.Sprintf("repos/%s/actions/caches", repoName) + totalDeleted := 0 for _, cache := range toDelete { - // TODO(babakks): We use two different endpoints here which have different - // response schemas: + // We use two different endpoints with different response schemas: // // 1. /repos/OWNER/REPO/actions/caches/ID (for deleting by cache ID) // - returns HTTP 204 (NO CONTENT) on success // 2. /repos/OWNER/REPO/actions/caches?key=KEY[&ref=REF] (for deleting by cache key, and optionally a ref) // - returns HTTP 200 on success including information about the deleted caches // - // So, if/when we decided to use the data in the response body we need - // to be careful with parsing. Probably want to split these API calls - // into separate functions. + // The API calls are split into separate functions to handle the different response handling. - path := "" + var count int + var err error if id, ok := parseCacheID(cache); ok { - path = fmt.Sprintf("%s/%d", base, id) + err = deleteCacheByID(client, repo, id) + count = 1 } else { - path = fmt.Sprintf("%s?key=%s", base, url.QueryEscape(cache)) - - if opts.Ref != "" { - path += fmt.Sprintf("&ref=%s", url.QueryEscape(opts.Ref)) - } + count, err = deleteCacheByKey(client, repo, cache, opts.Ref) } - err := client.REST(repo.RepoHost(), "DELETE", path, nil, nil) if err != nil { var httpErr api.HTTPError if errors.As(err, &httpErr) { @@ -207,17 +201,38 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface opts.IO.StopProgressIndicator() return err } + + totalDeleted += count } opts.IO.StopProgressIndicator() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Deleted %s from %s\n", cs.SuccessIcon(), text.Pluralize(len(toDelete), "cache"), repoName) + fmt.Fprintf(opts.IO.Out, "%s Deleted %s from %s\n", cs.SuccessIcon(), text.Pluralize(totalDeleted, "cache"), repoName) } return nil } +func deleteCacheByID(client *api.Client, repo ghrepo.Interface, id int) error { + path := fmt.Sprintf("repos/%s/actions/caches/%d", ghrepo.FullName(repo), id) + return client.REST(repo.RepoHost(), "DELETE", path, nil, nil) +} + +func deleteCacheByKey(client *api.Client, repo ghrepo.Interface, key, ref string) (int, error) { + path := fmt.Sprintf("repos/%s/actions/caches?key=%s", ghrepo.FullName(repo), url.QueryEscape(key)) + if ref != "" { + path += fmt.Sprintf("&ref=%s", url.QueryEscape(ref)) + } + var payload shared.CachePayload + err := client.REST(repo.RepoHost(), "DELETE", path, nil, &payload) + if err != nil { + return 0, err + } + + return payload.TotalCount, nil +} + func parseCacheID(arg string) (int, bool) { id, err := strconv.Atoi(arg) return id, err == nil diff --git a/pkg/cmd/cache/delete/delete_test.go b/pkg/cmd/cache/delete/delete_test.go index 4cfa3ce26..a05c57c7c 100644 --- a/pkg/cmd/cache/delete/delete_test.go +++ b/pkg/cmd/cache/delete/delete_test.go @@ -235,13 +235,30 @@ func TestDeleteRun(t *testing.T) { httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{ "key": []string{"a weird_cache+key"}, }), - // The response is a JSON object but we don't need it here. - httpmock.StatusStringResponse(200, "{}"), + httpmock.JSONResponse(shared.CachePayload{ + TotalCount: 1, + }), ) }, tty: true, wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n", }, + { + name: "deletes multiple caches by key", + opts: DeleteOptions{Identifier: "shared-cache-key"}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{ + "key": []string{"shared-cache-key"}, + }), + httpmock.JSONResponse(shared.CachePayload{ + TotalCount: 5, + }), + ) + }, + tty: true, + wantStdout: "✓ Deleted 5 caches from OWNER/REPO\n", + }, { name: "no caches to delete when deleting all", opts: DeleteOptions{DeleteAll: true}, @@ -299,8 +316,9 @@ func TestDeleteRun(t *testing.T) { "key": []string{"cache-key"}, "ref": []string{"refs/heads/main"}, }), - // The response is a JSON object but we don't need it here. - httpmock.StatusStringResponse(200, "{}"), + httpmock.JSONResponse(shared.CachePayload{ + TotalCount: 1, + }), ) }, tty: true, @@ -315,13 +333,31 @@ func TestDeleteRun(t *testing.T) { "key": []string{"cache-key"}, "ref": []string{"refs/heads/main"}, }), - // The response is a JSON object but we don't need it here. - httpmock.StatusStringResponse(200, "{}"), + httpmock.JSONResponse(shared.CachePayload{ + TotalCount: 1, + }), ) }, tty: false, wantStdout: "", }, + { + name: "deletes multiple caches by key and ref", + opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/feature"}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{ + "key": []string{"cache-key"}, + "ref": []string{"refs/heads/feature"}, + }), + httpmock.JSONResponse(shared.CachePayload{ + TotalCount: 3, + }), + ) + }, + tty: true, + wantStdout: "✓ Deleted 3 caches from OWNER/REPO\n", + }, { // As of now, the API returns HTTP 404 for invalid or non-existent refs. name: "cache key exists but ref is invalid/not-found", From 7094a55eec08fc5224ed4b1926684dbb81cf214e Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:47:39 -0600 Subject: [PATCH 37/87] Remove unused ghIds function and githubv4 import Deleted the unused ghIds helper function and the associated githubv4 import from edit.go to clean up the codebase. --- pkg/cmd/pr/edit/edit.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index de65f1c72..2c7f25018 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -16,7 +16,6 @@ import ( "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/set" - "github.com/shurcooL/githubv4" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) @@ -440,14 +439,3 @@ type editorRetriever struct { func (e editorRetriever) Retrieve() (string, error) { return cmdutil.DetermineEditor(e.config) } - -func ghIds(s *[]string) *[]githubv4.ID { - if s == nil { - return nil - } - ids := make([]githubv4.ID, len(*s)) - for i, v := range *s { - ids[i] = v - } - return &ids -} From 57fce1dc3aa1044dd8872061e58b2f6bd9ef4bf3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:55:12 -0600 Subject: [PATCH 38/87] Escape repo owner and name in PR reviewer API paths Updated AddPullRequestReviews and RemovePullRequestReviews to use url.PathEscape for repo owner and name in API paths. This ensures correct handling of special characters in repository identifiers. --- api/queries_pr.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 0d0fed03d..60e6834ac 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -637,7 +637,12 @@ func AddPullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, return nil } - path := fmt.Sprintf("repos/%s/%s/pulls/%d/requested_reviewers", repo.RepoOwner(), repo.RepoName(), prNumber) + path := fmt.Sprintf( + "repos/%s/%s/pulls/%d/requested_reviewers", + url.PathEscape(repo.RepoOwner()), + url.PathEscape(repo.RepoName()), + prNumber, + ) body := struct { Reviewers []string `json:"reviewers,omitempty"` TeamReviewers []string `json:"team_reviewers,omitempty"` @@ -666,7 +671,12 @@ func RemovePullRequestReviews(client *Client, repo ghrepo.Interface, prNumber in teams = []string{} } - path := fmt.Sprintf("repos/%s/%s/pulls/%d/requested_reviewers", repo.RepoOwner(), repo.RepoName(), prNumber) + path := fmt.Sprintf( + "repos/%s/%s/pulls/%d/requested_reviewers", + url.PathEscape(repo.RepoOwner()), + url.PathEscape(repo.RepoName()), + prNumber, + ) body := struct { Reviewers []string `json:"reviewers"` TeamReviewers []string `json:"team_reviewers"` From 848faf81155e72c70ffc28fb7dbb71f3157ace5c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:02:05 -0600 Subject: [PATCH 39/87] Refactor reviewer partitioning in PR edit command Extracted logic for splitting reviewer identifiers into users and teams into a new helper function, partitionUsersAndTeams. Updated updatePullRequestReviews to use this function for both adding and removing reviewers, improving code clarity and maintainability. Also clarified comments regarding PR author handling. --- api/queries_pr.go | 2 +- pkg/cmd/pr/edit/edit.go | 45 ++++++++++++++++++----------------------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 60e6834ac..010a915e2 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -631,7 +631,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter return pr, nil } -// AddPullRequestReviews updates the requested reviewers on a pull request using the REST API. +// AddPullRequestReviews adds the given user and team reviewers to a pull request using the REST API. func AddPullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, users, teams []string) error { if len(users) == 0 && len(teams) == 0 { return nil diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 2c7f25018..3e00bfab9 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -300,11 +300,8 @@ func editRun(opts *EditOptions) error { } if opts.Interactive { - // Remove PR author from reviewer options - // as it is not a valid option for a reviewer. - // The REST API will return an error if we - // attempt to add the PR author as a reviewer. - // However, the GraphQL API will silently ignore it. + // Remove PR author from reviewer options; + // REST API errors if author is included (GraphQL silently ignores). if editable.Reviewers.Edited { s := set.NewStringSet() s.AddValues(editable.Reviewers.Options) @@ -361,16 +358,7 @@ func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, nu editable.Reviewers.Value = s.ToSlice() } - var addUsers []string - var addTeams []string - for _, r := range editable.Reviewers.Value { - if strings.ContainsRune(r, '/') { - teamSlug := strings.Split(r, "/")[1] - addTeams = append(addTeams, teamSlug) - } else { - addUsers = append(addUsers, r) - } - } + addUsers, addTeams := partitionUsersAndTeams(editable.Reviewers.Value) // Reviewers in Default but not in the Value have been removed interactively. var toRemove []string @@ -379,16 +367,7 @@ func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, nu toRemove = append(toRemove, r) } } - var removeUsers []string - var removeTeams []string - for _, r := range toRemove { - if strings.ContainsRune(r, '/') { - teamSlug := strings.Split(r, "/")[1] - removeTeams = append(removeTeams, teamSlug) - } else { - removeUsers = append(removeUsers, r) - } - } + removeUsers, removeTeams := partitionUsersAndTeams(toRemove) client := api.NewClientFromHTTP(httpClient) wg := errgroup.Group{} @@ -439,3 +418,19 @@ type editorRetriever struct { func (e editorRetriever) Retrieve() (string, error) { return cmdutil.DetermineEditor(e.config) } + +// partitionUsersAndTeams splits reviewer identifiers into user logins and team slugs. +// Team identifiers are in the form "org/slug"; only the slug portion is returned for teams. +func partitionUsersAndTeams(values []string) (users []string, teams []string) { + for _, v := range values { + if strings.ContainsRune(v, '/') { + parts := strings.SplitN(v, "/", 2) + if len(parts) == 2 && parts[1] != "" { + teams = append(teams, parts[1]) + } + } else if v != "" { + users = append(users, v) + } + } + return +} From 52bb1dec30b33ab3ef51d06c799aa8b6ef91434b Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:09:33 -0600 Subject: [PATCH 40/87] Fix typo in error message for required flags Corrected '--tile' to '--title' in the error message shown when required flags are missing in non-interactive mode. --- pkg/cmd/pr/edit/edit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 3e00bfab9..e593b791a 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -172,7 +172,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman } if opts.Interactive && !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively") + return cmdutil.FlagErrorf("--title, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively") } if runF != nil { From 66fae72872c27037e15df92ae7a12da7ceb718f5 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:12:06 -0600 Subject: [PATCH 41/87] Remove default empty slices in RemovePullRequestReviews Eliminates unnecessary initialization of users and teams to empty slices in RemovePullRequestReviews. Also updates the request body struct to use 'omitempty' for reviewers and team_reviewers, ensuring empty fields are omitted from the JSON payload. --- api/queries_pr.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 010a915e2..4262738c3 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -664,13 +664,6 @@ func RemovePullRequestReviews(client *Client, repo ghrepo.Interface, prNumber in return nil } - if users == nil { - users = []string{} - } - if teams == nil { - teams = []string{} - } - path := fmt.Sprintf( "repos/%s/%s/pulls/%d/requested_reviewers", url.PathEscape(repo.RepoOwner()), @@ -678,8 +671,8 @@ func RemovePullRequestReviews(client *Client, repo ghrepo.Interface, prNumber in prNumber, ) body := struct { - Reviewers []string `json:"reviewers"` - TeamReviewers []string `json:"team_reviewers"` + Reviewers []string `json:"reviewers,omitempty"` + TeamReviewers []string `json:"team_reviewers,omitempty"` }{ Reviewers: users, TeamReviewers: teams, From d574873f3b921ec4664d72ba8eab3982ac643318 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 2 Oct 2025 11:33:26 +0100 Subject: [PATCH 42/87] docs(cache delete): add godoc for `deleteCacheByKey` Signed-off-by: Babak K. Shandiz --- pkg/cmd/cache/delete/delete.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index 3f17d5d31..480dbc2f2 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -219,6 +219,12 @@ func deleteCacheByID(client *api.Client, repo ghrepo.Interface, id int) error { return client.REST(repo.RepoHost(), "DELETE", path, nil, nil) } +// deleteCacheByKey deletes cache entries by given key (and optional ref) and +// returns the number of deleted entries. +// +// Note that a key/ref combination does not necessarily map to a single cache +// entry. There may be more than one entries with the same key/ref combination, +// but those entries will have different IDs. func deleteCacheByKey(client *api.Client, repo ghrepo.Interface, key, ref string) (int, error) { path := fmt.Sprintf("repos/%s/actions/caches?key=%s", ghrepo.FullName(repo), url.QueryEscape(key)) if ref != "" { From ab9c99ec04e9d9cdfaff5ffe59cb5c23d6a1ff97 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 2 Oct 2025 11:36:00 +0100 Subject: [PATCH 43/87] docs(cache delete): remove redundant comment Signed-off-by: Babak K. Shandiz --- pkg/cmd/cache/delete/delete.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index 480dbc2f2..b8367cb5c 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -167,15 +167,6 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface totalDeleted := 0 for _, cache := range toDelete { - // We use two different endpoints with different response schemas: - // - // 1. /repos/OWNER/REPO/actions/caches/ID (for deleting by cache ID) - // - returns HTTP 204 (NO CONTENT) on success - // 2. /repos/OWNER/REPO/actions/caches?key=KEY[&ref=REF] (for deleting by cache key, and optionally a ref) - // - returns HTTP 200 on success including information about the deleted caches - // - // The API calls are split into separate functions to handle the different response handling. - var count int var err error if id, ok := parseCacheID(cache); ok { @@ -215,6 +206,7 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface } func deleteCacheByID(client *api.Client, repo ghrepo.Interface, id int) error { + // returns HTTP 204 (NO CONTENT) on success path := fmt.Sprintf("repos/%s/actions/caches/%d", ghrepo.FullName(repo), id) return client.REST(repo.RepoHost(), "DELETE", path, nil, nil) } From bf728893fa2348b2b2c200db237498cfe9adb036 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:54:01 -0600 Subject: [PATCH 44/87] Fix argument order in httpStubs test functions Swaps the argument order of the httpStubs functions in edit_test.go to match the expected (t *testing.T, reg *httpmock.Registry) signature. This improves consistency and prevents potential confusion or errors when calling these test helpers. --- pkg/cmd/pr/edit/edit_test.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 803f506bc..5ed4d4fdd 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -354,7 +354,7 @@ func Test_editRun(t *testing.T) { tests := []struct { name string input *EditOptions - httpStubs func(*httpmock.Registry, *testing.T) + httpStubs func(*testing.T, *httpmock.Registry) stdout string stderr string }{ @@ -411,7 +411,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true, teamReviewers: false, assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestUpdateActorAssignees(reg) @@ -469,7 +469,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { mockRepoMetadata(reg, mockRepoMetadataOptions{assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestUpdateActorAssignees(reg) @@ -542,7 +542,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true, teamReviewers: false, assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestRemoveReviewers(reg) @@ -565,7 +565,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { // reviewers only (users), no team reviewers fetched mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) // explicitly assert that no OrganizationTeamList query occurs @@ -587,7 +587,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { // reviewer add includes team but non-interactive Add/Remove provided -> no team fetch mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) reg.Exclude(t, httpmock.GraphQL(`query OrganizationTeamList\b`)) @@ -613,7 +613,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) reg.Exclude(t, httpmock.GraphQL(`query OrganizationTeamList\b`)) mockPullRequestUpdate(reg) @@ -633,7 +633,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { // reviewers only (users), no team reviewers fetched mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) // explicitly assert that no OrganizationTeamList query occurs @@ -679,7 +679,7 @@ func Test_editRun(t *testing.T) { Fetcher: testFetcher{}, EditorRetriever: testEditorRetriever{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true, teamReviewers: true, assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestUpdateActorAssignees(reg) @@ -723,7 +723,7 @@ func Test_editRun(t *testing.T) { Fetcher: testFetcher{}, EditorRetriever: testEditorRetriever{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { // interactive but reviewers not chosen; need everything except reviewers/teams mockRepoMetadata(reg, mockRepoMetadataOptions{assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) @@ -780,7 +780,7 @@ func Test_editRun(t *testing.T) { Fetcher: testFetcher{}, EditorRetriever: testEditorRetriever{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true, teamReviewers: true, assignees: true, labels: true, projects: true, milestones: true}) mockPullRequestUpdate(reg) mockPullRequestRemoveReviewers(reg) @@ -827,7 +827,7 @@ func Test_editRun(t *testing.T) { Fetcher: testFetcher{}, EditorRetriever: testEditorRetriever{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`query RepositoryAssignableActors\b`), httpmock.StringResponse(` @@ -874,7 +874,7 @@ func Test_editRun(t *testing.T) { }, Fetcher: testFetcher{}, }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { + httpStubs: func(t *testing.T, reg *httpmock.Registry) { // Notice there is no call to mockReplaceActorsForAssignable() // and no GraphQL call to RepositoryAssignableActors below. reg.Register( @@ -902,7 +902,7 @@ func Test_editRun(t *testing.T) { reg := &httpmock.Registry{} defer reg.Verify(t) - tt.httpStubs(reg, t) + tt.httpStubs(t, reg) httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } From 8d8594609896f662e345a8b4f67e39fb685e5287 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:55:45 -0600 Subject: [PATCH 45/87] Apply suggestion from @babakks Co-authored-by: Babak K. Shandiz --- pkg/cmd/pr/edit/edit_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 5ed4d4fdd..0fa74d030 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -590,6 +590,7 @@ func Test_editRun(t *testing.T) { httpStubs: func(t *testing.T, reg *httpmock.Registry) { // reviewer add includes team but non-interactive Add/Remove provided -> no team fetch mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) + // explicitly assert that no OrganizationTeamList query occurs reg.Exclude(t, httpmock.GraphQL(`query OrganizationTeamList\b`)) mockPullRequestUpdate(reg) mockPullRequestAddReviewers(reg) From 43e834a6919d2d1e1fd5904e0ba0058b0ef0aaeb Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:55:58 -0600 Subject: [PATCH 46/87] Apply suggestion from @babakks Co-authored-by: Babak K. Shandiz --- pkg/cmd/pr/edit/edit_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 0fa74d030..47ce30f22 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -616,6 +616,7 @@ func Test_editRun(t *testing.T) { }, httpStubs: func(t *testing.T, reg *httpmock.Registry) { mockRepoMetadata(reg, mockRepoMetadataOptions{reviewers: true}) + // explicitly assert that no OrganizationTeamList query occurs reg.Exclude(t, httpmock.GraphQL(`query OrganizationTeamList\b`)) mockPullRequestUpdate(reg) mockPullRequestRemoveReviewers(reg) From 60b6a5eed59d982aca365045dfc7d2d8e0a30f5c Mon Sep 17 00:00:00 2001 From: Lucas Melin Date: Sat, 16 Mar 2024 23:05:34 -0400 Subject: [PATCH 47/87] feat: implement `pr revert` --- api/queries_pr.go | 22 +++++- pkg/cmd/pr/pr.go | 2 + pkg/cmd/pr/revert/revert.go | 122 +++++++++++++++++++++++++++++++ pkg/cmd/pr/revert/revert_test.go | 120 ++++++++++++++++++++++++++++++ 4 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/pr/revert/revert.go create mode 100644 pkg/cmd/pr/revert/revert_test.go diff --git a/api/queries_pr.go b/api/queries_pr.go index 4262738c3..dd18b3630 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -517,8 +517,8 @@ func parseCheckStatusFromCheckConclusionState(state CheckConclusionState) checkS func (pr *PullRequest) DisplayableReviews() PullRequestReviews { published := []PullRequestReview{} for _, prr := range pr.Reviews.Nodes { - //Dont display pending reviews - //Dont display commenting reviews without top level comment body + // Dont display pending reviews + // Dont display commenting reviews without top level comment body if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") { published = append(published, prr) } @@ -598,7 +598,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter reviewParams["teamIds"] = ids } - //TODO: How much work to extract this into own method and use for create and edit? + // TODO: How much work to extract this into own method and use for create and edit? if len(reviewParams) > 0 { reviewQuery := ` mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) { @@ -764,6 +764,22 @@ func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) er return client.Mutate(repo.RepoHost(), "PullRequestReadyForReview", &mutation, variables) } +func PullRequestRevert(client *Client, repo ghrepo.Interface, params githubv4.RevertPullRequestInput) error { + var mutation struct { + RevertPullRequest struct { + PullRequest struct { + ID githubv4.ID + } + } `graphql:"revertPullRequest(input: $input)"` + } + + variables := map[string]interface{}{ + "input": params, + } + + return client.Mutate(repo.RepoHost(), "PullRequestRevert", &mutation, variables) +} + func ConvertPullRequestToDraft(client *Client, repo ghrepo.Interface, pr *PullRequest) error { var mutation struct { ConvertPullRequestToDraft struct { diff --git a/pkg/cmd/pr/pr.go b/pkg/cmd/pr/pr.go index 406cc08b7..e73193084 100644 --- a/pkg/cmd/pr/pr.go +++ b/pkg/cmd/pr/pr.go @@ -14,6 +14,7 @@ import ( cmdMerge "github.com/cli/cli/v2/pkg/cmd/pr/merge" cmdReady "github.com/cli/cli/v2/pkg/cmd/pr/ready" cmdReopen "github.com/cli/cli/v2/pkg/cmd/pr/reopen" + cmdRevert "github.com/cli/cli/v2/pkg/cmd/pr/revert" cmdReview "github.com/cli/cli/v2/pkg/cmd/pr/review" cmdStatus "github.com/cli/cli/v2/pkg/cmd/pr/status" cmdUpdateBranch "github.com/cli/cli/v2/pkg/cmd/pr/update-branch" @@ -63,6 +64,7 @@ func NewCmdPR(f *cmdutil.Factory) *cobra.Command { cmdComment.NewCmdComment(f, nil), cmdClose.NewCmdClose(f, nil), cmdReopen.NewCmdReopen(f, nil), + cmdRevert.NewCmdRevert(f, nil), cmdEdit.NewCmdEdit(f, nil), cmdLock.NewCmdLock(f, cmd.Name(), nil), cmdLock.NewCmdUnlock(f, cmd.Name(), nil), diff --git a/pkg/cmd/pr/revert/revert.go b/pkg/cmd/pr/revert/revert.go new file mode 100644 index 000000000..7429f3436 --- /dev/null +++ b/pkg/cmd/pr/revert/revert.go @@ -0,0 +1,122 @@ +package revert + +import ( + "fmt" + "net/http" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/pr/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/shurcooL/githubv4" + "github.com/spf13/cobra" +) + +type RevertOptions struct { + HttpClient func() (*http.Client, error) + IO *iostreams.IOStreams + + Finder shared.PRFinder + + SelectorArg string + + Body string + BodySet bool + Title string + IsDraft bool +} + +func NewCmdRevert(f *cmdutil.Factory, runF func(*RevertOptions) error) *cobra.Command { + opts := &RevertOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + } + + var bodyFile string + + cmd := &cobra.Command{ + Use: "revert { | | }", + Short: "Revert a pull request", + Args: cmdutil.ExactArgs(1, "cannot revert pull request: number, url, or branch required"), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Finder = shared.NewFinder(f) + + if len(args) > 0 { + opts.SelectorArg = args[0] + } + + bodyProvided := cmd.Flags().Changed("body") + bodyFileProvided := bodyFile != "" + + if err := cmdutil.MutuallyExclusive( + "specify only one of `--body` or `--body-file`", + bodyProvided, + bodyFileProvided, + ); err != nil { + return err + } + + if bodyProvided || bodyFileProvided { + opts.BodySet = true + if bodyFileProvided { + b, err := cmdutil.ReadFile(bodyFile, opts.IO.In) + if err != nil { + return err + } + opts.Body = string(b) + } + } + + if runF != nil { + return runF(opts) + } + return revertRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.IsDraft, "draft", "d", false, "Mark revert pull request as a draft") + cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Title for the revert pull request") + cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Body for the revert pull request") + cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)") + return cmd +} + +func revertRun(opts *RevertOptions) error { + cs := opts.IO.ColorScheme() + + findOptions := shared.FindOptions{ + Selector: opts.SelectorArg, + Fields: []string{"id", "number", "state", "title"}, + } + pr, baseRepo, err := opts.Finder.Find(findOptions) + if err != nil { + return err + } + if pr.State != "MERGED" { + fmt.Fprintf(opts.IO.ErrOut, "%s Pull request %s#%d (%s) can't be reverted because it has not been merged\n", cs.FailureIcon(), ghrepo.FullName(baseRepo), pr.Number, pr.Title) + return cmdutil.SilentError + } + + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + apiClient := api.NewClientFromHTTP(httpClient) + + params := githubv4.RevertPullRequestInput{ + PullRequestID: pr.ID, + Title: githubv4.NewString(githubv4.String(opts.Title)), + Body: githubv4.NewString(githubv4.String(opts.Body)), + Draft: githubv4.NewBoolean(githubv4.Boolean(opts.IsDraft)), + } + + err = api.PullRequestRevert(apiClient, baseRepo, params) + if err != nil { + return fmt.Errorf("API call failed: %w", err) + } + + fmt.Fprintf(opts.IO.ErrOut, "%s Created revert PR for pull request %s#%d (%s)\n", cs.SuccessIconWithColor(cs.Green), ghrepo.FullName(baseRepo), pr.Number, pr.Title) + + return nil +} diff --git a/pkg/cmd/pr/revert/revert_test.go b/pkg/cmd/pr/revert/revert_test.go new file mode 100644 index 000000000..edd0a8595 --- /dev/null +++ b/pkg/cmd/pr/revert/revert_test.go @@ -0,0 +1,120 @@ +package revert + +import ( + "bytes" + "io" + "net/http" + "testing" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/pr/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/test" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) { + ios, _, stdout, stderr := iostreams.Test() + ios.SetStdoutTTY(isTTY) + ios.SetStdinTTY(isTTY) + ios.SetStderrTTY(isTTY) + + factory := &cmdutil.Factory{ + IOStreams: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: rt}, nil + }, + } + + cmd := NewCmdRevert(factory, nil) + + argv, err := shlex.Split(cli) + if err != nil { + return nil, err + } + cmd.SetArgs(argv) + + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + _, err = cmd.ExecuteC() + return &test.CmdOut{ + OutBuf: stdout, + ErrBuf: stderr, + }, err +} + +func TestPRRevert(t *testing.T) { + http := &httpmock.Registry{} + defer http.Verify(t) + + shared.RunCommandFinder("123", &api.PullRequest{ + ID: "SOME-ID", + Number: 123, + State: "MERGED", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + http.Register( + httpmock.GraphQL(`mutation PullRequestRevert\b`), + httpmock.GraphQLMutation(`{"id": "SOME-ID"}`, + func(inputs map[string]interface{}) { + assert.Equal(t, inputs["pullRequestId"], "SOME-ID") + }), + ) + + output, err := runCommand(http, true, "123") + assert.NoError(t, err) + assert.Equal(t, "", output.String()) + assert.Equal(t, "✓ Created revert PR for pull request OWNER/REPO#123 (The title of the PR)\n", output.Stderr()) +} + +func TestPRRevert_notRevertable(t *testing.T) { + http := &httpmock.Registry{} + defer http.Verify(t) + + shared.RunCommandFinder("123", &api.PullRequest{ + ID: "SOME-ID", + Number: 123, + State: "OPEN", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + output, err := runCommand(http, true, "123") + assert.Error(t, err) + assert.Equal(t, "", output.String()) + assert.Equal(t, "X Pull request OWNER/REPO#123 (The title of the PR) can't be reverted because it has not been merged\n", output.Stderr()) +} + +func TestPRRevert_withAllOpts(t *testing.T) { + http := &httpmock.Registry{} + defer http.Verify(t) + + shared.RunCommandFinder("123", &api.PullRequest{ + ID: "THE-ID", + Number: 123, + State: "MERGED", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + http.Register( + httpmock.GraphQL(`mutation PullRequestRevert\b`), + httpmock.GraphQLMutation(`{"id": "THE-ID" }`, + func(inputs map[string]interface{}) { + assert.Equal(t, inputs["pullRequestId"], "THE-ID") + assert.Equal(t, inputs["title"], "Revert PR title") + assert.Equal(t, inputs["body"], "Revert PR body") + assert.Equal(t, inputs["draft"], true) + }), + ) + + output, err := runCommand(http, true, "123 --title 'Revert PR title' --body 'Revert PR body' --draft") + assert.NoError(t, err) + assert.Equal(t, "", output.String()) + assert.Equal(t, "✓ Created revert PR for pull request OWNER/REPO#123 (The title of the PR)\n", output.Stderr()) +} From 9467d66b112f052e2d1fcf25edfb86e5e731d39e Mon Sep 17 00:00:00 2001 From: Lucas Melin Date: Sat, 16 Mar 2024 23:20:43 -0400 Subject: [PATCH 48/87] Undo autoformat changes --- api/queries_pr.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index dd18b3630..b6312485a 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -517,8 +517,8 @@ func parseCheckStatusFromCheckConclusionState(state CheckConclusionState) checkS func (pr *PullRequest) DisplayableReviews() PullRequestReviews { published := []PullRequestReview{} for _, prr := range pr.Reviews.Nodes { - // Dont display pending reviews - // Dont display commenting reviews without top level comment body + //Dont display pending reviews + //Dont display commenting reviews without top level comment body if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") { published = append(published, prr) } @@ -598,7 +598,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter reviewParams["teamIds"] = ids } - // TODO: How much work to extract this into own method and use for create and edit? + //TODO: How much work to extract this into own method and use for create and edit? if len(reviewParams) > 0 { reviewQuery := ` mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) { From 023a639c8ca1070fe46d72b400e6e36667fb4766 Mon Sep 17 00:00:00 2001 From: Lucas Melin Date: Sun, 17 Mar 2024 09:27:45 -0400 Subject: [PATCH 49/87] feat: include revert PR info in message output --- api/queries_pr.go | 11 ++++++++--- pkg/cmd/pr/revert/revert.go | 14 ++++++++++++-- pkg/cmd/pr/revert/revert_test.go | 28 ++++++++++++++++++++++------ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index b6312485a..467194017 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -764,20 +764,25 @@ func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) er return client.Mutate(repo.RepoHost(), "PullRequestReadyForReview", &mutation, variables) } -func PullRequestRevert(client *Client, repo ghrepo.Interface, params githubv4.RevertPullRequestInput) error { +func PullRequestRevert(client *Client, repo ghrepo.Interface, params githubv4.RevertPullRequestInput) (*PullRequest, error) { var mutation struct { RevertPullRequest struct { PullRequest struct { ID githubv4.ID } + RevertPullRequest PullRequest } `graphql:"revertPullRequest(input: $input)"` } variables := map[string]interface{}{ "input": params, } - - return client.Mutate(repo.RepoHost(), "PullRequestRevert", &mutation, variables) + err := client.Mutate(repo.RepoHost(), "PullRequestRevert", &mutation, variables) + if err != nil { + return nil, err + } + revertPR := &mutation.RevertPullRequest.RevertPullRequest + return revertPR, err } func ConvertPullRequestToDraft(client *Client, repo ghrepo.Interface, pr *PullRequest) error { diff --git a/pkg/cmd/pr/revert/revert.go b/pkg/cmd/pr/revert/revert.go index 7429f3436..72ce53490 100644 --- a/pkg/cmd/pr/revert/revert.go +++ b/pkg/cmd/pr/revert/revert.go @@ -111,12 +111,22 @@ func revertRun(opts *RevertOptions) error { Draft: githubv4.NewBoolean(githubv4.Boolean(opts.IsDraft)), } - err = api.PullRequestRevert(apiClient, baseRepo, params) + revertPR, err := api.PullRequestRevert(apiClient, baseRepo, params) if err != nil { return fmt.Errorf("API call failed: %w", err) } - fmt.Fprintf(opts.IO.ErrOut, "%s Created revert PR for pull request %s#%d (%s)\n", cs.SuccessIconWithColor(cs.Green), ghrepo.FullName(baseRepo), pr.Number, pr.Title) + fmt.Fprintf( + opts.IO.ErrOut, + "%s Created pull request %s#%d (%s) that reverts %s#%d (%s)\n", + cs.SuccessIconWithColor(cs.Green), + ghrepo.FullName(baseRepo), + revertPR.Number, + revertPR.Title, + ghrepo.FullName(baseRepo), + pr.Number, + pr.Title, + ) return nil } diff --git a/pkg/cmd/pr/revert/revert_test.go b/pkg/cmd/pr/revert/revert_test.go index edd0a8595..2833f36f8 100644 --- a/pkg/cmd/pr/revert/revert_test.go +++ b/pkg/cmd/pr/revert/revert_test.go @@ -62,7 +62,15 @@ func TestPRRevert(t *testing.T) { http.Register( httpmock.GraphQL(`mutation PullRequestRevert\b`), - httpmock.GraphQLMutation(`{"id": "SOME-ID"}`, + httpmock.GraphQLMutation(` + { "data": { "revertPullRequest": { "pullRequest": { + "ID": "SOME-ID" + }, "revertPullRequest": { + "ID": "NEW-ID", + "Title": "Revert PR title", + "Number": 456 + } } } } + `, func(inputs map[string]interface{}) { assert.Equal(t, inputs["pullRequestId"], "SOME-ID") }), @@ -71,7 +79,7 @@ func TestPRRevert(t *testing.T) { output, err := runCommand(http, true, "123") assert.NoError(t, err) assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created revert PR for pull request OWNER/REPO#123 (The title of the PR)\n", output.Stderr()) + assert.Equal(t, "✓ Created pull request OWNER/REPO#456 (Revert PR title) that reverts OWNER/REPO#123 (The title of the PR)\n", output.Stderr()) } func TestPRRevert_notRevertable(t *testing.T) { @@ -96,7 +104,7 @@ func TestPRRevert_withAllOpts(t *testing.T) { defer http.Verify(t) shared.RunCommandFinder("123", &api.PullRequest{ - ID: "THE-ID", + ID: "SOME-ID", Number: 123, State: "MERGED", Title: "The title of the PR", @@ -104,9 +112,17 @@ func TestPRRevert_withAllOpts(t *testing.T) { http.Register( httpmock.GraphQL(`mutation PullRequestRevert\b`), - httpmock.GraphQLMutation(`{"id": "THE-ID" }`, + httpmock.GraphQLMutation(` + { "data": { "revertPullRequest": { "pullRequest": { + "ID": "SOME-ID" + }, "revertPullRequest": { + "ID": "NEW-ID", + "Title": "Revert PR title", + "Number": 456 + } } } } + `, func(inputs map[string]interface{}) { - assert.Equal(t, inputs["pullRequestId"], "THE-ID") + assert.Equal(t, inputs["pullRequestId"], "SOME-ID") assert.Equal(t, inputs["title"], "Revert PR title") assert.Equal(t, inputs["body"], "Revert PR body") assert.Equal(t, inputs["draft"], true) @@ -116,5 +132,5 @@ func TestPRRevert_withAllOpts(t *testing.T) { output, err := runCommand(http, true, "123 --title 'Revert PR title' --body 'Revert PR body' --draft") assert.NoError(t, err) assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created revert PR for pull request OWNER/REPO#123 (The title of the PR)\n", output.Stderr()) + assert.Equal(t, "✓ Created pull request OWNER/REPO#456 (Revert PR title) that reverts OWNER/REPO#123 (The title of the PR)\n", output.Stderr()) } From 5368f409a7c48799dfcb54f7d28f2585d4cac85b Mon Sep 17 00:00:00 2001 From: Lucas Melin Date: Mon, 6 Oct 2025 11:51:36 -0400 Subject: [PATCH 50/87] Fix revert unit tests by using renamed helper --- pkg/cmd/pr/revert/revert_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/revert/revert_test.go b/pkg/cmd/pr/revert/revert_test.go index 2833f36f8..aa2d5fe71 100644 --- a/pkg/cmd/pr/revert/revert_test.go +++ b/pkg/cmd/pr/revert/revert_test.go @@ -53,7 +53,7 @@ func TestPRRevert(t *testing.T) { http := &httpmock.Registry{} defer http.Verify(t) - shared.RunCommandFinder("123", &api.PullRequest{ + shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{ ID: "SOME-ID", Number: 123, State: "MERGED", @@ -86,7 +86,7 @@ func TestPRRevert_notRevertable(t *testing.T) { http := &httpmock.Registry{} defer http.Verify(t) - shared.RunCommandFinder("123", &api.PullRequest{ + shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{ ID: "SOME-ID", Number: 123, State: "OPEN", @@ -103,7 +103,7 @@ func TestPRRevert_withAllOpts(t *testing.T) { http := &httpmock.Registry{} defer http.Verify(t) - shared.RunCommandFinder("123", &api.PullRequest{ + shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{ ID: "SOME-ID", Number: 123, State: "MERGED", From b229402cabc81d990496bb6fc42ad191da47d0b7 Mon Sep 17 00:00:00 2001 From: Lucas Melin Date: Mon, 6 Oct 2025 11:51:58 -0400 Subject: [PATCH 51/87] Return nil when err is nil --- api/queries_pr.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 467194017..c34c9a80c 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -782,7 +782,7 @@ func PullRequestRevert(client *Client, repo ghrepo.Interface, params githubv4.Re return nil, err } revertPR := &mutation.RevertPullRequest.RevertPullRequest - return revertPR, err + return revertPR, nil } func ConvertPullRequestToDraft(client *Client, repo ghrepo.Interface, pr *PullRequest) error { From 4f10a525bc966b405b7117435af4c47ada38da5b Mon Sep 17 00:00:00 2001 From: Lucas Melin Date: Mon, 6 Oct 2025 12:52:02 -0400 Subject: [PATCH 52/87] Adjust PR revert based on new acceptance criteria Also adds test cases for the new acceptance criteria. --- pkg/cmd/pr/revert/revert.go | 16 +-- pkg/cmd/pr/revert/revert_test.go | 234 ++++++++++++++++++++++++++++--- 2 files changed, 218 insertions(+), 32 deletions(-) diff --git a/pkg/cmd/pr/revert/revert.go b/pkg/cmd/pr/revert/revert.go index 72ce53490..474e17af9 100644 --- a/pkg/cmd/pr/revert/revert.go +++ b/pkg/cmd/pr/revert/revert.go @@ -113,20 +113,12 @@ func revertRun(opts *RevertOptions) error { revertPR, err := api.PullRequestRevert(apiClient, baseRepo, params) if err != nil { + fmt.Fprintf(opts.IO.ErrOut, "%s %s\n", cs.FailureIcon(), err) return fmt.Errorf("API call failed: %w", err) } - fmt.Fprintf( - opts.IO.ErrOut, - "%s Created pull request %s#%d (%s) that reverts %s#%d (%s)\n", - cs.SuccessIconWithColor(cs.Green), - ghrepo.FullName(baseRepo), - revertPR.Number, - revertPR.Title, - ghrepo.FullName(baseRepo), - pr.Number, - pr.Title, - ) - + if revertPR != nil { + fmt.Fprintln(opts.IO.Out, revertPR.URL) + } return nil } diff --git a/pkg/cmd/pr/revert/revert_test.go b/pkg/cmd/pr/revert/revert_test.go index aa2d5fe71..7ee9f5496 100644 --- a/pkg/cmd/pr/revert/revert_test.go +++ b/pkg/cmd/pr/revert/revert_test.go @@ -49,7 +49,7 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err }, err } -func TestPRRevert(t *testing.T) { +func TestPRRevert_missingArgument(t *testing.T) { http := &httpmock.Registry{} defer http.Verify(t) @@ -60,26 +60,68 @@ func TestPRRevert(t *testing.T) { Title: "The title of the PR", }, ghrepo.New("OWNER", "REPO")) - http.Register( - httpmock.GraphQL(`mutation PullRequestRevert\b`), - httpmock.GraphQLMutation(` + // No arguments provided. + _, err := runCommand(http, true, "") + // Exits non-zero and prints an argument error. + assert.EqualError(t, err, "cannot revert pull request: number, url, or branch required") +} + +func TestPRRevert_acceptedIdentifierFormats(t *testing.T) { + tests := []struct { + name string + args string + }{ + { + name: "Revert by pull request number", + args: "123", + }, + { + name: "Revert by pull request identifier", + args: "owner/repo#123", + }, + { + name: "Revert by pull request URL", + args: "https://github.com/owner/repo/pull/123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + http := &httpmock.Registry{} + defer http.Verify(t) + + shared.StubFinderForRunCommandStyleTests(t, tt.args, &api.PullRequest{ + ID: "SOME-ID", + Number: 123, + State: "MERGED", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + http.Register( + httpmock.GraphQL(`mutation PullRequestRevert\b`), + httpmock.GraphQLMutation(` { "data": { "revertPullRequest": { "pullRequest": { "ID": "SOME-ID" }, "revertPullRequest": { "ID": "NEW-ID", "Title": "Revert PR title", - "Number": 456 + "Number": 456, + "URL": "https://github.com/OWNER/REPO/pull/456" } } } } `, - func(inputs map[string]interface{}) { - assert.Equal(t, inputs["pullRequestId"], "SOME-ID") - }), - ) + func(inputs map[string]interface{}) { + assert.Equal(t, inputs["pullRequestId"], "SOME-ID") + }), + ) - output, err := runCommand(http, true, "123") - assert.NoError(t, err) - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created pull request OWNER/REPO#456 (Revert PR title) that reverts OWNER/REPO#123 (The title of the PR)\n", output.Stderr()) + output, err := runCommand(http, true, tt.args) + // Revert PR is created and only its URL is printed. + assert.NoError(t, err) + assert.Equal(t, "https://github.com/OWNER/REPO/pull/456\n", output.String()) + assert.Equal(t, "", output.Stderr()) + }) + } } func TestPRRevert_notRevertable(t *testing.T) { @@ -93,13 +135,16 @@ func TestPRRevert_notRevertable(t *testing.T) { Title: "The title of the PR", }, ghrepo.New("OWNER", "REPO")) + // Target PR is not merged. output, err := runCommand(http, true, "123") - assert.Error(t, err) - assert.Equal(t, "", output.String()) + // API error, non-zero exit. + assert.EqualError(t, err, "SilentError") assert.Equal(t, "X Pull request OWNER/REPO#123 (The title of the PR) can't be reverted because it has not been merged\n", output.Stderr()) + // No URL printed. + assert.Equal(t, "", output.String()) } -func TestPRRevert_withAllOpts(t *testing.T) { +func TestPRRevert_withTitleAndBody(t *testing.T) { http := &httpmock.Registry{} defer http.Verify(t) @@ -118,19 +163,168 @@ func TestPRRevert_withAllOpts(t *testing.T) { }, "revertPullRequest": { "ID": "NEW-ID", "Title": "Revert PR title", - "Number": 456 + "Number": 456, + "URL": "https://github.com/OWNER/REPO/pull/456" } } } } `, func(inputs map[string]interface{}) { assert.Equal(t, inputs["pullRequestId"], "SOME-ID") assert.Equal(t, inputs["title"], "Revert PR title") assert.Equal(t, inputs["body"], "Revert PR body") + }), + ) + + output, err := runCommand(http, true, "123 --title 'Revert PR title' --body 'Revert PR body'") + // Revert PR created. + assert.NoError(t, err) + // Only URL printed. + assert.Equal(t, "https://github.com/OWNER/REPO/pull/456\n", output.String()) + assert.Equal(t, "", output.Stderr()) +} + +func TestPRRevert_withDraft(t *testing.T) { + http := &httpmock.Registry{} + defer http.Verify(t) + + shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{ + ID: "SOME-ID", + Number: 123, + State: "MERGED", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + http.Register( + httpmock.GraphQL(`mutation PullRequestRevert\b`), + httpmock.GraphQLMutation(` + { "data": { "revertPullRequest": { "pullRequest": { + "ID": "SOME-ID" + }, "revertPullRequest": { + "ID": "NEW-ID", + "Title": "Revert PR title", + "Number": 456, + "URL": "https://github.com/OWNER/REPO/pull/456" + } } } } + `, + func(inputs map[string]interface{}) { + assert.Equal(t, inputs["pullRequestId"], "SOME-ID") assert.Equal(t, inputs["draft"], true) }), ) - output, err := runCommand(http, true, "123 --title 'Revert PR title' --body 'Revert PR body' --draft") + output, err := runCommand(http, true, "123 --draft") + // Revert PR created as a draft. assert.NoError(t, err) - assert.Equal(t, "", output.String()) - assert.Equal(t, "✓ Created pull request OWNER/REPO#456 (Revert PR title) that reverts OWNER/REPO#123 (The title of the PR)\n", output.Stderr()) + // Only URL printed. + assert.Equal(t, "https://github.com/OWNER/REPO/pull/456\n", output.String()) + assert.Equal(t, "", output.Stderr()) +} + +func TestPRRevert_APIFailure(t *testing.T) { + http := &httpmock.Registry{} + defer http.Verify(t) + + shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{ + ID: "SOME-ID", + Number: 123, + State: "MERGED", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + http.Register( + httpmock.GraphQL(`mutation PullRequestRevert\b`), + httpmock.GraphQLMutation(` + { "errors": [{ + "message": "Authorization error" + }]}`, + func(inputs map[string]interface{}) { + assert.Equal(t, inputs["pullRequestId"], "SOME-ID") + }), + ) + + output, err := runCommand(http, true, "123") + // Non-zero exit, stderr shows the API error, stdout empty. + assert.EqualError(t, err, "API call failed: GraphQL: Authorization error") + assert.Equal(t, "X GraphQL: Authorization error\n", output.Stderr()) + assert.Equal(t, "", output.String()) +} + +func TestPRRevert_multipleInvocations(t *testing.T) { + http := &httpmock.Registry{} + defer http.Verify(t) + + shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{ + ID: "SOME-ID", + Number: 123, + State: "MERGED", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + http.Register( + httpmock.GraphQL(`mutation PullRequestRevert\b`), + httpmock.GraphQLMutation(` + { "data": { "revertPullRequest": { "pullRequest": { + "ID": "SOME-ID" + }, "revertPullRequest": { + "ID": "NEW-ID", + "Title": "Revert PR title", + "Number": 456, + "URL": "https://github.com/OWNER/REPO/pull/456" + } } } } + `, + func(inputs map[string]interface{}) { + assert.Equal(t, inputs["pullRequestId"], "SOME-ID") + }), + ) + + output, err := runCommand(http, true, "123") + // Revert PR is created and only its URL is printed. + assert.NoError(t, err) + assert.Equal(t, "https://github.com/OWNER/REPO/pull/456\n", output.String()) + assert.Equal(t, "", output.Stderr()) + + // Invoke the same command, behavior depends solely on API response + shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{ + ID: "SOME-ID", + Number: 123, + State: "MERGED", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + http.Register( + httpmock.GraphQL(`mutation PullRequestRevert\b`), + httpmock.GraphQLMutation(` + { "data": { "revertPullRequest": { "pullRequest": { + "ID": "SOME-ID" + }, "revertPullRequest": { + "ID": "NEW-ID", + "Title": "Revert PR title", + "Number": 456, + "URL": "https://github.com/OWNER/REPO/pull/456" + } } } } + `, + func(inputs map[string]interface{}) { + assert.Equal(t, inputs["pullRequestId"], "SOME-ID") + }), + ) + + output, err = runCommand(http, true, "123") + // Revert PR is created and only its URL is printed. + assert.NoError(t, err) + assert.Equal(t, "https://github.com/OWNER/REPO/pull/456\n", output.String()) + assert.Equal(t, "", output.Stderr()) + + // Invoke the same command, behavior depends solely on API response. + shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{ + ID: "SOME-ID", + Number: 123, + State: "OPEN", + Title: "The title of the PR", + }, ghrepo.New("OWNER", "REPO")) + + output, err = runCommand(http, true, "123") + // Revert PR is not created, API error, non-zero exit. + assert.EqualError(t, err, "SilentError") + assert.Equal(t, "X Pull request OWNER/REPO#123 (The title of the PR) can't be reverted because it has not been merged\n", output.Stderr()) + // No URL printed. + assert.Equal(t, "", output.String()) } From ef7ac8caab6ea3c7b86504ea715e836b12b8d5e7 Mon Sep 17 00:00:00 2001 From: Lucas Melin Date: Mon, 6 Oct 2025 12:56:09 -0400 Subject: [PATCH 53/87] Address remaining PR comments for revert implementation --- pkg/cmd/pr/revert/revert.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/revert/revert.go b/pkg/cmd/pr/revert/revert.go index 474e17af9..544550d41 100644 --- a/pkg/cmd/pr/revert/revert.go +++ b/pkg/cmd/pr/revert/revert.go @@ -106,10 +106,18 @@ func revertRun(opts *RevertOptions) error { params := githubv4.RevertPullRequestInput{ PullRequestID: pr.ID, - Title: githubv4.NewString(githubv4.String(opts.Title)), - Body: githubv4.NewString(githubv4.String(opts.Body)), Draft: githubv4.NewBoolean(githubv4.Boolean(opts.IsDraft)), } + // Only set the Body field when opts.BodySet is true to avoid overriding + // GitHub's default revert body generation. + if opts.BodySet { + params.Body = githubv4.NewString(githubv4.String(opts.Body)) + } + // Only set the Title field when opts.Title is not empty to avoid overriding + // GitHub's default revert title generation. + if opts.Title != "" { + params.Title = githubv4.NewString(githubv4.String(opts.Title)) + } revertPR, err := api.PullRequestRevert(apiClient, baseRepo, params) if err != nil { From a78bb5e89940c9cc7a6d4d28f05c01e8ade3a507 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:19:45 -0600 Subject: [PATCH 54/87] Fix --follow not killing the progress indicator Fixes --follow not stopping the progress indicator. Also includes a nice message to indicate what is happening because even after we create the agent task, there's a period of time where we poll and receive nothing as the task session starts. We want there to be some sort of feedback in that period of time to not make the user panic and think it has hanged. --- pkg/cmd/agent-task/create/create.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/agent-task/create/create.go b/pkg/cmd/agent-task/create/create.go index 9c9b8ed68..cf5f7fd11 100644 --- a/pkg/cmd/agent-task/create/create.go +++ b/pkg/cmd/agent-task/create/create.go @@ -166,6 +166,8 @@ func createRun(opts *CreateOptions) error { } if opts.Follow { + opts.IO.StopProgressIndicator() + fmt.Fprintf(opts.IO.Out, "Displaying session logs for job %s. Press Ctrl+C to stop.\n", job.ID) return followLogs(opts, client, job.SessionID) } From 91c6bc609a8a324fe7c984d631791a574b7ffeb8 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:37:43 -0600 Subject: [PATCH 55/87] Add new displaying message to test expectation Updated the Test_createRun test to expect a message indicating that session logs are being displayed for the job --- pkg/cmd/agent-task/create/create_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/agent-task/create/create_test.go b/pkg/cmd/agent-task/create/create_test.go index ef7f529ac..aa02150ba 100644 --- a/pkg/cmd/agent-task/create/create_test.go +++ b/pkg/cmd/agent-task/create/create_test.go @@ -488,6 +488,7 @@ func Test_createRun(t *testing.T) { } }, wantStdout: heredoc.Doc(` + Displaying session logs for job job123. Press Ctrl+C to stop. (rendered:) (rendered:) `), From af0905efeb2948be89e62f04079e57714c517515 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 14:03:20 +0000 Subject: [PATCH 56/87] chore(deps): bump github/codeql-action from 3 to 4 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/govulncheck.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1e3cd897c..f3525a591 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -34,13 +34,13 @@ jobs: go-version-file: "go.mod" - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} queries: security-and-quality - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{ matrix.language }}" upload: false @@ -56,7 +56,7 @@ jobs: output: sarif-results/${{ matrix.language }}.sarif - name: Upload filtered SARIF - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-results/${{ matrix.language }}.sarif category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 7cc113878..fa0fb6f51 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -24,6 +24,6 @@ jobs: go run golang.org/x/vuln/cmd/govulncheck@d1f380186385b4f64e00313f31743df8e4b89a77 -format sarif ./... > gh.sarif - name: Upload SARIF report - uses: github/codeql-action/upload-sarif@9b02dc2f60288b463e7a66e39c78829b62780db7 # 2.22.1 + uses: github/codeql-action/upload-sarif@a8d1ac45b9a34d11fe398d5503176af0d06b303e # 2.22.1 with: sarif_file: gh.sarif From 8840df2eb3c871fef1d53b9ccfa11d2d892e6bb9 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:39:52 -0600 Subject: [PATCH 57/87] Fix agentTaskCmd to use repoResolvingCmdFactory `agent-task` uses smart base repo func to fix bug with resolving upstream instead of local repo. --- pkg/cmd/root/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 27f028e44..0a4f04e35 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -127,7 +127,6 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, cmd.AddCommand(versionCmd.NewCmdVersion(f, version, buildDate)) cmd.AddCommand(accessibilityCmd.NewCmdAccessibility(f)) cmd.AddCommand(actionsCmd.NewCmdActions(f)) - cmd.AddCommand(agentTaskCmd.NewCmdAgentTask(f)) cmd.AddCommand(aliasCmd.NewCmdAlias(f)) cmd.AddCommand(authCmd.NewCmdAuth(f)) cmd.AddCommand(attestationCmd.NewCmdAttestation(f)) @@ -150,6 +149,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, repoResolvingCmdFactory := *f repoResolvingCmdFactory.BaseRepo = factory.SmartBaseRepoFunc(f) + cmd.AddCommand(agentTaskCmd.NewCmdAgentTask(&repoResolvingCmdFactory)) cmd.AddCommand(browseCmd.NewCmdBrowse(&repoResolvingCmdFactory, nil)) cmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory)) cmd.AddCommand(orgCmd.NewCmdOrg(&repoResolvingCmdFactory)) From b9e04ef83d40edefed972da7c0ac69fda5a5ad3f Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 13 Oct 2025 16:33:03 +0100 Subject: [PATCH 58/87] test(api): improve `NewHTTPClient` test assertions The assertions should check for header values instead of single string, because when getting a single header value, we get an empty string if the header is not set or is set to an empty string. So, to make sure a header is not set we should check the `Values` slice which is `nil` only if the header is missing. Signed-off-by: Babak K. Shandiz --- api/http_client_test.go | 44 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/api/http_client_test.go b/api/http_client_test.go index ce20a2684..216024699 100644 --- a/api/http_client_test.go +++ b/api/http_client_test.go @@ -26,7 +26,7 @@ func TestNewHTTPClient(t *testing.T) { name string args args host string - wantHeader map[string]string + wantHeader map[string][]string wantStderr string }{ { @@ -37,10 +37,10 @@ func TestNewHTTPClient(t *testing.T) { logVerboseHTTP: false, }, host: "github.com", - wantHeader: map[string]string{ - "authorization": "token MYTOKEN", - "user-agent": "GitHub CLI v1.2.3", - "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + wantHeader: map[string][]string{ + "authorization": {"token MYTOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -51,10 +51,10 @@ func TestNewHTTPClient(t *testing.T) { appVersion: "v1.2.3", }, host: "example.com", - wantHeader: map[string]string{ - "authorization": "token GHETOKEN", - "user-agent": "GitHub CLI v1.2.3", - "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + wantHeader: map[string][]string{ + "authorization": {"token GHETOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -66,10 +66,10 @@ func TestNewHTTPClient(t *testing.T) { logVerboseHTTP: false, }, host: "github.com", - wantHeader: map[string]string{ - "authorization": "", - "user-agent": "GitHub CLI v1.2.3", - "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + wantHeader: map[string][]string{ + "authorization": nil, // should not be set + "user-agent": {"GitHub CLI v1.2.3"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -81,10 +81,10 @@ func TestNewHTTPClient(t *testing.T) { logVerboseHTTP: false, }, host: "example.com", - wantHeader: map[string]string{ - "authorization": "", - "user-agent": "GitHub CLI v1.2.3", - "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + wantHeader: map[string][]string{ + "authorization": nil, // should not be set + "user-agent": {"GitHub CLI v1.2.3"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -96,10 +96,10 @@ func TestNewHTTPClient(t *testing.T) { logVerboseHTTP: true, }, host: "github.com", - wantHeader: map[string]string{ - "authorization": "token MYTOKEN", - "user-agent": "GitHub CLI v1.2.3", - "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + wantHeader: map[string][]string{ + "authorization": {"token MYTOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: heredoc.Doc(` * Request at