Merge branch 'trunk' into gist-edit-large-file

This commit is contained in:
Lucas Nørgård 2025-10-30 04:18:26 +01:00 committed by GitHub
commit cf7180379a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1956 additions and 734 deletions

View file

@ -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 }}"

View file

@ -50,7 +50,7 @@ jobs:
with:
go-version-file: 'go.mod'
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
with:
version: "~1.17.1"
install-only: true
@ -62,7 +62,7 @@ jobs:
run: |
go run ./cmd/gen-docs --website --doc-path dist/manual
tar -czvf dist/manual.tar.gz -C dist -- manual
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: linux
if-no-files-found: error
@ -103,7 +103,7 @@ jobs:
security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain"
rm "$RUNNER_TEMP/cert.p12"
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
with:
version: "~1.17.1"
install-only: true
@ -134,7 +134,7 @@ jobs:
run: |
shopt -s failglob
script/pkgmacos "$TAG_NAME"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: macos
if-no-files-found: error
@ -157,7 +157,7 @@ jobs:
with:
go-version-file: 'go.mod'
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
with:
version: "~1.17.1"
install-only: true
@ -238,7 +238,7 @@ jobs:
Get-ChildItem -Path .\dist -Filter *.msi | ForEach-Object {
.\script\sign.ps1 $_.FullName
}
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: windows
if-no-files-found: error
@ -256,7 +256,7 @@ jobs:
- name: Checkout cli/cli
uses: actions/checkout@v5
- name: Merge built artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
- name: Checkout documentation site
uses: actions/checkout@v5
with:
@ -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
@ -384,7 +384,7 @@ jobs:
git diff --name-status @{upstream}..
fi
- name: Bump homebrew-core formula
uses: mislav/bump-homebrew-formula-action@8e2baa47daaa8db10fcdeb04105dfa6850eb0d68
uses: mislav/bump-homebrew-formula-action@56a283fa15557e9abaa4bdb63b8212abc68e655c
if: inputs.environment == 'production' && !contains(inputs.tag_name, '-')
with:
formula-name: gh

View file

@ -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@v4
with:
sarif_file: gh.sarif

View file

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Bump homebrew-core formula
uses: mislav/bump-homebrew-formula-action@8e2baa47daaa8db10fcdeb04105dfa6850eb0d68
uses: mislav/bump-homebrew-formula-action@56a283fa15557e9abaa4bdb63b8212abc68e655c
if: inputs.environment == 'production' && !contains(inputs.tag_name, '-')
with:
formula-name: gh

View file

