Merge branch 'trunk' into gh-attestation-tuf-client-retry

This commit is contained in:
Meredith Lancaster 2025-05-28 10:24:32 -06:00
commit 23382f498e
13 changed files with 499 additions and 16 deletions

4
go.mod
View file

@ -44,13 +44,13 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
github.com/sigstore/protobuf-specs v0.4.1
github.com/sigstore/protobuf-specs v0.4.2
github.com/sigstore/sigstore-go v1.0.0
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/theupdateframework/go-tuf/v2 v2.1.1
github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark v1.7.12
github.com/zalando/go-keyring v0.2.5
golang.org/x/crypto v0.38.0
golang.org/x/sync v0.14.0

8
go.sum
View file

@ -455,8 +455,8 @@ github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJL
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/sigstore/protobuf-specs v0.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc=
github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
github.com/sigstore/protobuf-specs v0.4.2 h1:bD5bnhctpGNiR+FAEZl7N95XkN8TJFrNMIcWLunDtxA=
github.com/sigstore/protobuf-specs v0.4.2/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
github.com/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU=
github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A=
github.com/sigstore/sigstore v1.9.4 h1:64+OGed80+A4mRlNzRd055vFcgBeDghjZw24rPLZgDU=
@ -521,8 +521,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=

View file

@ -29,14 +29,6 @@ import (
"github.com/spf13/cobra"
)
// updaterEnabled is a statically linked build property set in gh formula within homebrew/homebrew-core
// used to control whether users are notified of newer GitHub CLI releases.
// This needs to be set to 'cli/cli' as it affects where update.CheckForUpdate() checks for releases.
// It is unclear whether this means that only homebrew builds will check for updates or not.
// Development builds leave this empty as impossible to determine if newer or not.
// For more information, <https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/gh.rb>.
var updaterEnabled = ""
type exitCode int
const (

View file

@ -0,0 +1,6 @@
//go:build !updateable
package ghcmd
// See update_enabled.go comment for more information.
var updaterEnabled = ""

View file

@ -0,0 +1,18 @@
//go:build updateable
package ghcmd
// `updateable` is a build tag set in the gh formula within homebrew/homebrew-core
// and is used to control whether users are notified of newer GitHub CLI releases.
//
// Currently, updaterEnabled needs to be set to 'cli/cli' as it affects where
// update.CheckForUpdate() checks for releases. It is unclear to what extent
// this updaterEnabled is being used by unofficial forks or builds, so we decided
// to leave it available for injection as a string variable for now.
//
// Development builds do not generate update messages by default.
//
// For more information, see:
// - the Homebrew formula for gh: <https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/gh.rb>.
// - a discussion about adding this build tag: <https://github.com/cli/cli/pull/11024#discussion_r2107597618>.
var updaterEnabled = "cli/cli"

View file

@ -236,6 +236,8 @@ func editRun(opts *EditOptions) error {
if filename == "" {
if len(candidates) == 1 {
filename = candidates[0]
} else if len(candidates) == 0 {
return errors.New("no file in the gist")
} else {
if !opts.IO.CanPrompt() {
return errors.New("unsure what file to edit; either specify --filename or run interactively")

View file

@ -557,6 +557,30 @@ func Test_editRun(t *testing.T) {
},
wantErr: "gist ID or URL required when not running interactively",
},
{
name: "edit no-file gist (#10626)",
opts: &EditOptions{
Selector: "1234",
},
mockGist: &shared.Gist{
ID: "1234",
Files: map[string]*shared.GistFile{},
Owner: &shared.GistOwner{Login: "octocat"},
},
wantErr: "no file in the gist",
},
{
name: "edit no-file gist, nil map (#10626)",
opts: &EditOptions{
Selector: "1234",
},
mockGist: &shared.Gist{
ID: "1234",
Files: nil,
Owner: &shared.GistOwner{Login: "octocat"},
},
wantErr: "no file in the gist",
},
}
for _, tt := range tests {

View file

@ -44,6 +44,9 @@ func (g Gist) Filename() string {
for fn := range g.Files {
filenames = append(filenames, fn)
}
if len(filenames) == 0 {
return ""
}
sort.Strings(filenames)
return filenames[0]
}

View file

@ -163,6 +163,33 @@ func TestPromptGists(t *testing.T) {
response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`,
wantOut: Gist{},
},
{
name: "prompt list contains no-file gist (#10626)",
prompterStubs: func(pm *prompter.MockPrompter) {
pm.RegisterSelect("Select a gist",
[]string{" about 6 hours ago", "gistfile0.txt about 6 hours ago"},
func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, " about 6 hours ago")
})
},
response: `{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234",
"files": [],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "5678",
"files": [{ "name": "gistfile0.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
}
] } } } }`,
wantOut: Gist{ID: "1234", Files: map[string]*GistFile{}, UpdatedAt: sixHoursAgo, Public: true},
},
}
ios, _, _, _ := iostreams.Test()

View file

@ -201,6 +201,8 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
Long: heredoc.Docf(`
Create a pull request on GitHub.
Upon success, the URL of the created pull request will be printed.
When the current branch isn't fully pushed to a git remote, a prompt will ask where
to push the branch and offer an option to fork the base repository. Use %[1]s--head%[1]s to
explicitly skip any forking or pushing behavior.

View file

@ -64,6 +64,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
# List PRs authored by you
$ gh pr list --author "@me"
# List PRs with a specific head branch name
$ gh pr list --head "typo"
# List only PRs with all of the given labels
$ gh pr list --label bug --label "priority 1"
@ -102,7 +105,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of items to fetch")
cmdutil.StringEnumFlag(cmd, &opts.State, "state", "s", "open", []string{"open", "closed", "merged", "all"}, "Filter by state")
cmd.Flags().StringVarP(&opts.BaseBranch, "base", "B", "", "Filter by base branch")
cmd.Flags().StringVarP(&opts.HeadBranch, "head", "H", "", "Filter by head branch")
cmd.Flags().StringVarP(&opts.HeadBranch, "head", "H", "", `Filter by head branch ("<owner>:<branch>" syntax not supported)`)
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by label")
cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author")
cmd.Flags().StringVar(&appAuthor, "app", "", "Filter by GitHub App author")

View file

@ -0,0 +1,398 @@
package shared
import (
"testing"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRenderJobs(t *testing.T) {
startedAt, err := time.Parse(time.RFC3339, "2009-03-19T00:00:00Z")
require.NoError(t, err)
completedAt, err := time.Parse(time.RFC3339, "2009-03-19T00:01:00Z")
require.NoError(t, err)
tests := []struct {
name string
jobs []Job
wantVerbose string
wantNormal string
wantCompact string
}{
{
name: "nil jobs",
jobs: nil,
},
{
name: "empty jobs",
jobs: []Job{},
},
{
// This is not a real-world case, but nevertheless the code should
// be able to handle that without error/panic.
name: "in-progress job without steps",
jobs: []Job{
{
Name: "foo",
ID: 999,
StartedAt: startedAt,
Status: InProgress,
},
},
wantCompact: heredoc.Doc(`
* foo (ID 999)`),
wantNormal: heredoc.Doc(`
* foo (ID 999)`),
wantVerbose: heredoc.Doc(`
* foo (ID 999)`),
},
{
// This is not a real-world case, but nevertheless the code should
// be able to handle that without error/panic.
name: "successful job without steps",
jobs: []Job{
{
Name: "foo",
ID: 999,
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
},
},
wantCompact: heredoc.Doc(`
foo in 1m0s (ID 999)`),
wantNormal: heredoc.Doc(`
foo in 1m0s (ID 999)`),
wantVerbose: heredoc.Doc(`
foo in 1m0s (ID 999)`),
},
{
// This is not a real-world case, but nevertheless the code should
// be able to handle that without error/panic.
name: "failed job without steps",
jobs: []Job{
{
Name: "foo",
ID: 999,
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Failure,
},
},
wantCompact: heredoc.Doc(`
X foo in 1m0s (ID 999)`),
wantNormal: heredoc.Doc(`
X foo in 1m0s (ID 999)`),
wantVerbose: heredoc.Doc(`
X foo in 1m0s (ID 999)`),
},
{
name: "in-progress job with various step status values",
jobs: []Job{
{
Name: "foo",
ID: 999,
StartedAt: startedAt,
Status: InProgress,
Steps: []Step{
{
Name: "passed",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "skipped",
Status: Completed,
Conclusion: Skipped,
Number: 2,
},
{
Name: "failed 1",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Failure,
Number: 3,
},
{
Name: "failed 2",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Failure,
Number: 4,
},
{
Name: "in-progress",
StartedAt: startedAt,
Status: InProgress,
Number: 5,
},
{
Name: "pending",
Status: Pending,
Number: 6,
},
},
},
},
wantCompact: heredoc.Doc(`
* foo (ID 999)
X failed 1
X failed 2
* in-progress`),
wantNormal: heredoc.Doc(`
* foo (ID 999)`),
wantVerbose: heredoc.Doc(`
* foo (ID 999)
passed
- skipped
X failed 1
X failed 2
* in-progress
* pending`),
},
{
// As of my observations (babakks) when there is a failed step, the
// job run is marked as failed. In other words, a successful job run
// cannot have any failed steps. That's why there's no failed steps
// in this test case.
name: "successful job with various step status values",
jobs: []Job{
{
Name: "foo",
ID: 999,
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Steps: []Step{
{
Name: "passed 1",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "skipped",
Status: Completed,
Conclusion: Skipped,
Number: 2,
},
{
Name: "passed 2",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Number: 3,
},
},
},
},
wantCompact: heredoc.Doc(`
foo in 1m0s (ID 999)`),
wantNormal: heredoc.Doc(`
foo in 1m0s (ID 999)`),
wantVerbose: heredoc.Doc(`
foo in 1m0s (ID 999)
passed 1
- skipped
passed 2`),
},
{
name: "failed job with various step status values",
jobs: []Job{
{
Name: "foo",
ID: 999,
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Failure,
Steps: []Step{
{
Name: "passed 1",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "skipped",
Status: Completed,
Conclusion: Skipped,
Number: 2,
},
{
Name: "failed 1",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Failure,
Number: 3,
},
{
Name: "failed 2",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Failure,
Number: 4,
},
{
Name: "passed 2",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Number: 5,
},
},
},
},
wantCompact: heredoc.Doc(`
X foo in 1m0s (ID 999)
X failed 1
X failed 2`),
wantNormal: heredoc.Doc(`
X foo in 1m0s (ID 999)
passed 1
- skipped
X failed 1
X failed 2
passed 2`),
wantVerbose: heredoc.Doc(`
X foo in 1m0s (ID 999)
passed 1
- skipped
X failed 1
X failed 2
passed 2`),
},
{
name: "multiple jobs",
jobs: []Job{
{
Name: "in-progress",
ID: 999,
StartedAt: startedAt,
Status: InProgress,
Steps: []Step{
{
Name: "passed",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "in-progress",
StartedAt: startedAt,
Status: InProgress,
Number: 2,
},
},
},
{
Name: "successful",
ID: 999,
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Steps: []Step{
{
Name: "passed",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "skipped",
Status: Completed,
Conclusion: Skipped,
Number: 2,
},
},
},
{
Name: "failed",
ID: 999,
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Failure,
Steps: []Step{
{
Name: "passed",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Success,
Number: 1,
},
{
Name: "failed",
StartedAt: startedAt,
CompletedAt: completedAt,
Status: Completed,
Conclusion: Failure,
Number: 2,
},
},
},
},
wantCompact: heredoc.Doc(`
* in-progress (ID 999)
* in-progress
successful in 1m0s (ID 999)
X failed in 1m0s (ID 999)
X failed`),
wantNormal: heredoc.Doc(`
* in-progress (ID 999)
successful in 1m0s (ID 999)
X failed in 1m0s (ID 999)
passed
X failed`),
wantVerbose: heredoc.Doc(`
* in-progress (ID 999)
passed
* in-progress
successful in 1m0s (ID 999)
passed
- skipped
X failed in 1m0s (ID 999)
passed
X failed`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotCompact := RenderJobsCompact(&iostreams.ColorScheme{}, tt.jobs)
assert.Equal(t, tt.wantCompact, gotCompact, "unexpected compact mode output")
gotNormal := RenderJobs(&iostreams.ColorScheme{}, tt.jobs, false)
assert.Equal(t, tt.wantNormal, gotNormal, "unexpected normal mode output")
gotVerbose := RenderJobs(&iostreams.ColorScheme{}, tt.jobs, true)
assert.Equal(t, tt.wantVerbose, gotVerbose, "unexpected verbose mode output")
})
}
}

View file

@ -53,7 +53,15 @@ var tasks = map[string]func(string) error{
ldflags = fmt.Sprintf("-X github.com/cli/cli/v2/internal/authflow.oauthClientID=%s %s", os.Getenv("GH_OAUTH_CLIENT_ID"), ldflags)
}
return run("go", "build", "-trimpath", "-ldflags", ldflags, "-o", exe, "./cmd/gh")
buildTags, _ := os.LookupEnv("GO_BUILDTAGS")
args := []string{"go", "build", "-trimpath"}
if buildTags != "" {
args = append(args, "-tags", buildTags)
}
args = append(args, "-ldflags", ldflags, "-o", exe, "./cmd/gh")
return run(args...)
},
"manpages": func(_ string) error {
return run("go", "run", "./cmd/gen-docs", "--man-page", "--doc-path", "./share/man/man1/")