@ -17,22 +17,24 @@ type tokenGetter interface {
}
type HTTPClientOptions struct {
AppVersion string
CacheTTL time.Duration
Config tokenGetter
EnableCache bool
Log io.Writer
LogColorize bool
LogVerboseHTTP bool
AppVersion string
CacheTTL time.Duration
Config tokenGetter
EnableCache bool
Log io.Writer
LogColorize bool
LogVerboseHTTP bool
SkipDefaultHeaders bool
}
func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) {
// Provide invalid host, and token values so gh.HTTPClient will not automatically resolve them.
// The real host and token are inserted at request time.
clientOpts := ghAPI.ClientOptions{
Host: "none",
AuthToken: "none",
LogIgnoreEnv: true,
Host: "none",
AuthToken: "none",
LogIgnoreEnv: true,
SkipDefaultHeaders: opts.SkipDefaultHeaders,
}
debugEnabled, debugValue := utils.IsDebugEnabled()

View file

@ -18,15 +18,16 @@ import (
func TestNewHTTPClient(t *testing.T) {
type args struct {
config tokenGetter
appVersion string
logVerboseHTTP bool
config tokenGetter
appVersion string
logVerboseHTTP bool
skipDefaultHeaders bool
}
tests := []struct {
name string
args args
host string
wantHeader map[string]string
wantHeader map[string][]string
wantStderr string
}{
{
@ -37,10 +38,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 +52,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 +67,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 +82,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 +97,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 <time>
@ -115,6 +116,34 @@ func TestNewHTTPClient(t *testing.T) {
< HTTP/1.1 204 No Content
< Date: <time>
* Request took <duration>
`),
},
{
name: "respect skip default headers option",
args: args{
appVersion: "v1.2.3",
logVerboseHTTP: true,
skipDefaultHeaders: true,
},
host: "github.com",
wantHeader: map[string][]string{
"accept": nil,
"authorization": nil,
"content-type": nil,
"user-agent": {"GitHub CLI v1.2.3"},
},
wantStderr: heredoc.Doc(`
* Request at <time>
* Request to http://<host>:<port>
> GET / HTTP/1.1
> Host: github.com
> Time-Zone: <timezone>
> User-Agent: GitHub CLI v1.2.3
< HTTP/1.1 204 No Content
< Date: <time>
* Request took <duration>
`),
},
@ -131,10 +160,11 @@ func TestNewHTTPClient(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, stderr := iostreams.Test()
client, err := NewHTTPClient(HTTPClientOptions{
AppVersion: tt.args.appVersion,
Config: tt.args.config,
Log: ios.ErrOut,
LogVerboseHTTP: tt.args.logVerboseHTTP,
AppVersion: tt.args.appVersion,
Config: tt.args.config,
Log: ios.ErrOut,
LogVerboseHTTP: tt.args.logVerboseHTTP,
SkipDefaultHeaders: tt.args.skipDefaultHeaders,
})
require.NoError(t, err)
@ -148,7 +178,7 @@ func TestNewHTTPClient(t *testing.T) {
require.NoError(t, err)
for name, value := range tt.wantHeader {
assert.Equal(t, value, gotReq.Header.Get(name), name)
assert.Equal(t, value, gotReq.Header.Values(name), name)
}
assert.Equal(t, 204, res.StatusCode)

View file

@ -1,6 +1,8 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
@ -629,17 +631,58 @@ 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 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
}
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",
url.PathEscape(repo.RepoOwner()),
url.PathEscape(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
}
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"`
}{
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 {
@ -721,6 +764,37 @@ 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) (*PullRequest, error) {
var mutation struct {
RevertPullRequest struct {
PullRequest struct {
ID githubv4.ID
}
RevertPullRequest struct {
ID string
Number int
URL string
}
} `graphql:"revertPullRequest(input: $input)"`
}
variables := map[string]interface{}{
"input": params,
}
err := client.Mutate(repo.RepoHost(), "PullRequestRevert", &mutation, variables)
if err != nil {
return nil, err
}
pr := &mutation.RevertPullRequest.RevertPullRequest
revertPR := &PullRequest{
ID: pr.ID,
Number: pr.Number,
URL: pr.URL,
}
return revertPR, nil
}
func ConvertPullRequestToDraft(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
var mutation struct {
ConvertPullRequestToDraft struct {

85
go.mod
View file

@ -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
@ -23,8 +23,8 @@ require (
github.com/creack/pty v1.1.24
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/gabriel-vasile/mimetype v1.4.10
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
@ -32,7 +32,6 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.7.0
github.com/henvic/httpretty v0.1.4
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec
github.com/in-toto/attestation v1.1.2
github.com/joho/godotenv v1.5.1
@ -43,23 +42,23 @@ require (
github.com/microsoft/dev-tunnels v0.1.13
github.com/muhammadmuzzammil1998/jsonc v1.0.0
github.com/opentracing/opentracing-go v1.2.0
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb
github.com/rivo/tview v0.42.0
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 +66,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 +85,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 +118,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 +130,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,10 +152,11 @@ 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
github.com/henvic/httpretty v0.1.4 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // 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
)

281
go.sum
View file

@ -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/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
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,23 +1284,22 @@ 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=
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kAIBc067SWyuPIrsocjketYW8=
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
github.com/rivo/tview v0.42.0/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=

View file

@ -6,17 +6,13 @@ import (
"io"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/atotto/clipboard"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/cli/oauth"
"github.com/henvic/httpretty"
ghauth "github.com/cli/go-gh/v2/pkg/auth"
)
@ -26,21 +22,15 @@ var (
oauthClientID = "178c6fc778ccc68e1d6a"
// This value is safe to be embedded in version control
oauthClientSecret = "34ddeff2b558a23d38fba8a6de74f086ede1cc0b"
jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)
)
func AuthFlow(oauthHost string, IO *iostreams.IOStreams, notice string, additionalScopes []string, isInteractive bool, b browser.Browser, isCopyToClipboard bool) (string, string, error) {
// AuthFlow initiates an OAuth device or web application flow to acquire a
// token. The provided HTTP client should be a plain client that does not set
// auth or other headers.
func AuthFlow(httpClient *http.Client, oauthHost string, IO *iostreams.IOStreams, notice string, additionalScopes []string, isInteractive bool, b browser.Browser, isCopyToClipboard bool) (string, string, error) {
w := IO.ErrOut
cs := IO.ColorScheme()
httpClient := &http.Client{}
debugEnabled, debugValue := utils.IsDebugEnabled()
if debugEnabled {
logTraffic := strings.Contains(debugValue, "api")
httpClient.Transport = verboseLog(IO.ErrOut, logTraffic, IO.ColorEnabled())(httpClient.Transport)
}
minimumScopes := []string{"repo", "read:org", "gist"}
scopes := append(minimumScopes, additionalScopes...)
@ -150,28 +140,3 @@ func waitForEnter(r io.Reader) error {
scanner.Scan()
return scanner.Err()
}
func verboseLog(out io.Writer, logTraffic bool, colorize bool) func(http.RoundTripper) http.RoundTripper {
logger := &httpretty.Logger{
Time: true,
TLS: false,
Colors: colorize,
RequestHeader: logTraffic,
RequestBody: logTraffic,
ResponseHeader: logTraffic,
ResponseBody: logTraffic,
Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}},
MaxResponseBody: 10000,
}
logger.SetOutput(out)
logger.SetBodyFilter(func(h http.Header) (skip bool, err error) {
return !inspectableMIMEType(h.Get("Content-Type")), nil
})
return logger.RoundTripper
}
func inspectableMIMEType(t string) bool {
return strings.HasPrefix(t, "text/") ||
strings.HasPrefix(t, "application/x-www-form-urlencoded") ||
jsonTypeRE.MatchString(t)
}

View file

@ -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)
}

View file

@ -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:) <raw-logs-one>
(rendered:) <raw-logs-two>
`),

View file

@ -41,8 +41,10 @@ func OnGetByDigestSuccess(params FetchParams) ([]*Attestation, error) {
att3 := makeTestReleaseAttestation()
attestations := []*Attestation{&att1, &att2}
if params.PredicateType != "" {
if params.PredicateType == "https://in-toto.io/attestation/release/v0.1" {
attestations = append(attestations, &att3)
// "release" is a sentinel value that returns all release attestations (v0.1, v0.2, etc.)
// This mimics the GitHub API behavior which handles this server-side
if params.PredicateType == "release" {
return []*Attestation{&att3}, nil
}
return FilterAttestations(params.PredicateType, attestations)
}

View file

@ -63,27 +63,39 @@ func NewLiveSigstoreVerifier(config SigstoreConfig) (*LiveSigstoreVerifier, erro
Logger: config.Logger,
NoPublicGood: config.NoPublicGood,
}
// if a custom trusted root is set, configure custom verifiers
// if a custom trusted root is set, configure custom verifiers and assume no Public Good or GitHub verifiers
// are needed
if config.TrustedRoot != "" {
customVerifiers, err := createCustomVerifiers(config.TrustedRoot, config.NoPublicGood)
if err != nil {
return nil, err
return nil, fmt.Errorf("error creating custom verifiers: %s", err)
}
liveVerifier.Custom = customVerifiers
return liveVerifier, nil
}
// No custom trusted root is set, so configure Public Good and GitHub verifiers
if !config.NoPublicGood {
publicGoodVerifier, err := newPublicGoodVerifier(config.TUFMetadataDir, config.HttpClient)
if err != nil {
return nil, err
// Log warning but continue - PGI unavailability should not block GitHub attestation verification
config.Logger.VerbosePrintf("Warning: failed to initialize Sigstore Public Good verifier: %v\n", err)
config.Logger.VerbosePrintf("Continuing without Public Good Instance verification\n")
} else {
liveVerifier.PublicGood = publicGoodVerifier
}
liveVerifier.PublicGood = publicGoodVerifier
}
github, err := newGitHubVerifier(config.TrustDomain, config.TUFMetadataDir, config.HttpClient)
if err != nil {
return nil, err
config.Logger.VerbosePrintf("Warning: failed to initialize GitHub verifier: %v\n", err)
} else {
liveVerifier.GitHub = github
}
if liveVerifier.noVerifierSet() {
return nil, fmt.Errorf("no valid Sigstore verifiers could be initialized")
}
liveVerifier.GitHub = github
return liveVerifier, nil
}
@ -206,6 +218,9 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.Verifier,
if v.NoPublicGood {
return nil, fmt.Errorf("detected public good instance but requested verification without public good instance")
}
if v.PublicGood == nil {
return nil, fmt.Errorf("public good verifier is not available (initialization may have failed)")
}
return v.PublicGood, nil
case GitHubIssuerOrg:
return v.GitHub, nil
@ -372,3 +387,7 @@ func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verif
return sv, nil
}
func (v *LiveSigstoreVerifier) noVerifierSet() bool {
return v.PublicGood == nil && v.GitHub == nil && len(v.Custom) == 0
}

View file

@ -0,0 +1,53 @@
package verification
import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/stretchr/testify/require"
)
// Note: Tests that require network access and TUF client initialization
// are in sigstore_integration_test.go with the //go:build integration tag.
// These unit tests focus on testing the logic without requiring network access.
// TestChooseVerifierWithNilPublicGood tests that chooseVerifier returns an error
// when a PGI attestation is encountered but the PGI verifier is nil (failed initialization).
func TestChooseVerifierWithNilPublicGood(t *testing.T) {
verifier := &LiveSigstoreVerifier{
Logger: io.NewTestHandler(),
NoPublicGood: false,
PublicGood: nil, // Simulate failed PGI initialization
GitHub: nil, // Not needed for this test
}
_, err := verifier.chooseVerifier(PublicGoodIssuerOrg)
require.Error(t, err)
require.ErrorContains(t, err, "public good verifier is not available")
}
// TestChooseVerifierUnrecognizedIssuer tests that an error is returned
// for unrecognized issuers.
func TestChooseVerifierUnrecognizedIssuer(t *testing.T) {
verifier := &LiveSigstoreVerifier{
Logger: io.NewTestHandler(),
NoPublicGood: false,
}
_, err := verifier.chooseVerifier("unknown-issuer")
require.Error(t, err)
require.ErrorContains(t, err, "leaf certificate issuer is not recognized")
}
func TestLiveSigstoreVerifier_noVerifierSet(t *testing.T) {
verifier := &LiveSigstoreVerifier{
Logger: io.NewTestHandler(),
NoPublicGood: true,
PublicGood: nil,
GitHub: nil,
}
require.True(t, verifier.noVerifierSet())
}

View file

@ -20,12 +20,13 @@ import (
)
type LoginOptions struct {
IO *iostreams.IOStreams
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
GitClient *git.Client
Prompter shared.Prompt
Browser browser.Browser
IO *iostreams.IOStreams
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
PlainHttpClient func() (*http.Client, error)
GitClient *git.Client
Prompter shared.Prompt
Browser browser.Browser
MainExecutable string
@ -43,12 +44,13 @@ type LoginOptions struct {
func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
opts := &LoginOptions{
IO: f.IOStreams,
Config: f.Config,
HttpClient: f.HttpClient,
GitClient: f.GitClient,
Prompter: f.Prompter,
Browser: f.Browser,
IO: f.IOStreams,
Config: f.Config,
HttpClient: f.HttpClient,
PlainHttpClient: f.PlainHttpClient,
GitClient: f.GitClient,
Prompter: f.Prompter,
Browser: f.Browser,
}
var tokenStdin bool
@ -190,6 +192,11 @@ func loginRun(opts *LoginOptions) error {
return cmdutil.SilentError
}
plainHTTPClient, err := opts.PlainHttpClient()
if err != nil {
return err
}
httpClient, err := opts.HttpClient()
if err != nil {
return err
@ -210,16 +217,17 @@ func loginRun(opts *LoginOptions) error {
}
return shared.Login(&shared.LoginOptions{
IO: opts.IO,
Config: authCfg,
HTTPClient: httpClient,
Hostname: hostname,
Interactive: opts.Interactive,
Web: opts.Web,
Scopes: opts.Scopes,
GitProtocol: opts.GitProtocol,
Prompter: opts.Prompter,
Browser: opts.Browser,
IO: opts.IO,
Config: authCfg,
HTTPClient: httpClient,
PlainHTTPClient: plainHTTPClient,
Hostname: hostname,
Interactive: opts.Interactive,
Web: opts.Web,
Scopes: opts.Scopes,
GitProtocol: opts.GitProtocol,
Prompter: opts.Prompter,
Browser: opts.Browser,
CredentialFlow: &shared.GitCredentialFlow{
Prompter: opts.Prompter,
HelperConfig: &gitcredentials.HelperConfig{

View file

@ -483,6 +483,9 @@ func Test_loginRun_nontty(t *testing.T) {
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
tt.opts.PlainHttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
if tt.httpStubs != nil {
tt.httpStubs(reg)
}
@ -775,6 +778,9 @@ func Test_loginRun_Survey(t *testing.T) {
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
tt.opts.PlainHttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
if tt.httpStubs != nil {
tt.httpStubs(reg)
} else {

View file

@ -21,11 +21,11 @@ type token string
type username string
type RefreshOptions struct {
IO *iostreams.IOStreams
Config func() (gh.Config, error)
HttpClient *http.Client
GitClient *git.Client
Prompter shared.Prompt
IO *iostreams.IOStreams
Config func() (gh.Config, error)
PlainHttpClient func() (*http.Client, error)
GitClient *git.Client
Prompter shared.Prompt
MainExecutable string
@ -33,7 +33,7 @@ type RefreshOptions struct {
Scopes []string
RemoveScopes []string
ResetScopes bool
AuthFlow func(*iostreams.IOStreams, string, []string, bool, bool) (token, username, error)
AuthFlow func(*http.Client, *iostreams.IOStreams, string, []string, bool, bool) (token, username, error)
Interactive bool
InsecureStorage bool
@ -44,13 +44,13 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
opts := &RefreshOptions{
IO: f.IOStreams,
Config: f.Config,
AuthFlow: func(io *iostreams.IOStreams, hostname string, scopes []string, interactive bool, clipboard bool) (token, username, error) {
t, u, err := authflow.AuthFlow(hostname, io, "", scopes, interactive, f.Browser, clipboard)
AuthFlow: func(httpClient *http.Client, io *iostreams.IOStreams, hostname string, scopes []string, interactive bool, clipboard bool) (token, username, error) {
t, u, err := authflow.AuthFlow(httpClient, hostname, io, "", scopes, interactive, f.Browser, clipboard)
return token(t), username(u), err
},
HttpClient: &http.Client{},
GitClient: f.GitClient,
Prompter: f.Prompter,
PlainHttpClient: f.PlainHttpClient,
GitClient: f.GitClient,
Prompter: f.Prompter,
}
cmd := &cobra.Command{
@ -125,6 +125,11 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
}
func refreshRun(opts *RefreshOptions) error {
plainHTTPClient, err := opts.PlainHttpClient()
if err != nil {
return err
}
cfg, err := opts.Config()
if err != nil {
return err
@ -171,7 +176,7 @@ func refreshRun(opts *RefreshOptions) error {
if !opts.ResetScopes {
if oldToken, _ := authCfg.ActiveToken(hostname); oldToken != "" {
if oldScopes, err := shared.GetScopes(opts.HttpClient, hostname, oldToken); err == nil {
if oldScopes, err := shared.GetScopes(plainHTTPClient, hostname, oldToken); err == nil {
for _, s := range strings.Split(oldScopes, ",") {
s = strings.TrimSpace(s)
if s != "" {
@ -204,7 +209,7 @@ func refreshRun(opts *RefreshOptions) error {
additionalScopes.RemoveValues(opts.RemoveScopes)
authedToken, authedUser, err := opts.AuthFlow(opts.IO, hostname, additionalScopes.ToSlice(), opts.Interactive, opts.Clipboard)
authedToken, authedUser, err := opts.AuthFlow(plainHTTPClient, opts.IO, hostname, additionalScopes.ToSlice(), opts.Interactive, opts.Clipboard)
if err != nil {
return err
}

View file

@ -471,7 +471,7 @@ func Test_refreshRun(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
aa := authArgs{}
tt.opts.AuthFlow = func(_ *iostreams.IOStreams, hostname string, scopes []string, interactive bool, clipboard bool) (token, username, error) {
tt.opts.AuthFlow = func(_ *http.Client, _ *iostreams.IOStreams, hostname string, scopes []string, interactive bool, clipboard bool) (token, username, error) {
aa.hostname = hostname
aa.scopes = scopes
aa.interactive = interactive
@ -514,7 +514,9 @@ func Test_refreshRun(t *testing.T) {
}, nil
},
)
tt.opts.HttpClient = &http.Client{Transport: httpReg}
tt.opts.PlainHttpClient = func() (*http.Client, error) {
return &http.Client{Transport: httpReg}, nil
}
pm := &prompter.PrompterMock{}
if tt.prompterStubs != nil {

View file

@ -30,6 +30,7 @@ type LoginOptions struct {
IO *iostreams.IOStreams
Config iconfig
HTTPClient *http.Client
PlainHTTPClient *http.Client
Hostname string
Interactive bool
Web bool
@ -149,7 +150,7 @@ func Login(opts *LoginOptions) error {
if authMode == 0 {
var err error
authToken, username, err = authflow.AuthFlow(hostname, opts.IO, "", append(opts.Scopes, additionalScopes...), opts.Interactive, opts.Browser, opts.CopyToClipboard)
authToken, username, err = authflow.AuthFlow(opts.PlainHTTPClient, hostname, opts.IO, "", append(opts.Scopes, additionalScopes...), opts.Interactive, opts.Browser, opts.CopyToClipboard)
if err != nil {
return fmt.Errorf("failed to authenticate via web browser: %w", err)
}

View file

@ -19,113 +19,106 @@ import (
"github.com/spf13/cobra"
)
type validEntry struct {
active bool
host string
user string
token string
tokenSource string
gitProtocol string
scopes string
type authEntryState string
const (
authEntryStateSuccess = "success"
authEntryStateTimeout = "timeout"
authEntryStateError = "error"
)
type authEntry struct {
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"`
}
func (e validEntry) String(cs *iostreams.ColorScheme) string {
type authStatus struct {
Hosts map[string][]authEntry `json:"hosts"`
}
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
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)))
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(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 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)
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)))
}
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 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
}
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
}
type StatusOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Config func() (gh.Config, error)
Exporter cmdutil.Exporter
Hostname string
ShowToken bool
@ -148,11 +141,32 @@ 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.
`, "`"),
Example: heredoc.Doc(`
# Display authentication status for all accounts on all hosts
$ gh auth status
# Display authentication status for the active account on a specific host
$ gh auth status --active --hostname github.example.com
# Display tokens in plain text
$ gh auth status --show-token
# Format authentication status as JSON
$ gh auth status --json hosts
# Include plain text token in JSON output
$ gh auth status --json hosts --show-token
# Format hosts as a flat JSON array
$ gh auth status --json hosts --jq '.hosts | add'
`),
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil {
return runF(opts)
@ -166,6 +180,9 @@ 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")
// the json flags are intentionally not given a shorthand to avoid conflict with -t/--show-token
cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authStatusFields)
return cmd
}
@ -180,18 +197,26 @@ func statusRun(opts *StatusOptions) error {
stdout := opts.IO.Out
cs := opts.IO.ColorScheme()
statuses := make(map[string]Entries)
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, newAuthStatus())
return nil
}
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 {
// In machine-friendly mode, we always exit with no error.
opts.Exporter.Write(opts.IO, newAuthStatus())
return nil
}
return cmdutil.SilentError
}
@ -200,6 +225,9 @@ func statusRun(opts *StatusOptions) error {
return err
}
var finalErr error
statuses := newAuthStatus()
for _, hostname := range hostnames {
if opts.Hostname != "" && opts.Hostname != hostname {
continue
@ -215,15 +243,14 @@ func statusRun(opts *StatusOptions) error {
active: true,
gitProtocol: gitProtocol,
hostname: hostname,
showToken: opts.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 {
@ -240,28 +267,46 @@ func statusRun(opts *StatusOptions) error {
active: false,
gitProtocol: gitProtocol,
hostname: hostname,
showToken: opts.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
}
@ -270,22 +315,22 @@ 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
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))
}
@ -308,37 +353,39 @@ type buildEntryOptions struct {
active bool
gitProtocol string
hostname string
showToken bool
token string
tokenSource string
username string
}
func buildEntry(httpClient *http.Client, opts buildEntryOptions) Entry {
tokenIsWriteable := authTokenWriteable(opts.tokenSource)
if opts.tokenSource == "oauth_token" {
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.
opts.tokenSource = filepath.Join(config.ConfigDir(), "hosts.yml")
tokenSource = filepath.Join(config.ConfigDir(), "hosts.yml")
}
entry := authEntry{
Active: opts.active,
Host: opts.hostname,
Login: opts.username,
TokenSource: tokenSource,
Token: opts.token,
GitProtocol: opts.gitProtocol,
}
// 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(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 = authEntryStateError
entry.Error = err.Error()
return entry
}
}
@ -347,39 +394,21 @@ 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 = authEntryStateTimeout
entry.Error = err.Error()
return entry
}
return invalidTokenEntry{
active: opts.active,
host: opts.hostname,
user: opts.username,
tokenIsWriteable: tokenIsWriteable,
tokenSource: opts.tokenSource,
}
entry.State = authEntryStateError
entry.Error = err.Error()
return entry
}
entry.Scopes = scopesHeader
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 = authEntryStateSuccess
return entry
}
func authTokenWriteable(src string) bool {
return !strings.HasSuffix(src, "_TOKEN")
}
func isValidEntry(entry Entry) bool {
_, ok := entry.(validEntry)
return ok
}

View file

@ -3,6 +3,7 @@ package status
import (
"bytes"
"context"
"encoding/json"
"net/http"
"path/filepath"
"strings"
@ -14,6 +15,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"
@ -78,14 +80,23 @@ 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)
})
}
}
func TestJSONFields(t *testing.T) {
jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdStatus, []string{
"hosts",
})
}
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)
@ -528,16 +539,185 @@ func Test_statusRun(t *testing.T) {
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe-2
`),
},
{
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: "json, no token for given --hostname",
opts: StatusOptions{
Hostname: "foo.com",
},
jsonFields: []string{"hosts"},
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "gho_abc123", "https")
},
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: "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")
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: `{"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: "json, all valid tokens with hostname",
opts: StatusOptions{
Hostname: "github.com",
},
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")
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: `{"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: "json, all valid tokens with active",
opts: StatusOptions{
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")
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: `{"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: "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", ""),
httpmock.ScopesResponder(""))
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data":{"viewer":{"login":"monalisa"}}}`))
},
wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_TOKEN","gitProtocol":"https"}]}}` + "\n",
},
{
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")
},
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"))
},
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: "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`))
},
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: "json, timeout error",
opts: StatusOptions{
Hostname: "github.com",
},
jsonFields: []string{"hosts"},
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
})
},
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: "json, with show token",
opts: StatusOptions{
Hostname: "github.com",
ShowToken: true,
},
jsonFields: []string{"hosts"},
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: `{"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)
@ -555,6 +735,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)
}
@ -565,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)
@ -574,8 +761,24 @@ 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)
}
// 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
}

View file

@ -164,33 +164,18 @@ 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:
//
// 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.
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 +192,45 @@ 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 {
// 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)
}
// 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 != "" {
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

View file

@ -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",

View file

@ -33,15 +33,16 @@ func New(appVersion string) *cmdutil.Factory {
ExecutableName: "gh",
}
f.IOStreams = ioStreams(f) // Depends on Config
f.HttpClient = httpClientFunc(f, appVersion) // Depends on Config, IOStreams, and appVersion
f.GitClient = newGitClient(f) // Depends on IOStreams, and Executable
f.Remotes = remotesFunc(f) // Depends on Config, and GitClient
f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes
f.Prompter = newPrompter(f) // Depends on Config and IOStreams
f.Browser = newBrowser(f) // Depends on Config, and IOStreams
f.ExtensionManager = extensionManager(f) // Depends on Config, HttpClient, and IOStreams
f.Branch = branchFunc(f) // Depends on GitClient
f.IOStreams = ioStreams(f) // Depends on Config
f.HttpClient = httpClientFunc(f, appVersion) // Depends on Config, IOStreams, and appVersion
f.PlainHttpClient = plainHttpClientFunc(f, appVersion) // Depends on IOStreams, and appVersion
f.GitClient = newGitClient(f) // Depends on IOStreams, and Executable
f.Remotes = remotesFunc(f) // Depends on Config, and GitClient
f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes
f.Prompter = newPrompter(f) // Depends on Config and IOStreams
f.Browser = newBrowser(f) // Depends on Config, and IOStreams
f.ExtensionManager = extensionManager(f) // Depends on Config, HttpClient, and IOStreams
f.Branch = branchFunc(f) // Depends on GitClient
return f
}
@ -207,6 +208,24 @@ func httpClientFunc(f *cmdutil.Factory, appVersion string) func() (*http.Client,
}
}
func plainHttpClientFunc(f *cmdutil.Factory, appVersion string) func() (*http.Client, error) {
return func() (*http.Client, error) {
io := f.IOStreams
opts := api.HTTPClientOptions{
Log: io.ErrOut,
LogColorize: io.ColorEnabled(),
AppVersion: appVersion,
// This is required to prevent automatic setting of auth and other headers.
SkipDefaultHeaders: true,
}
client, err := api.NewHTTPClient(opts)
if err != nil {
return nil, err
}
return client, nil
}
}
func newGitClient(f *cmdutil.Factory) *git.Client {
io := f.IOStreams
ghPath := f.Executable()

View file

@ -710,6 +710,36 @@ func TestSSOURL(t *testing.T) {
}
}
func TestPlainHttpClient(t *testing.T) {
var receivedHeaders *http.Header
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedHeaders = &r.Header
w.WriteHeader(http.StatusNoContent)
}))
defer ts.Close()
f := New("1")
f.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}
ios, _, _, _ := iostreams.Test()
f.IOStreams = ios
client, err := plainHttpClientFunc(f, "v1.2.3")()
require.NoError(t, err)
req, err := http.NewRequest("GET", ts.URL, nil)
require.NoError(t, err)
res, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 204, res.StatusCode)
assert.Equal(t, []string{"GitHub CLI v1.2.3"}, receivedHeaders.Values("User-Agent"))
assert.Nil(t, receivedHeaders.Values("Authorization"))
assert.Nil(t, receivedHeaders.Values("Content-Type"))
assert.Nil(t, receivedHeaders.Values("Accept"))
assert.Nil(t, receivedHeaders.Values("Time-Zone"))
}
func TestNewGitClient(t *testing.T) {
tests := []struct {
name string

View file

@ -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

View file

@ -61,7 +61,7 @@ func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra.
# Create a branch for issue 123 based on the my-feature branch
$ gh issue develop 123 --base my-feature
# Create a branch for issue 123 and checkout it out
# Create a branch for issue 123 and check it out
$ gh issue develop 123 --checkout
# Create a branch in repo monalisa/cli for issue 123 in repo cli/cli

View file

@ -30,7 +30,7 @@ type EditOptions struct {
DetermineEditor func() (string, error)
FieldsToEditSurvey func(prShared.EditPrompter, *prShared.Editable) error
EditFieldsSurvey func(prShared.EditPrompter, *prShared.Editable, string) error
FetchOptions func(*api.Client, ghrepo.Interface, *prShared.Editable) error
FetchOptions func(*api.Client, ghrepo.Interface, *prShared.Editable, gh.ProjectsV1Support) error
IssueNumbers []int
Interactive bool
@ -248,7 +248,7 @@ func editRun(opts *EditOptions) error {
// Fetch editable shared fields once for all issues.
apiClient := api.NewClientFromHTTP(httpClient)
opts.IO.StartProgressIndicatorWithLabel("Fetching repository information")
err = opts.FetchOptions(apiClient, baseRepo, &editable)
err = opts.FetchOptions(apiClient, baseRepo, &editable, opts.Detector.ProjectsV1())
opts.IO.StopProgressIndicator()
if err != nil {
return err

View file

@ -347,6 +347,7 @@ func Test_editRun(t *testing.T) {
{
name: "non-interactive",
input: &EditOptions{
Detector: &fd.EnabledDetectorMock{},
IssueNumbers: []int{123},
Interactive: false,
Editable: prShared.Editable{
@ -403,6 +404,7 @@ func Test_editRun(t *testing.T) {
{
name: "non-interactive multiple issues",
input: &EditOptions{
Detector: &fd.EnabledDetectorMock{},
IssueNumbers: []int{456, 123},
Interactive: false,
Editable: prShared.Editable{
@ -457,6 +459,7 @@ func Test_editRun(t *testing.T) {
{
name: "non-interactive multiple issues with fetch failures",
input: &EditOptions{
Detector: &fd.EnabledDetectorMock{},
IssueNumbers: []int{123, 9999},
Interactive: false,
Editable: prShared.Editable{
@ -504,6 +507,7 @@ func Test_editRun(t *testing.T) {
{
name: "non-interactive multiple issues with update failures",
input: &EditOptions{
Detector: &fd.EnabledDetectorMock{},
IssueNumbers: []int{123, 456},
Interactive: false,
Editable: prShared.Editable{
@ -584,6 +588,7 @@ func Test_editRun(t *testing.T) {
{
name: "interactive",
input: &EditOptions{
Detector: &fd.EnabledDetectorMock{},
IssueNumbers: []int{123},
Interactive: true,
FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error {
@ -623,6 +628,7 @@ func Test_editRun(t *testing.T) {
{
name: "interactive prompts with actor assignee display names when actors available",
input: &EditOptions{
Detector: &fd.EnabledDetectorMock{},
IssueNumbers: []int{123},
Interactive: true,
FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error {

View file

@ -3,6 +3,8 @@ package edit
import (
"fmt"
"net/http"
"slices"
"strings"
"time"
"github.com/MakeNowJust/heredoc"
@ -13,7 +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/shurcooL/githubv4"
"github.com/cli/cli/v2/pkg/set"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
@ -170,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 {
@ -237,7 +239,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,
}
@ -291,13 +293,22 @@ func editRun(opts *EditOptions) error {
apiClient := api.NewClientFromHTTP(httpClient)
opts.IO.StartProgressIndicator()
err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable)
err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable, opts.Detector.ProjectsV1())
opts.IO.StopProgressIndicator()
if err != nil {
return err
}
if opts.Interactive {
// 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)
s.Remove(pr.Author.Login)
editable.Reviewers.Options = s.ToSlice()
}
editorCommand, err := opts.EditorRetriever.Retrieve()
if err != nil {
return err
@ -309,7 +320,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 +331,53 @@ 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()
}
addUsers, addTeams := partitionUsersAndTeams(editable.Reviewers.Value)
// 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)
}
}
removeUsers, removeTeams := partitionUsersAndTeams(toRemove)
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 {
@ -370,13 +398,13 @@ func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error
}
type EditableOptionsFetcher interface {
EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable) error
EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable, gh.ProjectsV1Support) error
}
type fetcher struct{}
func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error {
return shared.FetchOptions(client, repo, opts)
func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable, projectsV1Support gh.ProjectsV1Support) error {
return shared.FetchOptions(client, repo, opts, projectsV1Support)
}
type EditorRetriever interface {
@ -391,13 +419,18 @@ func (e editorRetriever) Retrieve() (string, error) {
return cmdutil.DetermineEditor(e.config)
}
func ghIds(s *[]string) *[]githubv4.ID {
if s == nil {
return nil
// 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)
}
}
ids := make([]githubv4.ID, len(*s))
for i, v := range *s {
ids[i] = v
}
return &ids
return
}

View file

@ -10,6 +10,7 @@ import (
"github.com/cli/cli/v2/api"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
shared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -354,7 +355,7 @@ func Test_editRun(t *testing.T) {
tests := []struct {
name string
input *EditOptions
httpStubs func(*httpmock.Registry)
httpStubs func(*testing.T, *httpmock.Registry)
stdout string
stderr string
}{
@ -411,11 +412,11 @@ func Test_editRun(t *testing.T) {
},
Fetcher: testFetcher{},
},
httpStubs: func(reg *httpmock.Registry) {
mockRepoMetadata(reg, false)
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)
mockPullRequestReviewersUpdate(reg)
mockPullRequestAddReviewers(reg)
mockPullRequestUpdateLabels(reg)
mockProjectV2ItemUpdate(reg)
},
@ -469,8 +470,8 @@ func Test_editRun(t *testing.T) {
},
Fetcher: testFetcher{},
},
httpStubs: func(reg *httpmock.Registry) {
mockRepoMetadata(reg, true)
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockRepoMetadata(reg, mockRepoMetadataOptions{assignees: true, labels: true, projects: true, milestones: true})
mockPullRequestUpdate(reg)
mockPullRequestUpdateActorAssignees(reg)
mockPullRequestUpdateLabels(reg)
@ -483,8 +484,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 +513,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 +543,109 @@ func Test_editRun(t *testing.T) {
},
Fetcher: testFetcher{},
},
httpStubs: func(reg *httpmock.Registry) {
mockRepoMetadata(reg, false)
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)
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(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
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(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)
},
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(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)
},
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(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
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 +682,11 @@ func Test_editRun(t *testing.T) {
Fetcher: testFetcher{},
EditorRetriever: testEditorRetriever{},
},
httpStubs: func(reg *httpmock.Registry) {
mockRepoMetadata(reg, false)
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)
mockPullRequestReviewersUpdate(reg)
mockPullRequestAddReviewers(reg)
mockPullRequestUpdateLabels(reg)
mockProjectV2ItemUpdate(reg)
},
@ -620,8 +726,9 @@ func Test_editRun(t *testing.T) {
Fetcher: testFetcher{},
EditorRetriever: testEditorRetriever{},
},
httpStubs: func(reg *httpmock.Registry) {
mockRepoMetadata(reg, true)
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)
mockPullRequestUpdateActorAssignees(reg)
mockPullRequestUpdateLabels(reg)
@ -634,8 +741,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 +783,10 @@ func Test_editRun(t *testing.T) {
Fetcher: testFetcher{},
EditorRetriever: testEditorRetriever{},
},
httpStubs: func(reg *httpmock.Registry) {
mockRepoMetadata(reg, false)
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)
mockPullRequestReviewersUpdate(reg)
mockPullRequestRemoveReviewers(reg)
mockPullRequestUpdateActorAssignees(reg)
mockPullRequestUpdateLabels(reg)
mockProjectV2ItemUpdate(reg)
@ -712,7 +830,7 @@ func Test_editRun(t *testing.T) {
Fetcher: testFetcher{},
EditorRetriever: testEditorRetriever{},
},
httpStubs: func(reg *httpmock.Registry) {
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
httpmock.StringResponse(`
@ -759,7 +877,7 @@ func Test_editRun(t *testing.T) {
},
Fetcher: testFetcher{},
},
httpStubs: func(reg *httpmock.Registry) {
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
// Notice there is no call to mockReplaceActorsForAssignable()
// and no GraphQL call to RepositoryAssignableActors below.
reg.Register(
@ -777,6 +895,67 @@ func Test_editRun(t *testing.T) {
},
stdout: "https://github.com/OWNER/REPO/pull/123\n",
},
{
name: "non-interactive projects v1 unsupported doesn't fetch v1 metadata",
input: &EditOptions{
Detector: &fd.DisabledDetectorMock{},
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{
Projects: shared.EditableProjects{
EditableSlice: shared.EditableSlice{
Add: []string{"CleanupV2"},
Remove: []string{"RoadmapV2"},
Edited: true,
},
},
},
Fetcher: testFetcher{},
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
// Ensure v1 project queries are NOT made.
reg.Exclude(t, httpmock.GraphQL(`query RepositoryProjectList\b`))
reg.Exclude(t, httpmock.GraphQL(`query OrganizationProjectList\b`))
// Provide only v2 project metadata queries.
reg.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [
{ "title": "CleanupV2", "id": "CLEANUPV2ID" },
{ "title": "RoadmapV2", "id": "ROADMAPV2ID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
reg.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [
{ "title": "TriageV2", "id": "TRIAGEV2ID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
reg.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [
{ "title": "MonalisaV2", "id": "MONALISAV2ID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
mockProjectV2ItemUpdate(reg)
mockPullRequestUpdate(reg)
},
stdout: "https://github.com/OWNER/REPO/pull/123\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -787,7 +966,7 @@ func Test_editRun(t *testing.T) {
reg := &httpmock.Registry{}
defer reg.Verify(t)
tt.httpStubs(reg)
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 }
@ -804,10 +983,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 +1006,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 +1021,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 +1034,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 +1047,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 +1057,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 +1068,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 +1078,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 +1088,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 +1101,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 +1126,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(`{}`))
}
@ -959,8 +1164,8 @@ func mockProjectV2ItemUpdate(reg *httpmock.Registry) {
type testFetcher struct{}
func (f testFetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error {
return shared.FetchOptions(client, repo, opts)
func (f testFetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable, projectsV1Support gh.ProjectsV1Support) error {
return shared.FetchOptions(client, repo, opts, projectsV1Support)
}
type testSurveyor struct {

View file

@ -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),

132
pkg/cmd/pr/revert/revert.go Normal file
View file

@ -0,0 +1,132 @@
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 {<number> | <url> | <branch>}",
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,
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 {
fmt.Fprintf(opts.IO.ErrOut, "%s %s\n", cs.FailureIcon(), err)
return fmt.Errorf("API call failed: %w", err)
}
if revertPR != nil {
fmt.Fprintln(opts.IO.Out, revertPR.URL)
}
return nil
}

View file

@ -0,0 +1,325 @@
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_missingArgument(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"))
// 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",
"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, 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) {
http := &httpmock.Registry{}
defer http.Verify(t)
shared.StubFinderForRunCommandStyleTests(t, "123", &api.PullRequest{
ID: "SOME-ID",
Number: 123,
State: "OPEN",
Title: "The title of the PR",
}, ghrepo.New("OWNER", "REPO"))
// Target PR is not merged.
output, err := runCommand(http, true, "123")
// 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_withTitleAndBody(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",
"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",
"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 --draft")
// Revert PR created as a draft.
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_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",
"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",
"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())
}

View file

@ -2,9 +2,9 @@ package shared
import (
"fmt"
"strings"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/set"
)
@ -78,37 +78,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
@ -427,22 +396,25 @@ func FieldsToEditSurvey(p EditPrompter, editable *Editable) error {
return nil
}
func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error {
func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable, projectV1Support gh.ProjectsV1Support) 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,
ProjectsV1: editable.Projects.Edited,
ProjectsV1: editable.Projects.Edited && projectV1Support == gh.ProjectsV1Supported,
ProjectsV2: editable.Projects.Edited,
Milestones: editable.Milestone.Edited,
}

View file

@ -106,6 +106,19 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
This may result in the same or duplicate release which may not be desirable in some cases.
Use %[1]s--fail-on-no-commits%[1]s to fail if no new commits are available. This flag has no
effect if there are no existing releases or this is the very first release.
## Immutable Releases
When release immutability is enabled for a repository, the following protections are enforced:
- Git tags associated with a release cannot be modified or deleted.
- Release assets cannot be modified or deleted.
Immutability is enforced only after a release is published. Draft releases can be modified
or deleted, and the associated git tags can be modified or deleted as well.
When using the %[1]screate%[1]s command to attach assets to a release, separate API calls
are made to create the release as a draft, upload the assets, and then publish the release.
Immutability protections will be enforced ONLY after the release is published.
`, "`"),
Example: heredoc.Doc(`
# Interactively create a release

View file

@ -17,8 +17,6 @@ import (
"google.golang.org/protobuf/encoding/protojson"
)
const ReleasePredicateType = "https://in-toto.io/attestation/release/v0.1"
type Verifier interface {
// VerifyAttestation verifies the attestation for a given artifact
VerifyAttestation(art *artifact.DigestedArtifact, att *api.Attestation) (*verification.AttestationProcessingResult, error)

View file

@ -40,23 +40,16 @@ func NewCmdVerifyAsset(f *cmdutil.Factory, runF func(*VerifyAssetConfig) error)
cmd := &cobra.Command{
Use: "verify-asset [<tag>] <file-path>",
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.
`),
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
@ -154,7 +147,7 @@ func verifyAssetRun(config *VerifyAssetConfig) error {
// Find attestations for the release tag SHA
attestations, err := config.AttClient.GetByDigest(api.FetchParams{
Digest: releaseRefDigest.DigestWithAlg(),
PredicateType: shared.ReleasePredicateType,
PredicateType: "release",
Owner: baseRepo.RepoOwner(),
Repo: baseRepo.RepoOwner() + "/" + baseRepo.RepoName(),
// TODO: Allow this value to be set via a flag.

View file

@ -41,21 +41,16 @@ func NewCmdVerify(f *cmdutil.Factory, runF func(config *VerifyConfig) error) *co
opts := &VerifyOptions{}
cmd := &cobra.Command{
Use: "verify [<tag>]",
Short: "Verify the attestation for a GitHub Release.",
Hidden: true,
Args: cobra.MaximumNArgs(1),
Use: "verify [<tag>]",
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
@ -140,7 +135,7 @@ func verifyRun(config *VerifyConfig) error {
// Find all the attestations for the release tag SHA
attestations, err := config.AttClient.GetByDigest(api.FetchParams{
Digest: releaseRefDigest.DigestWithAlg(),
PredicateType: shared.ReleasePredicateType,
PredicateType: "release",
Owner: baseRepo.RepoOwner(),
Repo: baseRepo.RepoOwner() + "/" + baseRepo.RepoName(),
Initiator: "github",

View file

@ -62,6 +62,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
if !opts.IO.CanPrompt() {
return cmdutil.FlagErrorf("cannot non-interactively delete current repository. Please specify a repository or run interactively")
}
_, _ = fmt.Fprintln(opts.IO.ErrOut, "Warning: `--yes` is ignored since no repository was specified")
opts.Confirmed = false
}

View file

@ -16,12 +16,13 @@ import (
func TestNewCmdDelete(t *testing.T) {
tests := []struct {
name string
input string
tty bool
output DeleteOptions
wantErr bool
errMsg string
name string
input string
tty bool
output DeleteOptions
wantErr bool
wantErrMsg string
wantStderr string
}{
{
name: "confirm flag",
@ -36,11 +37,11 @@ func TestNewCmdDelete(t *testing.T) {
output: DeleteOptions{RepoArg: "OWNER/REPO", Confirmed: true},
},
{
name: "no confirmation notty",
input: "OWNER/REPO",
output: DeleteOptions{RepoArg: "OWNER/REPO"},
wantErr: true,
errMsg: "--yes required when not running interactively",
name: "no confirmation notty",
input: "OWNER/REPO",
output: DeleteOptions{RepoArg: "OWNER/REPO"},
wantErr: true,
wantErrMsg: "--yes required when not running interactively",
},
{
name: "base repo resolution",
@ -49,33 +50,37 @@ func TestNewCmdDelete(t *testing.T) {
output: DeleteOptions{},
},
{
name: "yes flag ignored when no argument tty",
tty: true,
input: "--yes",
output: DeleteOptions{Confirmed: false}, // --yes should be ignored
name: "yes flag ignored when no argument tty",
tty: true,
input: "--yes",
output: DeleteOptions{Confirmed: false}, // --yes should be ignored
wantErr: false,
wantStderr: "Warning: `--yes` is ignored since no repository was specified\n",
},
{
name: "yes flag error when no argument notty",
input: "--yes",
wantErr: true,
errMsg: "cannot non-interactively delete current repository. Please specify a repository or run interactively",
name: "yes flag error when no argument notty",
input: "--yes",
wantErr: true,
wantErrMsg: "cannot non-interactively delete current repository. Please specify a repository or run interactively",
},
{
name: "confirm flag error when no argument notty",
input: "--confirm",
wantErr: true,
errMsg: "cannot non-interactively delete current repository. Please specify a repository or run interactively",
name: "confirm flag error when no argument notty",
input: "--confirm",
wantErr: true,
wantErrMsg: "cannot non-interactively delete current repository. Please specify a repository or run interactively",
},
{
name: "confirm flag also ignored when no argument tty",
tty: true,
input: "--confirm",
output: DeleteOptions{Confirmed: false}, // --confirm should also be ignored
name: "confirm flag also ignored when no argument tty",
tty: true,
input: "--confirm",
output: DeleteOptions{Confirmed: false}, // --confirm should also be ignored
wantStderr: "Warning: `--yes` is ignored since no repository was specified\n",
// Note: This does not confuse the user, as deprecation warnings are shown: "Flag --confirm has been deprecated, use `--yes` instead"
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
ios, _, _, stdErr := iostreams.Test()
ios.SetStdinTTY(tt.tty)
ios.SetStdoutTTY(tt.tty)
f := &cmdutil.Factory{
@ -91,15 +96,17 @@ func TestNewCmdDelete(t *testing.T) {
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetErr(stdErr)
_, err = cmd.ExecuteC()
if tt.wantErr {
assert.Error(t, err)
assert.Equal(t, tt.errMsg, err.Error())
assert.Equal(t, tt.wantErrMsg, err.Error())
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantStderr, stdErr.String())
assert.Equal(t, tt.output.RepoArg, gotOpts.RepoArg)
})
}

View file

@ -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))

View file

@ -30,7 +30,11 @@ type Factory struct {
Branch func() (string, error)
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
Remotes func() (context.Remotes, error)
// PlainHttpClient is a special HTTP client that does not automatically set
// auth and other headers. This is meant to be used in situations where the
// client needs to specify the headers itself (e.g. during login).
PlainHttpClient func() (*http.Client, error)
Remotes func() (context.Remotes, error)
}
// Executable is the path to the currently invoked binary

View file

@ -25,9 +25,33 @@ type JSONFlagError struct {
func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) {
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 := cmd.Flags()
addJsonFlag(f)
addJqFlag(f, "")
addTemplateFlag(f, "")
setupJsonFlags(cmd, exportTarget, fields)
}
func addJsonFlag(f *pflag.FlagSet) {
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\"")
}
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) {
_ = cmd.RegisterFlagCompletionFunc("json", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var results []string

View file

@ -119,6 +119,44 @@ func TestAddJSONFlags(t *testing.T) {
}
}
func TestAddJSONFlagsWithoutShorthand(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", "", "")
cmd.Flags().StringP("token", "t", "", "")
},
wantFlags: map[string]string{
"web": "w",
"token": "t",
"jq": "",
"template": "",
"json": "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := &cobra.Command{Run: func(*cobra.Command, []string) {}}
tt.setFlags(cmd)
AddJSONFlagsWithoutShorthand(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.