From cc3dd515a6dad6a965007bd95f7fcad0ffd27d6d Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 28 Feb 2025 12:25:49 +0500 Subject: [PATCH 001/116] Show host name in repo creation prompts --- pkg/cmd/repo/create/create.go | 28 ++++++++++++++++------- pkg/cmd/repo/create/create_test.go | 36 +++++++++++++++--------------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index cd7c56ea81d..be0be2dddc2 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -262,10 +262,15 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co func createRun(opts *CreateOptions) error { if opts.Interactive { + cfg, err := opts.Config() + if err != nil { + return err + } + host, _ := cfg.Authentication().DefaultHost() answer, err := opts.Prompter.Select("What would you like to do?", "", []string{ - "Create a new repository on GitHub from scratch", - "Create a new repository on GitHub from a template repository", - "Push an existing local repository to GitHub", + fmt.Sprintf("Create a new repository on %s from scratch", host), + fmt.Sprintf("Create a new repository on %s from a template repository", host), + fmt.Sprintf("Push an existing local repository to %s", host), }) if err != nil { return err @@ -323,7 +328,9 @@ func createFromScratch(opts *CreateOptions) error { if idx := strings.IndexRune(opts.Name, '/'); idx > 0 { targetRepo = opts.Name[0:idx+1] + shared.NormalizeRepoName(opts.Name[idx+1:]) } - confirmed, err := opts.Prompter.Confirm(fmt.Sprintf(`This will create "%s" as a %s repository on GitHub. Continue?`, targetRepo, strings.ToLower(opts.Visibility)), true) + confirmed, err := opts.Prompter.Confirm( + fmt.Sprintf(`This will create "%s" as a %s repository on %s. Continue?`, targetRepo, strings.ToLower(opts.Visibility), host), + true) if err != nil { return err } else if !confirmed { @@ -392,9 +399,10 @@ func createFromScratch(opts *CreateOptions) error { isTTY := opts.IO.IsStdoutTTY() if isTTY { fmt.Fprintf(opts.IO.Out, - "%s Created repository %s on GitHub\n %s\n", + "%s Created repository %s on %s\n %s\n", cs.SuccessIconWithColor(cs.Green), ghrepo.FullName(repo), + host, repo.URL) } else { fmt.Fprintln(opts.IO.Out, repo.URL) @@ -482,7 +490,9 @@ func createFromTemplate(opts *CreateOptions) error { if idx := strings.IndexRune(opts.Name, '/'); idx > 0 { targetRepo = opts.Name[0:idx+1] + shared.NormalizeRepoName(opts.Name[idx+1:]) } - confirmed, err := opts.Prompter.Confirm(fmt.Sprintf(`This will create "%s" as a %s repository on GitHub. Continue?`, targetRepo, strings.ToLower(opts.Visibility)), true) + confirmed, err := opts.Prompter.Confirm( + fmt.Sprintf(`This will create "%s" as a %s repository on %s. Continue?`, targetRepo, strings.ToLower(opts.Visibility), host), + true) if err != nil { return err } else if !confirmed { @@ -496,9 +506,10 @@ func createFromTemplate(opts *CreateOptions) error { cs := opts.IO.ColorScheme() fmt.Fprintf(opts.IO.Out, - "%s Created repository %s on GitHub\n %s\n", + "%s Created repository %s on %s\n %s\n", cs.SuccessIconWithColor(cs.Green), ghrepo.FullName(repo), + host, repo.URL) opts.Clone, err = opts.Prompter.Confirm("Clone the new repository locally?", true) @@ -622,9 +633,10 @@ func createFromLocal(opts *CreateOptions) error { if isTTY { fmt.Fprintf(stdout, - "%s Created repository %s on GitHub\n %s\n", + "%s Created repository %s on %s\n %s\n", cs.SuccessIconWithColor(cs.Green), ghrepo.FullName(repo), + host, repo.URL) } else { fmt.Fprintln(stdout, repo.URL) diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index c33cfdad695..76078978673 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -201,7 +201,7 @@ func Test_createRun(t *testing.T) { name: "interactive create from scratch with gitignore and license", opts: &CreateOptions{Interactive: true}, tty: true, - wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n", + wantStdout: "✓ Created repository OWNER/REPO on github.com\n https://github.com/OWNER/REPO\n", promptStubs: func(p *prompter.PrompterMock) { p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) { switch message { @@ -211,7 +211,7 @@ func Test_createRun(t *testing.T) { return true, nil case "Would you like to add a license?": return true, nil - case `This will create "REPO" as a private repository on GitHub. Continue?`: + case `This will create "REPO" as a private repository on github.com. Continue?`: return defaultValue, nil case "Clone the new repository locally?": return defaultValue, nil @@ -232,7 +232,7 @@ func Test_createRun(t *testing.T) { p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { switch message { case "What would you like to do?": - return prompter.IndexFor(options, "Create a new repository on GitHub from scratch") + return prompter.IndexFor(options, "Create a new repository on github.com from scratch") case "Visibility": return prompter.IndexFor(options, "Private") case "Choose a license": @@ -267,7 +267,7 @@ func Test_createRun(t *testing.T) { name: "interactive create from scratch but with prompted owner", opts: &CreateOptions{Interactive: true}, tty: true, - wantStdout: "✓ Created repository org1/REPO on GitHub\n https://github.com/org1/REPO\n", + wantStdout: "✓ Created repository org1/REPO on github.com\n https://github.com/org1/REPO\n", promptStubs: func(p *prompter.PrompterMock) { p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) { switch message { @@ -277,7 +277,7 @@ func Test_createRun(t *testing.T) { return false, nil case "Would you like to add a license?": return false, nil - case `This will create "org1/REPO" as a private repository on GitHub. Continue?`: + case `This will create "org1/REPO" as a private repository on github.com. Continue?`: return true, nil case "Clone the new repository locally?": return false, nil @@ -300,7 +300,7 @@ func Test_createRun(t *testing.T) { case "Repository owner": return prompter.IndexFor(options, "org1") case "What would you like to do?": - return prompter.IndexFor(options, "Create a new repository on GitHub from scratch") + return prompter.IndexFor(options, "Create a new repository on github.com from scratch") case "Visibility": return prompter.IndexFor(options, "Private") default: @@ -345,7 +345,7 @@ func Test_createRun(t *testing.T) { return false, nil case "Would you like to add a license?": return false, nil - case `This will create "REPO" as a private repository on GitHub. Continue?`: + case `This will create "REPO" as a private repository on github.com. Continue?`: return false, nil default: return false, fmt.Errorf("unexpected confirm prompt: %s", message) @@ -364,7 +364,7 @@ func Test_createRun(t *testing.T) { p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { switch message { case "What would you like to do?": - return prompter.IndexFor(options, "Create a new repository on GitHub from scratch") + return prompter.IndexFor(options, "Create a new repository on github.com from scratch") case "Visibility": return prompter.IndexFor(options, "Private") default: @@ -409,7 +409,7 @@ func Test_createRun(t *testing.T) { p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { switch message { case "What would you like to do?": - return prompter.IndexFor(options, "Push an existing local repository to GitHub") + return prompter.IndexFor(options, "Push an existing local repository to github.com") case "Visibility": return prompter.IndexFor(options, "Private") default: @@ -441,7 +441,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") }, - wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n", + wantStdout: "✓ Created repository OWNER/REPO on github.com\n https://github.com/OWNER/REPO\n", }, { name: "interactive with existing bare repository public and push", @@ -475,7 +475,7 @@ func Test_createRun(t *testing.T) { p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { switch message { case "What would you like to do?": - return prompter.IndexFor(options, "Push an existing local repository to GitHub") + return prompter.IndexFor(options, "Push an existing local repository to github.com") case "Visibility": return prompter.IndexFor(options, "Private") default: @@ -509,7 +509,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") cs.Register(`git -C . push origin --mirror`, 0, "") }, - wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Mirrored all refs to https://github.com/OWNER/REPO.git\n", + wantStdout: "✓ Created repository OWNER/REPO on github.com\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Mirrored all refs to https://github.com/OWNER/REPO.git\n", }, { name: "interactive with existing repository public add remote and push", @@ -543,7 +543,7 @@ func Test_createRun(t *testing.T) { p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { switch message { case "What would you like to do?": - return prompter.IndexFor(options, "Push an existing local repository to GitHub") + return prompter.IndexFor(options, "Push an existing local repository to github.com") case "Visibility": return prompter.IndexFor(options, "Private") default: @@ -577,7 +577,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") cs.Register(`git -C . push --set-upstream origin HEAD`, 0, "") }, - wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n", + wantStdout: "✓ Created repository OWNER/REPO on github.com\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n", }, { name: "interactive create from a template repository", @@ -586,7 +586,7 @@ func Test_createRun(t *testing.T) { promptStubs: func(p *prompter.PrompterMock) { p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) { switch message { - case `This will create "OWNER/REPO" as a private repository on GitHub. Continue?`: + case `This will create "OWNER/REPO" as a private repository on github.com. Continue?`: return defaultValue, nil case "Clone the new repository locally?": return defaultValue, nil @@ -611,7 +611,7 @@ func Test_createRun(t *testing.T) { case "Choose a template repository": return prompter.IndexFor(options, "REPO") case "What would you like to do?": - return prompter.IndexFor(options, "Create a new repository on GitHub from a template repository") + return prompter.IndexFor(options, "Create a new repository on github.com from a template repository") case "Visibility": return prompter.IndexFor(options, "Private") default: @@ -654,7 +654,7 @@ func Test_createRun(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone --branch main https://github.com/OWNER/REPO`, 0, "") }, - wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n", + wantStdout: "✓ Created repository OWNER/REPO on github.com\n https://github.com/OWNER/REPO\n", }, { name: "interactive create from template repo but there are no template repos", @@ -680,7 +680,7 @@ func Test_createRun(t *testing.T) { p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { switch message { case "What would you like to do?": - return prompter.IndexFor(options, "Create a new repository on GitHub from a template repository") + return prompter.IndexFor(options, "Create a new repository on github.com from a template repository") case "Visibility": return prompter.IndexFor(options, "Private") default: From a1136bf5cd51d0bd8fd99ca639f1096e2ee9af2e Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Sun, 9 Mar 2025 12:19:16 +0500 Subject: [PATCH 002/116] Add initial test --- pkg/cmd/repo/create/create_test.go | 90 +++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index 76078978673..5f1f17e604b 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -10,6 +10,7 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" + ghmock "github.com/cli/cli/v2/internal/gh/mock" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/run" "github.com/cli/cli/v2/pkg/cmdutil" @@ -950,6 +951,88 @@ func Test_createRun(t *testing.T) { }, wantStdout: "https://github.com/OWNER/REPO\n", }, + { + name: "interactive create from scratch with host override", + opts: &CreateOptions{ + Interactive: true, + Config: func() (gh.Config, error) { + cfg := &ghmock.ConfigMock{ + AuthenticationFunc: func() gh.AuthConfig { + authCfg := &config.AuthConfig{} + authCfg.SetHosts([]string{"example.com"}) + authCfg.SetDefaultHost("example.com", "GH_HOST") + return authCfg + }, + } + return cfg, nil + }, + }, + tty: true, + promptStubs: func(p *prompter.PrompterMock) { + p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) { + switch message { + case "Would you like to add a README file?": + return false, nil + case "Would you like to add a .gitignore?": + return false, nil + case "Would you like to add a license?": + return false, nil + case `This will create "REPO" as a private repository on example.com. Continue?`: + return defaultValue, nil + case "Clone the new repository locally?": + return false, nil + default: + return false, fmt.Errorf("unexpected confirm prompt: %s", message) + } + } + p.InputFunc = func(message, defaultValue string) (string, error) { + switch message { + case "Repository name": + return "REPO", nil + case "Description": + return "my new repo", nil + default: + return "", fmt.Errorf("unexpected input prompt: %s", message) + } + } + p.SelectFunc = func(message, defaultValue string, options []string) (int, error) { + switch message { + case "What would you like to do?": + return prompter.IndexFor(options, "Create a new repository on example.com from scratch") + case "Visibility": + return prompter.IndexFor(options, "Private") + case "Choose a license": + return prompter.IndexFor(options, "GNU Lesser General Public License v3.0") + case "Choose a .gitignore template": + return prompter.IndexFor(options, "Go") + default: + return 0, fmt.Errorf("unexpected select prompt: %s", message) + } + } + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`)) + reg.Register( + httpmock.GraphQL(`mutation RepositoryCreate\b`), + httpmock.StringResponse(` + { + "data": { + "createRepository": { + "repository": { + "id": "REPOID", + "name": "REPO", + "owner": {"login":"OWNER"}, + "url": "https://example.com/OWNER/REPO" + } + } + } + }`), + ) + }, + wantStdout: "✓ Created repository OWNER/REPO on example.com\n https://example.com/OWNER/REPO\n", + }, } for _, tt := range tests { prompterMock := &prompter.PrompterMock{} @@ -965,8 +1048,11 @@ func Test_createRun(t *testing.T) { tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } - tt.opts.Config = func() (gh.Config, error) { - return config.NewBlankConfig(), nil + + if tt.opts.Config == nil { + tt.opts.Config = func() (gh.Config, error) { + return config.NewBlankConfig(), nil + } } tt.opts.GitClient = &git.Client{ From f3e4976da39badaf2f07cc86f7041c88841b5b98 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 14 Mar 2025 11:51:09 +0500 Subject: [PATCH 003/116] `./script/sign` cleanup --- .goreleaser.yml | 2 +- docs/release-process-deep-dive.md | 3 --- script/sign | 33 +++---------------------------- 3 files changed, 4 insertions(+), 34 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index a7b293d6e56..9584f74036a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -40,7 +40,7 @@ builds: hooks: post: - cmd: >- - {{ if eq .Runtime.Goos "windows" }}pwsh .\script\sign.ps1{{ else }}./script/sign{{ end }} '{{ .Path }}' + {{ if eq .Runtime.Goos "windows" }}pwsh .\script\sign.ps1{{ end }} '{{ .Path }}' output: true binary: bin/gh main: ./cmd/gh diff --git a/docs/release-process-deep-dive.md b/docs/release-process-deep-dive.md index ed9362d38ae..31f44f6efd2 100644 --- a/docs/release-process-deep-dive.md +++ b/docs/release-process-deep-dive.md @@ -428,9 +428,6 @@ Breaking this command down: * `/dlib` points to the previously extracted DLL * `/dmdf` points to the previously created metadata file -> [!WARNING] -> The [`GoReleaser` signing hook](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.goreleaser.yml#L43) can currently call `./script/sign` on a non-windows machine, but this is an artifact from pre-HSM that should be removed. - ## [release](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L250-L395)
diff --git a/script/sign b/script/sign index 1630a06b53a..0c2a4602f49 100755 --- a/script/sign +++ b/script/sign @@ -1,34 +1,10 @@ #!/bin/bash # usage: script/sign # -# Signs macOS binaries using codesign, notarizes macOS zip archives using notarytool, and signs -# Windows EXE and MSI files using osslsigncode. +# Signs macOS binaries using codesign, notarizes macOS zip archives using notarytool # set -e -sign_windows() { - if [ -z "$CERT_FILE" ]; then - echo "skipping Windows code-signing; CERT_FILE not set" >&2 - return 0 - fi - - if [ ! -f "$CERT_FILE" ]; then - echo "error Windows code-signing; file '$CERT_FILE' not found" >&2 - return 1 - fi - - if [ -z "$CERT_PASSWORD" ]; then - echo "error Windows code-signing; no value for CERT_PASSWORD" >&2 - return 1 - fi - - osslsigncode sign -n "GitHub CLI" -t http://timestamp.digicert.com \ - -pkcs12 "$CERT_FILE" -readpass <(printf "%s" "$CERT_PASSWORD") -h sha256 \ - -in "$1" -out "$1"~ - - mv "$1"~ "$1" -} - sign_macos() { if [ -z "$APPLE_DEVELOPER_ID" ]; then echo "skipping macOS code-signing; APPLE_DEVELOPER_ID not set" >&2 @@ -51,10 +27,7 @@ platform="$(uname -s)" for input_file; do case "$input_file" in - *.exe | *.msi ) - sign_windows "$input_file" - ;; - * ) + *) if [ "$platform" = "Darwin" ]; then sign_macos "$input_file" else @@ -62,4 +35,4 @@ for input_file; do fi ;; esac -done \ No newline at end of file +done From 342e3cd70c1896c71c4196d7f6fc225a27314c12 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 14 Mar 2025 12:17:24 +0500 Subject: [PATCH 004/116] More cleanup --- .goreleaser.yml | 3 +-- script/sign | 18 +++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 9584f74036a..6ef1ecc8b52 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -39,8 +39,7 @@ builds: goarch: [386, amd64, arm64] hooks: post: - - cmd: >- - {{ if eq .Runtime.Goos "windows" }}pwsh .\script\sign.ps1{{ end }} '{{ .Path }}' + - cmd: pwsh .\script\sign.ps1 '{{ .Path }}' output: true binary: bin/gh main: ./cmd/gh diff --git a/script/sign b/script/sign index 0c2a4602f49..f07a7d2d46e 100755 --- a/script/sign +++ b/script/sign @@ -6,7 +6,7 @@ set -e sign_macos() { - if [ -z "$APPLE_DEVELOPER_ID" ]; then + if [[ -z "$APPLE_DEVELOPER_ID" ]]; then echo "skipping macOS code-signing; APPLE_DEVELOPER_ID not set" >&2 return 0 fi @@ -18,21 +18,17 @@ sign_macos() { fi } -if [ $# -eq 0 ]; then +if [[ $# -eq 0 ]]; then echo "usage: script/sign " >&2 exit 1 fi platform="$(uname -s)" +if [[ $platform != "Darwin" ]]; then + echo "error: must run on macOS; skipping codesigning/notarization" >&2 + exit 1 +fi for input_file; do - case "$input_file" in - *) - if [ "$platform" = "Darwin" ]; then - sign_macos "$input_file" - else - printf "warning: don't know how to sign %s on %s\n" "$1", "$platform" >&2 - fi - ;; - esac + sign_macos "$input_file" done From e02ee18ed2b23bd8c8a3141e622e953f5723368b Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Sat, 15 Mar 2025 17:55:29 +0500 Subject: [PATCH 005/116] [gh ext] Fix `GitKind` extension directory path --- pkg/cmd/extension/extension.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go index dc7ffa8c3a3..f30bf63c15c 100644 --- a/pkg/cmd/extension/extension.go +++ b/pkg/cmd/extension/extension.go @@ -163,7 +163,8 @@ func (e *Extension) IsPinned() bool { isPinned = manifest.IsPinned } case GitKind: - pinPath := filepath.Join(e.Path(), fmt.Sprintf(".pin-%s", e.CurrentVersion())) + extDir := filepath.Dir(e.path) + pinPath := filepath.Join(extDir, fmt.Sprintf(".pin-%s", e.CurrentVersion())) if _, err := os.Stat(pinPath); err == nil { isPinned = true } else { From 6355e54e3cc1becd3a454ea5f0508f27f64b849a Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 21 Mar 2025 11:51:03 -0400 Subject: [PATCH 006/116] Ensure table headers are thematically contrasting This commit refactors the color format around table headers to ensure the GitHub CLI uses thematically appropriate colors based on dark background, light background, or no color at all. In order to do so, `ColorScheme` needs information from the terminal about the background appearance (dark, light, none) to determine appropriate muted color. --- internal/tableprinter/table_printer.go | 2 +- pkg/cmd/gist/list/list_test.go | 2 +- pkg/iostreams/color.go | 61 +++++++++++++++++--------- pkg/iostreams/color_test.go | 16 +++---- pkg/iostreams/iostreams.go | 2 +- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/internal/tableprinter/table_printer.go b/internal/tableprinter/table_printer.go index e1454170f71..69b22be122c 100644 --- a/internal/tableprinter/table_printer.go +++ b/internal/tableprinter/table_printer.go @@ -80,7 +80,7 @@ func NewWithWriter(w io.Writer, isTTY bool, maxWidth int, cs *iostreams.ColorSch tp.AddHeader( upperCasedHeaders, WithPadding(paddingFunc), - WithColor(cs.LightGrayUnderline), + WithColor(cs.TableHeader), ) } diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 0acfae109d5..870f58147fc 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -694,7 +694,7 @@ func Test_highlightMatch(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cs := iostreams.NewColorScheme(tt.color, false, false) + cs := iostreams.NewColorScheme(tt.color, false, false, iostreams.NoTheme) matched := false got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index c8d48168fa4..f97d1f93e94 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -9,34 +9,44 @@ import ( ) const ( + NoTheme = "none" + DarkTheme = "dark" + LightTheme = "light" highlightStyle = "black:yellow" ) +// Special cases like darkTableHeader / lightTableHeader are necessary when using color and modifiers +// (bold, underline, dim) because ansi.ColorFunc requires a foreground color and resets formats. var ( - magenta = ansi.ColorFunc("magenta") - cyan = ansi.ColorFunc("cyan") - red = ansi.ColorFunc("red") - yellow = ansi.ColorFunc("yellow") - blue = ansi.ColorFunc("blue") - green = ansi.ColorFunc("green") - gray = ansi.ColorFunc("black+h") - lightGrayUnderline = ansi.ColorFunc("white+du") - bold = ansi.ColorFunc("default+b") - cyanBold = ansi.ColorFunc("cyan+b") - greenBold = ansi.ColorFunc("green+b") - highlightStart = ansi.ColorCode(highlightStyle) - highlight = ansi.ColorFunc(highlightStyle) + magenta = ansi.ColorFunc("magenta") + cyan = ansi.ColorFunc("cyan") + red = ansi.ColorFunc("red") + yellow = ansi.ColorFunc("yellow") + blue = ansi.ColorFunc("blue") + green = ansi.ColorFunc("green") + gray = ansi.ColorFunc("black+h") + bold = ansi.ColorFunc("default+b") + cyanBold = ansi.ColorFunc("cyan+b") + greenBold = ansi.ColorFunc("green+b") + highlightStart = ansi.ColorCode(highlightStyle) + highlight = ansi.ColorFunc(highlightStyle) + darkTableHeader = ansi.ColorFunc("white+du") + lightTableHeader = ansi.ColorFunc("black+hu") gray256 = func(t string) string { return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t) } ) -func NewColorScheme(enabled, is256enabled bool, trueColor bool) *ColorScheme { +// NewColorScheme initializes color logic based on provided terminal capabilities. +// Logic dealing with terminal theme detected, such as whether color is enabled, 8-bit color supported, true color supported, +// and terminal theme detected. +func NewColorScheme(enabled, is256enabled, trueColor bool, theme string) *ColorScheme { return &ColorScheme{ enabled: enabled, is256enabled: is256enabled, hasTrueColor: trueColor, + theme: theme, } } @@ -44,6 +54,7 @@ type ColorScheme struct { enabled bool is256enabled bool hasTrueColor bool + theme string } func (c *ColorScheme) Enabled() bool { @@ -115,13 +126,6 @@ func (c *ColorScheme) Grayf(t string, args ...interface{}) string { return c.Gray(fmt.Sprintf(t, args...)) } -func (c *ColorScheme) LightGrayUnderline(t string) string { - if !c.enabled { - return t - } - return lightGrayUnderline(t) -} - func (c *ColorScheme) Magenta(t string) string { if !c.enabled { return t @@ -254,3 +258,18 @@ func (c *ColorScheme) HexToRGB(hex string, x string) string { b, _ := strconv.ParseInt(hex[4:6], 16, 64) return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x) } + +func (c *ColorScheme) TableHeader(t string) string { + if !c.enabled { + return t + } + + switch c.theme { + case DarkTheme: + return darkTableHeader(t) + case LightTheme: + return lightTableHeader(t) + default: + return t + } +} diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index 59fea53ed1e..9c84f72e108 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -19,28 +19,28 @@ func TestColorFromRGB(t *testing.T) { hex: "fc0303", text: "red", wants: "\033[38;2;252;3;3mred\033[0m", - cs: NewColorScheme(true, true, true), + cs: NewColorScheme(true, true, true, NoTheme), }, { name: "no truecolor", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, false), + cs: NewColorScheme(true, true, false, NoTheme), }, { name: "no color", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(false, false, false), + cs: NewColorScheme(false, false, false, NoTheme), }, { name: "invalid hex", hex: "fc0", text: "red", wants: "red", - cs: NewColorScheme(false, false, false), + cs: NewColorScheme(false, false, false, NoTheme), }, } @@ -63,28 +63,28 @@ func TestHexToRGB(t *testing.T) { hex: "fc0303", text: "red", wants: "\033[38;2;252;3;3mred\033[0m", - cs: NewColorScheme(true, true, true), + cs: NewColorScheme(true, true, true, NoTheme), }, { name: "no truecolor", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, false), + cs: NewColorScheme(true, true, false, NoTheme), }, { name: "no color", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(false, false, false), + cs: NewColorScheme(false, false, false, NoTheme), }, { name: "invalid hex", hex: "fc0", text: "red", wants: "red", - cs: NewColorScheme(false, false, false), + cs: NewColorScheme(false, false, false, NoTheme), }, } diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 2bc712a3cd4..6c12d7911a6 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -366,7 +366,7 @@ func (s *IOStreams) TerminalWidth() int { } func (s *IOStreams) ColorScheme() *ColorScheme { - return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor()) + return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.TerminalTheme()) } func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) { From 736ce69f66c69a974dd5b489ea918b3da5a4836a Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 21 Mar 2025 12:27:09 -0400 Subject: [PATCH 007/116] Underline table headers if colors enabled but no theme This enhances the table header stylizing logic to ensure they are underlined if color is enabled but no theme is desired. --- pkg/iostreams/color.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index f97d1f93e94..e2841d03048 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -32,6 +32,7 @@ var ( highlight = ansi.ColorFunc(highlightStyle) darkTableHeader = ansi.ColorFunc("white+du") lightTableHeader = ansi.ColorFunc("black+hu") + noneTableHeader = ansi.ColorFunc("default+u") gray256 = func(t string) string { return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t) @@ -260,6 +261,7 @@ func (c *ColorScheme) HexToRGB(hex string, x string) string { } func (c *ColorScheme) TableHeader(t string) string { + // Table headers are only stylized if color is enabled including underline modifier. if !c.enabled { return t } @@ -270,6 +272,6 @@ func (c *ColorScheme) TableHeader(t string) string { case LightTheme: return lightTableHeader(t) default: - return t + return noneTableHeader(t) } } From c7cd041a4057e82ad859dd8495ecbf2e97705474 Mon Sep 17 00:00:00 2001 From: Ryan Winograd Date: Sat, 22 Mar 2025 15:21:24 -0500 Subject: [PATCH 008/116] Fix typos in CONTRIBUTING.md --- .github/CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4e2032a875c..a1ed27d990a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -62,9 +62,9 @@ To propose a design: - Include a link to the issue that the design is for. - Describe the design you are proposing to resolve the issue, leveraging the [CLI Design System][]. - Mock up the design you are proposing using our [Google Docs Template][] or code blocks. - - Mock ups should cleary illustrate the command(s) being run and the expected output(s). + - Mock ups should clearly illustrate the command(s) being run and the expected output(s). -### (core team only) Revewing a design +### (core team only) Reviewing a design A member of the core team will [triage](../docs/triage.md) the design proposal. Once a member of the core team has reviewed the design, they may add the [`help wanted`][hw] label to the issue, so a PR can be opened to provide the implementation. From 5817f6fce92b36b935c8d7653e1abe51f9cae17d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:34:50 -0600 Subject: [PATCH 009/116] fix(run list): do not fail on org workflows --- pkg/cmd/run/list/list_test.go | 38 +++++++++++++++++++++++++++++++++++ pkg/cmd/run/shared/shared.go | 10 +++++++++ pkg/cmd/run/shared/test.go | 16 +++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/pkg/cmd/run/list/list_test.go b/pkg/cmd/run/list/list_test.go index 7717c2ff98f..e41809bf19e 100644 --- a/pkg/cmd/run/list/list_test.go +++ b/pkg/cmd/run/list/list_test.go @@ -366,6 +366,44 @@ func TestListRun(t *testing.T) { completed stale cool commit CI trunk push 10 4m34s 2021-02-23T04:51:00Z `), }, + { + name: "organization required workflow in run list (workflow GET returns 404)", + opts: &ListOptions{ + Limit: defaultLimit, + now: shared.TestRunStartTime.Add(time.Minute*4 + time.Second*34), + }, + isTTY: true, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRunsWithOrgRequiredWorkflows, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/456"), + httpmock.StatusStringResponse(404, "not found"), + ) + }, + wantOut: heredoc.Doc(` + STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE + X cool commit trunk push 1 4m34s about 4 minutes ago + * cool commit trunk push 2 4m34s about 4 minutes ago + ✓ cool commit trunk push 3 4m34s about 4 minutes ago + X cool commit trunk push 4 4m34s about 4 minutes ago + X cool commit CI trunk push 5 4m34s about 4 minutes ago + - cool commit CI trunk push 6 4m34s about 4 minutes ago + - cool commit CI trunk push 7 4m34s about 4 minutes ago + * cool commit CI trunk push 8 4m34s about 4 minutes ago + * cool commit CI trunk push 9 4m34s about 4 minutes ago + `), + }, { name: "pagination", opts: &ListOptions{ diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index 040888ab5ff..b35f0c4627a 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -446,6 +446,16 @@ func preloadWorkflowNames(client *api.Client, repo ghrepo.Interface, runs []Run) if _, ok := workflowMap[run.WorkflowID]; !ok { // Look up workflow by ID because it may have been deleted workflow, err := workflowShared.GetWorkflow(client, repo, run.WorkflowID) + // If the error is an httpError and it is a 404, this is likely a + // organization-level "required workflow" ruleset. The user does not + // have permissions to view the details of the workflow, so we cannot + // look it up directly without receiving a 404, but it is nonetheless + // in the workflow run list. To handle this, we set the workflow name + // to an empty string. + if httpErr, ok := err.(api.HTTPError); ok && httpErr.StatusCode == 404 { + workflowMap[run.WorkflowID] = "" + continue + } if err != nil { return err } diff --git a/pkg/cmd/run/shared/test.go b/pkg/cmd/run/shared/test.go index 7caa37039af..0619541a452 100644 --- a/pkg/cmd/run/shared/test.go +++ b/pkg/cmd/run/shared/test.go @@ -18,6 +18,10 @@ func TestRunWithCommit(id int64, s Status, c Conclusion, commit string) Run { return TestRunWithWorkflowAndCommit(123, id, s, c, commit) } +func TestRunWithOrgRequiredWorkflow(id int64, s Status, c Conclusion, commit string) Run { + return TestRunWithWorkflowAndCommit(456, id, s, c, commit) +} + func TestRunWithWorkflowAndCommit(workflowId, runId int64, s Status, c Conclusion, commit string) Run { return Run{ WorkflowID: workflowId, @@ -57,6 +61,18 @@ var TestRuns []Run = []Run{ TestRun(10, Completed, Stale), } +var TestRunsWithOrgRequiredWorkflows []Run = []Run{ + TestRunWithOrgRequiredWorkflow(1, Completed, TimedOut, "cool commit"), + TestRunWithOrgRequiredWorkflow(2, InProgress, "", "cool commit"), + TestRunWithOrgRequiredWorkflow(3, Completed, Success, "cool commit"), + TestRunWithOrgRequiredWorkflow(4, Completed, Cancelled, "cool commit"), + TestRun(5, Completed, Failure), + TestRun(6, Completed, Neutral), + TestRun(7, Completed, Skipped), + TestRun(8, Requested, ""), + TestRun(9, Queued, ""), +} + var WorkflowRuns []Run = []Run{ TestRun(2, InProgress, ""), SuccessfulRun, From d3ad4f410b97a1a01ddefe9c3ead5f41c0808e52 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:45:03 -0600 Subject: [PATCH 010/116] docs(run list): doc runs without workflow names --- pkg/cmd/run/list/list.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index 3113fdabd72..adf0aeeb5cf 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -62,6 +62,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Note that providing the %[1]sworkflow_name%[1]s to the %[1]s-w%[1]s flag will not fetch disabled workflows. Also pass the %[1]s-a%[1]s flag to fetch disabled workflow runs using the %[1]sworkflow_name%[1]s and the %[1]s-w%[1]s flag. + + A run with no workflow name indicates that the run likely belongs an organization ruleset required workflow, + and the authenticated user does not have access to the workflow definition. `, "`"), Aliases: []string{"ls"}, Args: cobra.NoArgs, From a35ae3b8bb5648d7908abfe89deab318abc5e5af Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:54:47 -0600 Subject: [PATCH 011/116] refactor(tests): update test name for org workflows --- pkg/cmd/run/list/list_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/run/list/list_test.go b/pkg/cmd/run/list/list_test.go index e41809bf19e..1215bddc956 100644 --- a/pkg/cmd/run/list/list_test.go +++ b/pkg/cmd/run/list/list_test.go @@ -367,7 +367,7 @@ func TestListRun(t *testing.T) { `), }, { - name: "organization required workflow in run list (workflow GET returns 404)", + name: "org required workflow in runs list shows with empty workflow name", opts: &ListOptions{ Limit: defaultLimit, now: shared.TestRunStartTime.Add(time.Minute*4 + time.Second*34), From 30960d59bc011936482128d58ebb18c4aaf95980 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:04:16 -0600 Subject: [PATCH 012/116] doc(run list): add enterprise ruleset notes Co-authored-by: Andy Feller --- pkg/cmd/run/list/list.go | 3 +-- pkg/cmd/run/shared/shared.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index adf0aeeb5cf..7b18c391d50 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -63,8 +63,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Note that providing the %[1]sworkflow_name%[1]s to the %[1]s-w%[1]s flag will not fetch disabled workflows. Also pass the %[1]s-a%[1]s flag to fetch disabled workflow runs using the %[1]sworkflow_name%[1]s and the %[1]s-w%[1]s flag. - A run with no workflow name indicates that the run likely belongs an organization ruleset required workflow, - and the authenticated user does not have access to the workflow definition. + Runs created by organization and enterprise ruleset workflows will not display a workflow name due to GitHub API limitations. `, "`"), Aliases: []string{"ls"}, Args: cobra.NoArgs, diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index b35f0c4627a..248b5c5a0fd 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -447,7 +447,7 @@ func preloadWorkflowNames(client *api.Client, repo ghrepo.Interface, runs []Run) // Look up workflow by ID because it may have been deleted workflow, err := workflowShared.GetWorkflow(client, repo, run.WorkflowID) // If the error is an httpError and it is a 404, this is likely a - // organization-level "required workflow" ruleset. The user does not + // organization or enterprise ruleset workflow. The user does not // have permissions to view the details of the workflow, so we cannot // look it up directly without receiving a 404, but it is nonetheless // in the workflow run list. To handle this, we set the workflow name From c6f574ccb19fa4d8819c3554e806eb07f87a7ebe Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:14:50 -0600 Subject: [PATCH 013/116] test(run list): update rulesets test name Co-authored-by: Andy Feller --- pkg/cmd/run/list/list_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/run/list/list_test.go b/pkg/cmd/run/list/list_test.go index 1215bddc956..0f11e492a79 100644 --- a/pkg/cmd/run/list/list_test.go +++ b/pkg/cmd/run/list/list_test.go @@ -367,7 +367,7 @@ func TestListRun(t *testing.T) { `), }, { - name: "org required workflow in runs list shows with empty workflow name", + name: "org ruleset workflow in runs list shows with empty workflow name", opts: &ListOptions{ Limit: defaultLimit, now: shared.TestRunStartTime.Add(time.Minute*4 + time.Second*34), From 79f1b07fb1bc4b144098ae570008662e429f1990 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Tue, 25 Mar 2025 15:29:05 -0400 Subject: [PATCH 014/116] Implement and fix tests for table headers --- pkg/cmd/gist/list/list_test.go | 2 +- pkg/iostreams/color_test.go | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 870f58147fc..4f6c8a9f7e2 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -486,7 +486,7 @@ func Test_listRun(t *testing.T) { ) }, wantOut: heredoc.Docf(` - %[1]s[0;2;4;37mID %[1]s[0m %[1]s[0;2;4;37mDESCRIPTION %[1]s[0m %[1]s[0;2;4;37mFILES %[1]s[0m %[1]s[0;2;4;37mVISIBILITY%[1]s[0m %[1]s[0;2;4;37mUPDATED %[1]s[0m + %[1]s[0;4;39mID %[1]s[0m %[1]s[0;4;39mDESCRIPTION %[1]s[0m %[1]s[0;4;39mFILES %[1]s[0m %[1]s[0;4;39mVISIBILITY%[1]s[0m %[1]s[0;4;39mUPDATED %[1]s[0m 1234 %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m 1 file %[1]s[0;32mpublic %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[m 2345 %[1]s[0;1;39mmatch in the file name %[1]s[0m %[1]s[0;30;43m2 files%[1]s[0m %[1]s[0;31msecret %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[m `, "\x1b"), diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index 9c84f72e108..b35c2eb73a8 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -1,6 +1,7 @@ package iostreams import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -93,3 +94,84 @@ func TestHexToRGB(t *testing.T) { assert.Equal(t, tt.wants, output) } } + +func TestTableHeader(t *testing.T) { + reset := "\x1b[0m" + defaultUnderline := "\x1b[0;4;39m" + brightBlackUnderline := "\x1b[0;4;90m" + dimBlackUnderline := "\x1b[0;2;4;37m" + + tests := []struct { + name string + cs *ColorScheme + input string + expected string + }{ + { + name: "when color is disabled, text is not stylized", + cs: NewColorScheme(false, false, false, NoTheme), + input: "this should not be stylized", + expected: "this should not be stylized", + }, + { + name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: NewColorScheme(true, false, false, NoTheme), + input: "this should have no explicit color but underlined", + expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), + }, + { + name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: NewColorScheme(true, false, false, LightTheme), + input: "this should have dark foreground color and underlined", + expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), + }, + { + name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used", + cs: NewColorScheme(true, false, false, DarkTheme), + input: "this should have light foreground color and underlined", + expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), + }, + { + name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: NewColorScheme(true, true, false, NoTheme), + input: "this should have no explicit color but underlined", + expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), + }, + { + name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: NewColorScheme(true, true, false, LightTheme), + input: "this should have dark foreground color and underlined", + expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), + }, + { + name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used", + cs: NewColorScheme(true, true, false, DarkTheme), + input: "this should have light foreground color and underlined", + expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), + }, + { + name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: NewColorScheme(true, true, true, NoTheme), + input: "this should have no explicit color but underlined", + expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), + }, + { + name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: NewColorScheme(true, true, true, LightTheme), + input: "this should have dark foreground color and underlined", + expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), + }, + { + name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used", + cs: NewColorScheme(true, true, true, DarkTheme), + input: "this should have light foreground color and underlined", + expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.cs.TableHeader(tt.input)) + }) + } +} From efa1825eff655c0ae38ec2767940c20aa1137969 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:21:22 -0600 Subject: [PATCH 015/116] chore: update go-gh to v2.12.0 --- go.mod | 36 ++++++++++++++++++--------- go.sum | 78 ++++++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 76 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index 433156789c0..bea712a2d54 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,9 @@ require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/briandowns/spinner v1.18.1 github.com/cenkalti/backoff/v4 v4.3.0 - github.com/charmbracelet/glamour v0.8.0 - github.com/charmbracelet/lipgloss v0.12.1 - github.com/cli/go-gh/v2 v2.11.2 + github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 + github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc + github.com/cli/go-gh/v2 v2.12.0 github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 github.com/cli/oauth v1.1.1 github.com/cli/safeexec v1.0.1 @@ -47,9 +47,9 @@ require ( github.com/stretchr/testify v1.10.0 github.com/zalando/go-keyring v0.2.5 golang.org/x/crypto v0.35.0 - golang.org/x/sync v0.11.0 - golang.org/x/term v0.29.0 - golang.org/x/text v0.22.0 + golang.org/x/sync v0.12.0 + golang.org/x/term v0.30.0 + golang.org/x/text v0.23.0 google.golang.org/grpc v1.69.4 google.golang.org/protobuf v1.36.5 gopkg.in/h2non/gock.v1 v1.1.2 @@ -57,13 +57,20 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/alessio/shellescape v1.4.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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 - github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cli/browser v1.3.0 // indirect github.com/cli/shurcooL-graphql v0.0.4 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect @@ -99,6 +106,7 @@ require ( github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // 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 github.com/itchyny/gojq v0.12.15 // indirect @@ -110,12 +118,14 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect @@ -130,6 +140,7 @@ require ( github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect 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.3.8 // indirect github.com/sigstore/sigstore v1.8.12 // indirect @@ -147,8 +158,9 @@ require ( github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/vbatts/tar-split v0.11.6 // indirect - github.com/yuin/goldmark v1.7.4 // indirect - github.com/yuin/goldmark-emoji v1.0.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect @@ -159,7 +171,7 @@ require ( golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.36.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/tools v0.29.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect diff --git a/go.sum b/go.sum index b5db03d9c9b..2b5a3121206 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ cloud.google.com/go/kms v1.20.4 h1:CJ0hMpOg1ANN9tx/a/GPJ+Uxudy8k6f3fvGFuTHiE5A= cloud.google.com/go/kms v1.20.4/go.mod h1:gPLsp1r4FblUgBYPOcvI/bUPpdMg2Jm1ZVKU4tQUfcc= cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= @@ -33,6 +35,12 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8het github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= @@ -91,19 +99,25 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= -github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= -github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= -github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= -github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= -github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4 h1:6KzMkQeAF56rggw2NZu1L+TH7j9+DM1/2Kmh7KUxg1I= -github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25SvI2WiZdt/gxINcYBnHD7PE2Vr9auqwg5B05g= +github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s= +github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys= +github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/cli/go-gh/v2 v2.11.2 h1:oad1+sESTPNTiTvh3I3t8UmxuovNDxhwLzeMHk45Q9w= -github.com/cli/go-gh/v2 v2.11.2/go.mod h1:vVFhi3TfjseIW26ED9itAR8gQK0aVThTm8sYrsZ5QTI= +github.com/cli/go-gh/v2 v2.12.0 h1:PIurZ13fXbWDbr2//6ws4g4zDbryO+iDuTpiHgiV+6k= +github.com/cli/go-gh/v2 v2.12.0/go.mod h1:+5aXmEOJsH9fc9mBHfincDwnS02j2AIA/DsTH0Bk5uw= github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 h1:QDrhR4JA2n3ij9YQN0u5ZeuvRIIvsUGmf5yPlTS0w8E= github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24/go.mod h1:rr9GNING0onuVw8MnracQHn7PcchnFlP882Y0II2KZk= github.com/cli/oauth v1.1.1 h1:459gD3hSjlKX9B1uXBuiAMdpXBUQ9QGf/NDcCpoQxPs= @@ -266,6 +280,8 @@ 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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/in-toto/attestation v1.1.1 h1:QD3d+oATQ0dFsWoNh5oT0udQ3tUrOsZZ0Fc3tSgWbzI= github.com/in-toto/attestation v1.1.1/go.mod h1:Dcq1zVwA2V7Qin8I7rgOi+i837wEf/mOZwRm047Sjys= github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= @@ -308,6 +324,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ= github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -325,8 +343,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -334,14 +352,18 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/microsoft/dev-tunnels v0.0.25 h1:UlMKUI+2O8cSu4RlB52ioSyn1LthYSVkJA+CSTsdKoA= github.com/microsoft/dev-tunnels v0.0.25/go.mod h1:frU++12T/oqxckXkDpTuYa427ncguEOodSPZcGCCrzQ= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= -github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo= github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -401,6 +423,8 @@ 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= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJLvBliNGfcQgUmhlniWBDXC79oRxfZA0= 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= @@ -466,12 +490,14 @@ github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +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.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= -github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= -github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +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-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= github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= @@ -518,8 +544,8 @@ golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -529,19 +555,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From ffd42ea256a78dd852579f627365888dd5ad9b56 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:21:41 -0600 Subject: [PATCH 016/116] docs: add Sprig library functions to help topics --- pkg/cmd/root/help_topic.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 2b6432ef9db..df5a2e0c650 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -151,6 +151,13 @@ var HelpTopics = []helpTopic{ - %[1]struncate %[1]s: ensures input fits within length - %[1]shyperlink %[1]s: renders a terminal hyperlink + The following Sprig template library functions can also be used with this formatting directive: + - %[1]scontains%[1]s: returns true if the input contains the argument + - %[1]shasPrefix%[1]s: returns true if the input has the argument as a prefix + - %[1]shasSuffix%[1]s: returns true if the input has the argument as a suffix + - %[1]sregexMatch%[1]s: returns true if the input matches the argument + For more information about the Sprig library, see . + To learn more about Go templates, see: . `, "`"), example: heredoc.Doc(` From 86ad62a62d6df9d5f048da15e10f81b6901bf36f Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 26 Mar 2025 13:42:39 -0400 Subject: [PATCH 017/116] Fixes #10590 Implement missing safeguard causing `gh alias delete` tests to wipe out tester's GitHub CLI configuration. --- pkg/cmd/alias/delete/delete_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/alias/delete/delete_test.go b/pkg/cmd/alias/delete/delete_test.go index 845119a8007..9bc89830a7a 100644 --- a/pkg/cmd/alias/delete/delete_test.go +++ b/pkg/cmd/alias/delete/delete_test.go @@ -162,6 +162,9 @@ func TestDeleteRun(t *testing.T) { tt.opts.IO = ios cfg := config.NewFromString(tt.config) + cfg.WriteFunc = func() error { + return nil + } tt.opts.Config = func() (gh.Config, error) { return cfg, nil } From 229216e8308e51359f779f3798103dd3734870a4 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:29:20 -0600 Subject: [PATCH 018/116] doc(run shared): clarify 404 handling --- pkg/cmd/run/shared/shared.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index 248b5c5a0fd..ce909fd772e 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -452,6 +452,8 @@ func preloadWorkflowNames(client *api.Client, repo ghrepo.Interface, runs []Run) // look it up directly without receiving a 404, but it is nonetheless // in the workflow run list. To handle this, we set the workflow name // to an empty string. + // Deciding to put this here instead of in GetWorkflow to allow + // the caller to decide what a 404 means. if httpErr, ok := err.(api.HTTPError); ok && httpErr.StatusCode == 404 { workflowMap[run.WorkflowID] = "" continue From 321e5687a6349fd6acae53c054e5e95c1eeda977 Mon Sep 17 00:00:00 2001 From: Phill MV Date: Wed, 26 Mar 2025 17:40:35 -0400 Subject: [PATCH 019/116] Rewrote the gh at verify --help text to a) clarify and b) document the verificationResult object. --- pkg/cmd/attestation/verify/verify.go | 115 ++++++++++++++++++--------- 1 file changed, 79 insertions(+), 36 deletions(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 65ae8ca3ecf..469b2f453f4 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -30,58 +30,101 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command Verify the integrity and provenance of an artifact using its associated cryptographically signed attestations. - In order to verify an attestation, you must validate the identity of the Actions - workflow that produced the attestation (a.k.a. the signer workflow). Given this - identity, the verification process checks the signatures in the attestations, - and confirms that the attestation refers to provided artifact. + ## Verification - To specify the artifact, the command requires: + In order to verify an attestation, you must provide an artifact and validate: + * the identity of the actor that produced the attestation + * the expected attestation predicate type + + By default, this command enforces the "%[2]s" + predicate type. To verify other attestation predicate types use the + %[1]s--predicate-type%[1]s flag. + + The "actor identity" consists of: + * the repository or the repository owner the artifact is linked with + * the Actions workflow that produced the attestation (a.k.a the + signer workflow) + + This identity is then validated against the attestation's certificate's + SourceRepository, SourceRepositoryOwner, and SubjectAlternativeName + (SAN) fields. + + It is up to you to decide how precisely you want to enforce this identity. + + At a minimum, this command requires either: + * the %[1]s--repo%[1]s flag (e.g. --repo github/example), or + * the %[1]s--owner%[1]s flag (e.g. --owner github) + + Ideally, the path of the signer workflow is also validated using the + %[1]s--signer-workflow%[1]s or %[1]s--cert-identity%[1]s flags. + + Please note: if your attestation was generated via a reusable workflow then + that reusable workflow is the signer whose identity needs to be validated. + In this situation, you must also use either the %[1]s--signer-workflow%[1]s or + the %[1]s--signer-repo%[1]s flag. + + For more options, see the other available flags. + + ## Loading Artifacts And Attestations + + To specify the artifact, this command requires: * a file path to an artifact, or * a container image URI (e.g. %[1]soci://%[1]s) * (note that if you provide an OCI URL, you must already be authenticated with its container registry) - To fetch the attestation, and validate the identity of the signer, the command - requires either: - * the %[1]s--repo%[1]s flag (e.g. --repo github/example). - * the %[1]s--owner%[1]s flag (e.g. --owner github), or + By default, this command will attempt to fetch relevant attestations via the + GitHub API using the values provided to %[1]s--owner%[1]s or %[1]s--repo%[1]s. + + To instead fetch attestations from your artifact's OCI registry, use the + %[1]s--bundle-from-oci%[1]s flag. + + For offline verification using attestations stored on disk (c.f. the download command) + provide a path to the %[1]s--bundle%[1]s flag. - The %[1]s--repo%[1]s flag value must match the name of the GitHub repository - that the artifact is linked with. + ## Additional Policy Enforcement - The %[1]s--owner%[1]s flag value must match the name of the GitHub organization - that the artifact's linked repository belongs to. + Given the %[1]s--format=json%[1]s flag, upon successful verification this + command will output a JSON array containing one entry per verified attestation. - By default, the verify command will: - - only verify provenance attestations - - attempt to fetch relevant attestations via the GitHub API. + This output can then be used for additional policy enforcement, i.e. by being + piped into a policy engine. - To verify other types of attestations, use the %[1]s--predicate-type%[1]s flag. + Each object in the array contains two properties: + * an %[1]sattestation%[1]s object, which contains the bundle that was verified + * a %[1]sverificationResult%[1]s object, which is a parsed representation of the + contents of the bundle that was verified. - To use your artifact's OCI registry instead of GitHub's API, use the - %[1]s--bundle-from-oci%[1]s flag. For offline verification, using attestations - stored on desk (c.f. the download command), provide a path to the %[1]s--bundle%[1]s flag. + Within the %[1]sverificationResult%[1]s object you will find: + * %[1]ssignature.certificate%[1]s, which is a parsed representation of the X.509 + certificate embedded in the attestation, + * %[1]sverifiedTimestamps%[1]s, an array of objects denoting when the attestation + was witnessed by a transparency log or a timestamp authority + * %[1]sstatement%[1]s, which contains the %[1]ssubject%[1]s array referencing artifacts, + the %[1]spredicateType%[1]s field, and the %[1]spredicate%[1]s object which contains + additional, often user-controllable, metadata - To see the full results that are generated upon successful verification, i.e. - for use with a policy engine, provide the %[1]s--format=json%[1]s flag. + IMPORTANT: please note that only the %[1]ssignature.certificate%[1]s and the + %[1]sverifiedTimestamps%[1]s properties contain values that cannot be + manipulated by the workflow that originated the attestation. - The signer workflow's identity is validated against the Subject Alternative Name (SAN) - within the attestation certificate. Often, the signer workflow is the - same workflow that started the run and generated the attestation, and will be - located inside your repository. For this reason, by default this command uses - either the %[1]s--repo%[1]s or the %[1]s--owner%[1]s flag value to validate the SAN. + When dealing with attestations created within GitHub Actions, the contents of + %[1]ssignature.certificate%[1]s are populated directly from the OpenID Connect + token that GitHub has generated. The contents of the %[1]sverifiedTimestamps%[1]s + array are populated from the signed timestamps originating from either a + transparency log or a timestamp authority – and likewise cannot be forged by users. - However, sometimes the caller workflow is not the same workflow that - performed the signing. If your attestation was generated via a reusable - workflow, then that reusable workflow is the signer whose identity needs to be - validated. In this situation, the signer workflow may or may not be located - inside your %[1]s--repo%[1]s or %[1]s--owner%[1]s. + When designing policy enforcement using this output, special care must be taken + when examining the contents of the %[1]sstatement.predicate%[1]s property: + should an attacker gain access to your workflow's execution context, they + could then falsify the contents of the %[1]sstatement.predicate%[1]s. - When using reusable workflows, use the %[1]s--signer-repo%[1]s, %[1]s--signer-workflow%[1]s, - or %[1]s--cert-identity%[1]s flags to validate the signer workflow's identity. + To mitigate this attack vector, consider using a "trusted builder": when generating + an artifact, have the build and attestation signing occur within a reusable workflow + whose execution cannot be influenced by input provided through the caller workflow. - For more policy verification options, see the other available flags. - `, "`"), + See above re: %[1]s--signer-workflow%[1]s. + `, "`", verification.SLSAPredicateV1), Example: heredoc.Doc(` # Verify an artifact linked with a repository $ gh attestation verify example.bin --repo github/example From 33ab0b8f3b3558f2b8134089664bbdc418173bba Mon Sep 17 00:00:00 2001 From: Phill MV Date: Thu, 27 Mar 2025 09:47:11 -0400 Subject: [PATCH 020/116] Tweaked language a bit, improved error message. --- pkg/cmd/attestation/verify/policy.go | 2 +- pkg/cmd/attestation/verify/verify.go | 36 ++++++++++++++++------------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 1060a781e2c..1d1595eca70 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -161,7 +161,7 @@ func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) { // if the provided workflow did not match the expect format // we move onto creating a signer workflow using the provided host name if hostname == "" { - return "", errors.New("unknown host") + return "", errors.New("unknown signer workflow host") } return fmt.Sprintf("^https://%s/%s", hostname, signerWorkflow), nil diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 469b2f453f4..0acea06c7ac 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -30,13 +30,16 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command Verify the integrity and provenance of an artifact using its associated cryptographically signed attestations. - ## Verification + ## Understanding Verification + + An attestation is a claim (i.e. a provenance statement) made by an actor + (i.e. a GitHub Actions workflow) regarding a subject (i.e. an artifact). In order to verify an attestation, you must provide an artifact and validate: * the identity of the actor that produced the attestation - * the expected attestation predicate type + * the expected attestation predicate type (the nature of the claim) - By default, this command enforces the "%[2]s" + By default, this command enforces the %[1]s%[2]s%[1]s predicate type. To verify other attestation predicate types use the %[1]s--predicate-type%[1]s flag. @@ -52,8 +55,11 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command It is up to you to decide how precisely you want to enforce this identity. At a minimum, this command requires either: - * the %[1]s--repo%[1]s flag (e.g. --repo github/example), or - * the %[1]s--owner%[1]s flag (e.g. --owner github) + * the %[1]s--owner%[1]s flag (e.g. --owner github), or + * the %[1]s--repo%[1]s flag (e.g. --repo github/example) + + The more precisely you specify the identity, the more control you will + have over the security guarantees offered by the verification process. Ideally, the path of the signer workflow is also validated using the %[1]s--signer-workflow%[1]s or %[1]s--cert-identity%[1]s flags. @@ -224,23 +230,23 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") verifyCmd.MarkFlagsMutuallyExclusive("owner", "repo") verifyCmd.MarkFlagsOneRequired("owner", "repo") - verifyCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", verification.SLSAPredicateV1, "Filter attestations by provided predicate type") verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Do not verify attestations signed with Sigstore public good instance") verifyCmd.Flags().StringVarP(&opts.TrustedRoot, "custom-trusted-root", "", "", "Path to a trusted_root.jsonl file; likely for offline verification") verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") cmdutil.AddFormatFlags(verifyCmd, &opts.exporter) + verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") // policy enforcement flags + verifyCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", verification.SLSAPredicateV1, "Enforce that verified attestations' predicate type matches the provided value") verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners") - verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's subject alternative name matches the provided value exactly") - verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's subject alternative name matches the provided regex") - verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") - verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") + verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's SubjectAlternativeName matches the provided value exactly") + verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's SubjectAlternativeName matches the provided regex") + verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Enforce that the workflow that signed the attestation's repository matches the provided value (/)") + verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Enforce that the workflow that signed the attestation matches the provided value ([host/]////)") verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex", "signer-repo", "signer-workflow") - verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verification.GitHubOIDCIssuer, "Issuer of the OIDC token") - verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") - verifyCmd.Flags().StringVarP(&opts.SignerDigest, "signer-digest", "", "", "Digest associated with the signer workflow") - verifyCmd.Flags().StringVarP(&opts.SourceRef, "source-ref", "", "", "Ref associated with the source workflow") - verifyCmd.Flags().StringVarP(&opts.SourceDigest, "source-digest", "", "", "Digest associated with the source workflow") + verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verification.GitHubOIDCIssuer, "Enforce that the issuer of the OIDC token matches the provided value") + verifyCmd.Flags().StringVarP(&opts.SignerDigest, "signer-digest", "", "", "Enforce that the digest associated with the signer workflow matches the provided value") + verifyCmd.Flags().StringVarP(&opts.SourceRef, "source-ref", "", "", "Enforce that the git ref associated with the source repository matches the provided value") + verifyCmd.Flags().StringVarP(&opts.SourceDigest, "source-digest", "", "", "Enforce that the digest associated with the source repository matches the provided value") return verifyCmd } From 9c9b158d12f0ae735391373c63d102f2322b3f64 Mon Sep 17 00:00:00 2001 From: Phill MV Date: Thu, 27 Mar 2025 09:55:14 -0400 Subject: [PATCH 021/116] added minor caveat --- pkg/cmd/attestation/verify/verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 0acea06c7ac..34e53861214 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -50,7 +50,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command This identity is then validated against the attestation's certificate's SourceRepository, SourceRepositoryOwner, and SubjectAlternativeName - (SAN) fields. + (SAN) fields, among others. It is up to you to decide how precisely you want to enforce this identity. From f099a542438bd12b7cdca56b39150bc7226d2438 Mon Sep 17 00:00:00 2001 From: Phill MV Date: Thu, 27 Mar 2025 09:57:00 -0400 Subject: [PATCH 022/116] updated test --- pkg/cmd/attestation/verify/policy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index d376498b6aa..ff10cad11d7 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -275,7 +275,7 @@ func TestValidateSignerWorkflow(t *testing.T) { name: "workflow with no host specified", providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", expectErr: true, - errContains: "unknown host", + errContains: "unknown signer workflow host", }, { name: "workflow with default host", From c1fbc2f05bf301f6b49b07eb3fef4826357bb108 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:21:13 -0600 Subject: [PATCH 023/116] test(many): fix whitespace in tests expectations Due to https://github.com/charmbracelet/glamour/pull/334, the margin used for markdown rendering has changed by a couple spaces. This corrects the relevant tests to accomodate that change. --- pkg/cmd/gist/view/view_test.go | 4 ++-- pkg/cmd/pr/create/create_test.go | 4 ++-- pkg/cmd/pr/review/review_test.go | 2 +- pkg/cmd/release/view/view_test.go | 29 ++++++++++++++--------------- pkg/cmd/repo/view/view_test.go | 10 +++++----- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index 704766e10f8..706b85f103e 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -234,7 +234,7 @@ func Test_viewRun(t *testing.T) { }, }, }, - wantOut: "cicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n # foo \n\n", + wantOut: "cicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n # foo \n\n", }, { name: "multiple files, trailing newlines", @@ -277,7 +277,7 @@ func Test_viewRun(t *testing.T) { }, }, }, - wantOut: "some files\n\ncicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n \n • foo \n\n", + wantOut: "some files\n\ncicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n \n • foo \n\n", }, { name: "multiple files, raw", diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 4d7c294dbfa..55012d7ddd9 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -496,7 +496,7 @@ func Test_createRun(t *testing.T) { `MaintainerCanModify: false`, `Body:`, ``, - ` my body `, + ` my body `, ``, ``, }, @@ -568,7 +568,7 @@ func Test_createRun(t *testing.T) { `MaintainerCanModify: false`, `Body:`, ``, - ` BODY `, + ` BODY `, ``, ``, }, diff --git a/pkg/cmd/pr/review/review_test.go b/pkg/cmd/pr/review/review_test.go index 611f71894a1..f9e00c3b8ee 100644 --- a/pkg/cmd/pr/review/review_test.go +++ b/pkg/cmd/pr/review/review_test.go @@ -283,7 +283,7 @@ func TestPRReview_interactive(t *testing.T) { assert.Equal(t, heredoc.Doc(` Got: - cool story + cool story `), output.String()) assert.Equal(t, "✓ Approved pull request OWNER/REPO#123\n", output.Stderr()) diff --git a/pkg/cmd/release/view/view_test.go b/pkg/cmd/release/view/view_test.go index 1fed0fdc8e4..a5dd3b2e2f9 100644 --- a/pkg/cmd/release/view/view_test.go +++ b/pkg/cmd/release/view/view_test.go @@ -141,19 +141,18 @@ func Test_viewRun(t *testing.T) { opts: ViewOptions{ TagName: "v1.2.3", }, - wantStdout: heredoc.Doc(` - v1.2.3 - MonaLisa released this about 1 day ago - - - • Fixed bugs - - - Assets - windows.zip 12 B - linux.tgz 34 B - - View on GitHub: https://github.com/OWNER/REPO/releases/tags/v1.2.3 + wantStdout: heredoc.Doc(`v1.2.3 + MonaLisa released this about 1 day ago + + + • Fixed bugs + + + Assets + windows.zip 12 B + linux.tgz 34 B + + View on GitHub: https://github.com/OWNER/REPO/releases/tags/v1.2.3 `), wantStderr: ``, }, @@ -169,8 +168,8 @@ func Test_viewRun(t *testing.T) { v1.2.3 MonaLisa released this about 1 day ago - - • Fixed bugs + + • Fixed bugs Assets diff --git a/pkg/cmd/repo/view/view_test.go b/pkg/cmd/repo/view/view_test.go index d3b6d619b3e..f07f9de187a 100644 --- a/pkg/cmd/repo/view/view_test.go +++ b/pkg/cmd/repo/view/view_test.go @@ -260,7 +260,7 @@ func Test_ViewRun(t *testing.T) { social distancing - # truly cool readme check it out + # truly cool readme check it out @@ -279,7 +279,7 @@ func Test_ViewRun(t *testing.T) { social distancing - # truly cool readme check it out + # truly cool readme check it out @@ -297,7 +297,7 @@ func Test_ViewRun(t *testing.T) { social distancing - # truly cool readme check it out + # truly cool readme check it out @@ -312,7 +312,7 @@ func Test_ViewRun(t *testing.T) { social distancing - # truly cool readme check it out + # truly cool readme check it out @@ -650,7 +650,7 @@ func Test_ViewRun_HandlesSpecialCharacters(t *testing.T) { Some basic special characters " & / < > ' - # < is always > than & ' and " + # < is always > than & ' and " From 89def92f042505614f72d2fba8142f6808e6541d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:51:32 -0600 Subject: [PATCH 024/116] doc(formatting): update sprig func descriptions --- pkg/cmd/root/help_topic.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index df5a2e0c650..5e68f99d903 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -152,10 +152,10 @@ var HelpTopics = []helpTopic{ - %[1]shyperlink %[1]s: renders a terminal hyperlink The following Sprig template library functions can also be used with this formatting directive: - - %[1]scontains%[1]s: returns true if the input contains the argument - - %[1]shasPrefix%[1]s: returns true if the input has the argument as a prefix - - %[1]shasSuffix%[1]s: returns true if the input has the argument as a suffix - - %[1]sregexMatch%[1]s: returns true if the input matches the argument + - %[1]scontains %[1]s: checks if %[1]sstring%[1]s contains %[1]sarg%[1]s + - %[1]shasPrefix %[1]s: checks if %[1]sstring%[1]s starts with %[1]sprefix%[1]s + - %[1]shasSuffix %[1]s: checks if %[1]sstring%[1]s ends with %[1]ssuffix%[1]s + - %[1]sregexMatch %[1]s: checks if %[1]sstring%[1]s has any matches for %[1]sregex%[1]s For more information about the Sprig library, see . To learn more about Go templates, see: . From eabd02793e5cbaab5307049a4a4dc7e7a96e5ce0 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 28 Mar 2025 13:19:27 -0400 Subject: [PATCH 025/116] Renaming tabl eheader variables for maintainability --- pkg/iostreams/color.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index e2841d03048..07fdcd79fa6 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -15,24 +15,24 @@ const ( highlightStyle = "black:yellow" ) -// Special cases like darkTableHeader / lightTableHeader are necessary when using color and modifiers +// Special cases like darkThemeTableHeader / lightThemeTableHeader are necessary when using color and modifiers // (bold, underline, dim) because ansi.ColorFunc requires a foreground color and resets formats. var ( - magenta = ansi.ColorFunc("magenta") - cyan = ansi.ColorFunc("cyan") - red = ansi.ColorFunc("red") - yellow = ansi.ColorFunc("yellow") - blue = ansi.ColorFunc("blue") - green = ansi.ColorFunc("green") - gray = ansi.ColorFunc("black+h") - bold = ansi.ColorFunc("default+b") - cyanBold = ansi.ColorFunc("cyan+b") - greenBold = ansi.ColorFunc("green+b") - highlightStart = ansi.ColorCode(highlightStyle) - highlight = ansi.ColorFunc(highlightStyle) - darkTableHeader = ansi.ColorFunc("white+du") - lightTableHeader = ansi.ColorFunc("black+hu") - noneTableHeader = ansi.ColorFunc("default+u") + magenta = ansi.ColorFunc("magenta") + cyan = ansi.ColorFunc("cyan") + red = ansi.ColorFunc("red") + yellow = ansi.ColorFunc("yellow") + blue = ansi.ColorFunc("blue") + green = ansi.ColorFunc("green") + gray = ansi.ColorFunc("black+h") + bold = ansi.ColorFunc("default+b") + cyanBold = ansi.ColorFunc("cyan+b") + greenBold = ansi.ColorFunc("green+b") + highlightStart = ansi.ColorCode(highlightStyle) + highlight = ansi.ColorFunc(highlightStyle) + darkThemeTableHeader = ansi.ColorFunc("white+du") + lightThemeTableHeader = ansi.ColorFunc("black+hu") + noThemeTableHeader = ansi.ColorFunc("default+u") gray256 = func(t string) string { return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t) @@ -268,10 +268,10 @@ func (c *ColorScheme) TableHeader(t string) string { switch c.theme { case DarkTheme: - return darkTableHeader(t) + return darkThemeTableHeader(t) case LightTheme: - return lightTableHeader(t) + return lightThemeTableHeader(t) default: - return noneTableHeader(t) + return noThemeTableHeader(t) } } From c6b4da8f2072d56d40a98c2046369d110988638d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:00:06 -0600 Subject: [PATCH 026/116] doc(formatting): add line breaks for readability --- pkg/cmd/root/help_topic.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 5e68f99d903..db0ef098d18 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -138,6 +138,7 @@ var HelpTopics = []helpTopic{ The %[1]s--template%[1]s flag requires a string argument in Go template syntax, and will only print those JSON values which match the query. + In addition to the Go template functions in the standard library, the following functions can be used with this formatting directive: - %[1]sautocolor%[1]s: like %[1]scolor%[1]s, but only emits color to terminals @@ -156,6 +157,7 @@ var HelpTopics = []helpTopic{ - %[1]shasPrefix %[1]s: checks if %[1]sstring%[1]s starts with %[1]sprefix%[1]s - %[1]shasSuffix %[1]s: checks if %[1]sstring%[1]s ends with %[1]ssuffix%[1]s - %[1]sregexMatch %[1]s: checks if %[1]sstring%[1]s has any matches for %[1]sregex%[1]s + For more information about the Sprig library, see . To learn more about Go templates, see: . From aac43870d4526ebc88ee0ec5151f0bf857483e19 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:11:57 -0600 Subject: [PATCH 027/116] test(release view): fix indentation --- pkg/cmd/release/view/view_test.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/release/view/view_test.go b/pkg/cmd/release/view/view_test.go index a5dd3b2e2f9..8ca4f14c8df 100644 --- a/pkg/cmd/release/view/view_test.go +++ b/pkg/cmd/release/view/view_test.go @@ -141,18 +141,19 @@ func Test_viewRun(t *testing.T) { opts: ViewOptions{ TagName: "v1.2.3", }, - wantStdout: heredoc.Doc(`v1.2.3 - MonaLisa released this about 1 day ago - - - • Fixed bugs - - - Assets - windows.zip 12 B - linux.tgz 34 B - - View on GitHub: https://github.com/OWNER/REPO/releases/tags/v1.2.3 + wantStdout: heredoc.Doc(` + v1.2.3 + MonaLisa released this about 1 day ago + + + • Fixed bugs + + + Assets + windows.zip 12 B + linux.tgz 34 B + + View on GitHub: https://github.com/OWNER/REPO/releases/tags/v1.2.3 `), wantStderr: ``, }, From 0427f2688440ef515078a03d81ee64a6d4d9fdd2 Mon Sep 17 00:00:00 2001 From: Phill MV Date: Mon, 31 Mar 2025 11:05:23 -0400 Subject: [PATCH 028/116] Update pkg/cmd/attestation/verify/verify.go Co-authored-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 34e53861214..3affdfabb23 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -66,7 +66,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command Please note: if your attestation was generated via a reusable workflow then that reusable workflow is the signer whose identity needs to be validated. - In this situation, you must also use either the %[1]s--signer-workflow%[1]s or + In this situation, you must use either the %[1]s--signer-workflow%[1]s or the %[1]s--signer-repo%[1]s flag. For more options, see the other available flags. From cf9ac4447aee06259acd43786c7f6e20dcd08681 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 31 Mar 2025 14:06:24 +0200 Subject: [PATCH 029/116] Acceptance test issue/pr create/edit with project --- .../issue-create-edit-with-project.txtar | 43 ++++++++++++++++ .../pr/pr-create-edit-with-project.txtar | 51 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 acceptance/testdata/issue/issue-create-edit-with-project.txtar create mode 100644 acceptance/testdata/pr/pr-create-edit-with-project.txtar diff --git a/acceptance/testdata/issue/issue-create-edit-with-project.txtar b/acceptance/testdata/issue/issue-create-edit-with-project.txtar new file mode 100644 index 00000000000..568530cb9d3 --- /dev/null +++ b/acceptance/testdata/issue/issue-create-edit-with-project.txtar @@ -0,0 +1,43 @@ +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} + +# Create a repository with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup +defer gh repo delete --yes ${ORG}/${REPO} + +# Create a project +env PROJECT_TITLE=${REPO}-project +exec gh project create --owner=${ORG} --title=${PROJECT_TITLE} --format='json' --jq='.number' +stdout2env PROJECT_NUMBER + +defer gh project delete --owner=${ORG} ${PROJECT_NUMBER} + +# Clone the repo +exec gh repo clone ${ORG}/${REPO} + +# Create an issue in the repo +cd ${REPO} +exec gh issue create --title 'Feature Request' --body 'Feature Body' --project ${PROJECT_TITLE} +stdout2env ISSUE_URL + +# Check that default issue view is working +exec gh issue view ${ISSUE_URL} + +# Check the issue was added to the project +exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[0].title' +stdout ${PROJECT_TITLE} + +# Remove the issue from the project +exec gh issue edit ${ISSUE_URL} --remove-project ${PROJECT_TITLE} + +# Check the issue was removed from the project +exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[0].title' +! stdout ${PROJECT_TITLE} + +# Re add the issue to the project +exec gh issue edit ${ISSUE_URL} --add-project ${PROJECT_TITLE} + +# Check the issue was added to the project +exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[0].title' +stdout ${PROJECT_TITLE} diff --git a/acceptance/testdata/pr/pr-create-edit-with-project.txtar b/acceptance/testdata/pr/pr-create-edit-with-project.txtar new file mode 100644 index 00000000000..9850313f0a0 --- /dev/null +++ b/acceptance/testdata/pr/pr-create-edit-with-project.txtar @@ -0,0 +1,51 @@ +# Use gh as a credential helper +exec gh auth setup-git + +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} + +# Create a repository with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup +defer gh repo delete --yes ${ORG}/${REPO} + +# Create a project +env PROJECT_TITLE=${REPO}-project +exec gh project create --owner=${ORG} --title=${PROJECT_TITLE} --format='json' --jq='.number' +stdout2env PROJECT_NUMBER + +defer gh project delete --owner=${ORG} ${PROJECT_NUMBER} + +# Clone the repo +exec gh repo clone ${ORG}/${REPO} + +# Prepare a branch to PR +cd ${REPO} +exec git checkout -b feature-branch +exec git commit --allow-empty -m 'Empty Commit' +exec git push -u origin feature-branch + +# Create the PR +exec gh pr create --title 'Feature Title' --body 'Feature Body' --project ${PROJECT_TITLE} +stdout2env PR_URL + +# Check that default pr view is working +exec gh pr view ${PR_URL} + +# Check the pr was added to the project +exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[0].title' +stdout ${PROJECT_TITLE} + +# Remove the pr from the project +exec gh pr edit ${PR_URL} --remove-project ${PROJECT_TITLE} + +# Check the pr was removed from the project +exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[0].title' +! stdout ${PROJECT_TITLE} + +# Re add the pr to the project +exec gh pr edit ${PR_URL} --add-project ${PROJECT_TITLE} + +# Check the pr was added to the project +exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[0].title' +stdout ${PROJECT_TITLE} From 346fab212b177386de12beb8a6dc0f7406b96b88 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 31 Mar 2025 11:19:51 -0400 Subject: [PATCH 030/116] Accessible color config boilerplate, colors update This commit is focused on incorporating cli/go-gh accessible colors configuration setting into GitHub CLI experience along with new color function to supersede ColorScheme.Gray and ColorScheme.Grayf. Originally, I was considering having all use of ColorScheme.Gray and ColorScheme.Grayf fallback to the new muted logic if accessible colors were enabled, however I decided not being that it exceeds the acceptance criteria. This means that every command using ColorScheme.Gray needs to be updated to use ColorScheme.Muted --- internal/config/config.go | 18 ++++++++++++++ internal/gh/gh.go | 2 ++ internal/gh/mock/config.go | 44 ++++++++++++++++++++++++++++++++++ pkg/cmd/factory/default.go | 3 +++ pkg/iostreams/color.go | 47 ++++++++++++++++++++++++++++++------- pkg/iostreams/color_test.go | 36 ++++++++++++++-------------- pkg/iostreams/iostreams.go | 15 +++++++++--- 7 files changed, 135 insertions(+), 30 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 476154d667e..51116df6c2b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,9 +12,11 @@ import ( o "github.com/cli/cli/v2/pkg/option" ghauth "github.com/cli/go-gh/v2/pkg/auth" ghConfig "github.com/cli/go-gh/v2/pkg/config" + xcolor "github.com/cli/go-gh/v2/pkg/x/color" ) const ( + accessibleColorsKey = xcolor.AccessibleColorsSetting aliasesKey = "aliases" browserKey = "browser" editorKey = "editor" @@ -108,6 +110,11 @@ func (c *cfg) Authentication() gh.AuthConfig { return &AuthConfig{cfg: c.cfg} } +func (c *cfg) AccessibleColors(hostname string) gh.ConfigEntry { + // Intentionally panic if there is no user provided value or default value (which would be a programmer error) + return c.GetOrDefault(hostname, accessibleColorsKey).Unwrap() +} + func (c *cfg) Browser(hostname string) gh.ConfigEntry { // Intentionally panic if there is no user provided value or default value (which would be a programmer error) return c.GetOrDefault(hostname, browserKey).Unwrap() @@ -532,6 +539,8 @@ aliases: http_unix_socket: # What web browser gh should use when opening URLs. If blank, will refer to environment. browser: +# Preference for accessible colors that can be customized. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled +accessible_colors: disabled ` type ConfigOption struct { @@ -602,6 +611,15 @@ var Options = []ConfigOption{ return c.Browser(hostname).Value }, }, + { + Key: accessibleColorsKey, + Description: "toggle preference for accessible colors that can be customized", + DefaultValue: "disabled", + AllowedValues: []string{"enabled", "disabled"}, + CurrentValue: func(c gh.Config, hostname string) string { + return c.AccessibleColors(hostname).Value + }, + }, } func HomeDirPath(subdir string) (string, error) { diff --git a/internal/gh/gh.go b/internal/gh/gh.go index 8e640c41a88..4f3ece20487 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -35,6 +35,8 @@ type Config interface { // Set provides primitive access for setting configuration values, optionally scoped by host. Set(hostname string, key string, value string) + // AccessibleColors returns the configured accessible colors preference, optionally scoped by host. + AccessibleColors(hostname string) ConfigEntry // Browser returns the configured browser, optionally scoped by host. Browser(hostname string) ConfigEntry // Editor returns the configured editor, optionally scoped by host. diff --git a/internal/gh/mock/config.go b/internal/gh/mock/config.go index 569af1facb4..ecb8814b8e7 100644 --- a/internal/gh/mock/config.go +++ b/internal/gh/mock/config.go @@ -19,6 +19,9 @@ var _ gh.Config = &ConfigMock{} // // // make and configure a mocked gh.Config // mockedConfig := &ConfigMock{ +// AccessibleColorsFunc: func(hostname string) gh.ConfigEntry { +// panic("mock out the AccessibleColors method") +// }, // AliasesFunc: func() gh.AliasConfig { // panic("mock out the Aliases method") // }, @@ -71,6 +74,9 @@ var _ gh.Config = &ConfigMock{} // // } type ConfigMock struct { + // AccessibleColorsFunc mocks the AccessibleColors method. + AccessibleColorsFunc func(hostname string) gh.ConfigEntry + // AliasesFunc mocks the Aliases method. AliasesFunc func() gh.AliasConfig @@ -118,6 +124,11 @@ type ConfigMock struct { // calls tracks calls to the methods. calls struct { + // AccessibleColors holds details about calls to the AccessibleColors method. + AccessibleColors []struct { + // Hostname is the hostname argument value. + Hostname string + } // Aliases holds details about calls to the Aliases method. Aliases []struct { } @@ -190,6 +201,7 @@ type ConfigMock struct { Write []struct { } } + lockAccessibleColors sync.RWMutex lockAliases sync.RWMutex lockAuthentication sync.RWMutex lockBrowser sync.RWMutex @@ -207,6 +219,38 @@ type ConfigMock struct { lockWrite sync.RWMutex } +// AccessibleColors calls AccessibleColorsFunc. +func (mock *ConfigMock) AccessibleColors(hostname string) gh.ConfigEntry { + if mock.AccessibleColorsFunc == nil { + panic("ConfigMock.AccessibleColorsFunc: method is nil but Config.AccessibleColors was just called") + } + callInfo := struct { + Hostname string + }{ + Hostname: hostname, + } + mock.lockAccessibleColors.Lock() + mock.calls.AccessibleColors = append(mock.calls.AccessibleColors, callInfo) + mock.lockAccessibleColors.Unlock() + return mock.AccessibleColorsFunc(hostname) +} + +// AccessibleColorsCalls gets all the calls that were made to AccessibleColors. +// Check the length with: +// +// len(mockedConfig.AccessibleColorsCalls()) +func (mock *ConfigMock) AccessibleColorsCalls() []struct { + Hostname string +} { + var calls []struct { + Hostname string + } + mock.lockAccessibleColors.RLock() + calls = mock.calls.AccessibleColors + mock.lockAccessibleColors.RUnlock() + return calls +} + // Aliases calls AliasesFunc. func (mock *ConfigMock) Aliases() gh.AliasConfig { if mock.AliasesFunc == nil { diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index 9236514877c..598be0747a3 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -19,6 +19,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/extension" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" + xcolor "github.com/cli/go-gh/v2/pkg/x/color" ) var ssoHeader string @@ -292,6 +293,8 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { io.SetPager(pager.Value) } + io.SetAccessibleColorsEnabled(xcolor.IsAccessibleColorsEnabled()) + return io } diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 07fdcd79fa6..3e2d08ac21d 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -30,7 +30,9 @@ var ( greenBold = ansi.ColorFunc("green+b") highlightStart = ansi.ColorCode(highlightStyle) highlight = ansi.ColorFunc(highlightStyle) + darkThemeMuted = ansi.ColorFunc("white+d") darkThemeTableHeader = ansi.ColorFunc("white+du") + lightThemeMuted = ansi.ColorFunc("black+h") lightThemeTableHeader = ansi.ColorFunc("black+hu") noThemeTableHeader = ansi.ColorFunc("default+u") @@ -42,20 +44,22 @@ var ( // NewColorScheme initializes color logic based on provided terminal capabilities. // Logic dealing with terminal theme detected, such as whether color is enabled, 8-bit color supported, true color supported, // and terminal theme detected. -func NewColorScheme(enabled, is256enabled, trueColor bool, theme string) *ColorScheme { +func NewColorScheme(enabled, is256enabled, trueColor, accessibleColors bool, theme string) *ColorScheme { return &ColorScheme{ - enabled: enabled, - is256enabled: is256enabled, - hasTrueColor: trueColor, - theme: theme, + enabled: enabled, + is256enabled: is256enabled, + hasTrueColor: trueColor, + accessibleColors: accessibleColors, + theme: theme, } } type ColorScheme struct { - enabled bool - is256enabled bool - hasTrueColor bool - theme string + enabled bool + is256enabled bool + hasTrueColor bool + accessibleColors bool + theme string } func (c *ColorScheme) Enabled() bool { @@ -73,6 +77,29 @@ func (c *ColorScheme) Boldf(t string, args ...interface{}) string { return c.Bold(fmt.Sprintf(t, args...)) } +func (c *ColorScheme) Muted(t string) string { + // Fallback to previous logic if accessible colors preview is disabled. + if !c.accessibleColors { + return c.Gray(t) + } + + // Muted text is only stylized if color is enabled. + if !c.enabled { + return t + } + + switch c.theme { + case LightTheme: + return lightThemeMuted(t) + default: + return darkThemeMuted(t) + } +} + +func (c *ColorScheme) Mutedf(t string, args ...interface{}) string { + return c.Muted(fmt.Sprintf(t, args...)) +} + func (c *ColorScheme) Red(t string) string { if !c.enabled { return t @@ -113,6 +140,7 @@ func (c *ColorScheme) GreenBold(t string) string { return greenBold(t) } +// Deprecated: Use Muted instead for thematically contrasting color. func (c *ColorScheme) Gray(t string) string { if !c.enabled { return t @@ -123,6 +151,7 @@ func (c *ColorScheme) Gray(t string) string { return gray(t) } +// Deprecated: Use Mutedf instead for thematically contrasting color. func (c *ColorScheme) Grayf(t string, args ...interface{}) string { return c.Gray(fmt.Sprintf(t, args...)) } diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index b35c2eb73a8..a399e6d589c 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -20,28 +20,28 @@ func TestColorFromRGB(t *testing.T) { hex: "fc0303", text: "red", wants: "\033[38;2;252;3;3mred\033[0m", - cs: NewColorScheme(true, true, true, NoTheme), + cs: NewColorScheme(true, true, true, false, NoTheme), }, { name: "no truecolor", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, false, NoTheme), + cs: NewColorScheme(true, true, false, false, NoTheme), }, { name: "no color", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, NoTheme), + cs: NewColorScheme(false, false, false, false, NoTheme), }, { name: "invalid hex", hex: "fc0", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, NoTheme), + cs: NewColorScheme(false, false, false, false, NoTheme), }, } @@ -64,28 +64,28 @@ func TestHexToRGB(t *testing.T) { hex: "fc0303", text: "red", wants: "\033[38;2;252;3;3mred\033[0m", - cs: NewColorScheme(true, true, true, NoTheme), + cs: NewColorScheme(true, true, true, false, NoTheme), }, { name: "no truecolor", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, false, NoTheme), + cs: NewColorScheme(true, true, false, false, NoTheme), }, { name: "no color", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, NoTheme), + cs: NewColorScheme(false, false, false, false, NoTheme), }, { name: "invalid hex", hex: "fc0", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, NoTheme), + cs: NewColorScheme(false, false, false, false, NoTheme), }, } @@ -109,61 +109,61 @@ func TestTableHeader(t *testing.T) { }{ { name: "when color is disabled, text is not stylized", - cs: NewColorScheme(false, false, false, NoTheme), + cs: NewColorScheme(false, false, false, true, NoTheme), input: "this should not be stylized", expected: "this should not be stylized", }, { name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, false, false, NoTheme), + cs: NewColorScheme(true, false, false, true, NoTheme), input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, false, false, LightTheme), + cs: NewColorScheme(true, false, false, true, LightTheme), input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, false, false, DarkTheme), + cs: NewColorScheme(true, false, false, true, DarkTheme), input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, { name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, true, false, NoTheme), + cs: NewColorScheme(true, true, false, true, NoTheme), input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, true, false, LightTheme), + cs: NewColorScheme(true, true, false, true, LightTheme), input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, true, false, DarkTheme), + cs: NewColorScheme(true, true, false, true, DarkTheme), input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, { name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, true, true, NoTheme), + cs: NewColorScheme(true, true, true, true, NoTheme), input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, true, true, LightTheme), + cs: NewColorScheme(true, true, true, true, LightTheme), input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, true, true, DarkTheme), + cs: NewColorScheme(true, true, true, true, DarkTheme), input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 6c12d7911a6..30981386b30 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -70,8 +70,9 @@ type IOStreams struct { stderrTTYOverride bool stderrIsTTY bool - colorOverride bool - colorEnabled bool + colorOverride bool + colorEnabled bool + accessibleColorsEnabled bool pagerCommand string pagerProcess *os.Process @@ -366,7 +367,7 @@ func (s *IOStreams) TerminalWidth() int { } func (s *IOStreams) ColorScheme() *ColorScheme { - return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.TerminalTheme()) + return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.AccessibleColorsEnabled(), s.TerminalTheme()) } func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) { @@ -391,6 +392,14 @@ func (s *IOStreams) TempFile(dir, pattern string) (*os.File, error) { return os.CreateTemp(dir, pattern) } +func (s *IOStreams) SetAccessibleColorsEnabled(enabled bool) { + s.accessibleColorsEnabled = enabled +} + +func (s *IOStreams) AccessibleColorsEnabled() bool { + return s.accessibleColorsEnabled +} + func System() *IOStreams { terminal := ghTerm.FromEnv() From d2cd14b4cdb32d5edf97294dac9a462672450924 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 31 Mar 2025 11:58:30 -0400 Subject: [PATCH 031/116] Remove out of scope changes, update list commands After discussing this with the team, the `gh config` changes to display `accessible_colors` have been removed from this branch being outside of acceptance criteria. This will be moved to a separate issue along with any other work needed to finalize the public preview such as `gh help` entries for `GH_ACCESSIBLE_COLORS` environment variable. List commands that use ColorScheme.Gray have been updated to use ColorScheme.Muted. --- internal/config/config.go | 14 -------------- internal/gh/gh.go | 2 -- pkg/cmd/cache/list/list.go | 9 +++++---- pkg/cmd/codespace/list.go | 4 ++-- pkg/cmd/gist/list/list.go | 2 +- pkg/cmd/gpg-key/list/list.go | 4 ++-- pkg/cmd/pr/list/list.go | 2 +- pkg/cmd/release/list/list.go | 10 +++++----- pkg/cmd/repo/deploy-key/list/list.go | 2 +- pkg/cmd/repo/list/list.go | 4 ++-- pkg/cmd/run/list/list.go | 2 +- pkg/cmd/ssh-key/list/list.go | 4 ++-- 12 files changed, 22 insertions(+), 37 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 51116df6c2b..a3849b9afca 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -110,11 +110,6 @@ func (c *cfg) Authentication() gh.AuthConfig { return &AuthConfig{cfg: c.cfg} } -func (c *cfg) AccessibleColors(hostname string) gh.ConfigEntry { - // Intentionally panic if there is no user provided value or default value (which would be a programmer error) - return c.GetOrDefault(hostname, accessibleColorsKey).Unwrap() -} - func (c *cfg) Browser(hostname string) gh.ConfigEntry { // Intentionally panic if there is no user provided value or default value (which would be a programmer error) return c.GetOrDefault(hostname, browserKey).Unwrap() @@ -611,15 +606,6 @@ var Options = []ConfigOption{ return c.Browser(hostname).Value }, }, - { - Key: accessibleColorsKey, - Description: "toggle preference for accessible colors that can be customized", - DefaultValue: "disabled", - AllowedValues: []string{"enabled", "disabled"}, - CurrentValue: func(c gh.Config, hostname string) string { - return c.AccessibleColors(hostname).Value - }, - }, } func HomeDirPath(subdir string) (string, error) { diff --git a/internal/gh/gh.go b/internal/gh/gh.go index 4f3ece20487..8e640c41a88 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -35,8 +35,6 @@ type Config interface { // Set provides primitive access for setting configuration values, optionally scoped by host. Set(hostname string, key string, value string) - // AccessibleColors returns the configured accessible colors preference, optionally scoped by host. - AccessibleColors(hostname string) ConfigEntry // Browser returns the configured browser, optionally scoped by host. Browser(hostname string) ConfigEntry // Editor returns the configured editor, optionally scoped by host. diff --git a/pkg/cmd/cache/list/list.go b/pkg/cmd/cache/list/list.go index 9f39876cbc6..d699e2c3d9a 100644 --- a/pkg/cmd/cache/list/list.go +++ b/pkg/cmd/cache/list/list.go @@ -99,11 +99,12 @@ func listRun(opts *ListOptions) error { } client := api.NewClientFromHTTP(httpClient) + cs := opts.IO.ColorScheme() opts.IO.StartProgressIndicator() result, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: opts.Limit, Sort: opts.Sort, Order: opts.Order, Key: opts.Key, Ref: opts.Ref}) opts.IO.StopProgressIndicator() if err != nil { - return fmt.Errorf("%s Failed to get caches: %w", opts.IO.ColorScheme().FailureIcon(), err) + return fmt.Errorf("%s Failed to get caches: %w", cs.FailureIcon(), err) } if len(result.ActionsCaches) == 0 && opts.Exporter == nil { @@ -130,11 +131,11 @@ func listRun(opts *ListOptions) error { tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY", "SIZE", "CREATED", "ACCESSED")) for _, cache := range result.ActionsCaches { - tp.AddField(opts.IO.ColorScheme().Cyan(fmt.Sprintf("%d", cache.Id))) + tp.AddField(cs.Cyanf("%d", cache.Id)) tp.AddField(cache.Key) tp.AddField(humanFileSize(cache.SizeInBytes)) - tp.AddTimeField(opts.Now, cache.CreatedAt, opts.IO.ColorScheme().Gray) - tp.AddTimeField(opts.Now, cache.LastAccessedAt, opts.IO.ColorScheme().Gray) + tp.AddTimeField(opts.Now, cache.CreatedAt, cs.Muted) + tp.AddTimeField(opts.Now, cache.LastAccessedAt, cs.Muted) tp.EndRow() } diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index c1acccabb84..238c4a6a74b 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -152,7 +152,7 @@ func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Expo case false: nameColor = cs.Yellow case true: - nameColor = cs.Gray + nameColor = cs.Muted } tp.AddField(formattedName, tableprinter.WithColor(nameColor)) @@ -172,7 +172,7 @@ func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Expo if err != nil { return fmt.Errorf("error parsing date %q: %w", c.CreatedAt, err) } - tp.AddTimeField(time.Now(), ct, cs.Gray) + tp.AddTimeField(time.Now(), ct, cs.Muted) if hasNonProdVSCSTarget { tp.AddField(c.VSCSTarget) diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index d46d4da028a..a4fd245f688 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -206,7 +206,7 @@ func printTable(io *iostreams.IOStreams, gists []shared.Gist, filter *regexp.Reg tableprinter.WithColor(highlightFilesFunc(&gist)), ) tp.AddField(visibility, tableprinter.WithColor(visColor)) - tp.AddTimeField(time.Now(), gist.UpdatedAt, cs.Gray) + tp.AddTimeField(time.Now(), gist.UpdatedAt, cs.Muted) tp.EndRow() } diff --git a/pkg/cmd/gpg-key/list/list.go b/pkg/cmd/gpg-key/list/list.go index 9acf1d7b6df..4244ba34971 100644 --- a/pkg/cmd/gpg-key/list/list.go +++ b/pkg/cmd/gpg-key/list/list.go @@ -78,7 +78,7 @@ func listRun(opts *ListOptions) error { t.AddField(gpgKey.Emails.String()) t.AddField(gpgKey.KeyID) t.AddField(gpgKey.PublicKey, tableprinter.WithTruncate(truncateMiddle)) - t.AddTimeField(now, gpgKey.CreatedAt, cs.Gray) + t.AddTimeField(now, gpgKey.CreatedAt, cs.Muted) expiresAt := gpgKey.ExpiresAt.Format(time.RFC3339) if t.IsTTY() { if gpgKey.ExpiresAt.IsZero() { @@ -87,7 +87,7 @@ func listRun(opts *ListOptions) error { expiresAt = gpgKey.ExpiresAt.Format("2006-01-02") } } - t.AddField(expiresAt, tableprinter.WithColor(cs.Gray)) + t.AddField(expiresAt, tableprinter.WithColor(cs.Muted)) t.EndRow() } diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index ee708f161c1..0b86d5e1127 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -224,7 +224,7 @@ func listRun(opts *ListOptions) error { if !isTTY { table.AddField(shared.PrStateWithDraft(&pr)) } - table.AddTimeField(opts.Now(), pr.CreatedAt, cs.Gray) + table.AddTimeField(opts.Now(), pr.CreatedAt, cs.Muted) table.EndRow() } err = table.Render() diff --git a/pkg/cmd/release/list/list.go b/pkg/cmd/release/list/list.go index 24ec41840e1..dfc3ba8c1a6 100644 --- a/pkg/cmd/release/list/list.go +++ b/pkg/cmd/release/list/list.go @@ -88,7 +88,7 @@ func listRun(opts *ListOptions) error { } table := tableprinter.New(opts.IO, tableprinter.WithHeader("Title", "Type", "Tag name", "Published")) - iofmt := opts.IO.ColorScheme() + cs := opts.IO.ColorScheme() for _, rel := range releases { title := text.RemoveExcessiveWhitespace(rel.Name) if title == "" { @@ -100,13 +100,13 @@ func listRun(opts *ListOptions) error { var badgeColor func(string) string if rel.IsLatest { badge = "Latest" - badgeColor = iofmt.Green + badgeColor = cs.Green } else if rel.IsDraft { badge = "Draft" - badgeColor = iofmt.Red + badgeColor = cs.Red } else if rel.IsPrerelease { badge = "Pre-release" - badgeColor = iofmt.Yellow + badgeColor = cs.Yellow } table.AddField(badge, tableprinter.WithColor(badgeColor)) @@ -116,7 +116,7 @@ func listRun(opts *ListOptions) error { if rel.PublishedAt.IsZero() { pubDate = rel.CreatedAt } - table.AddTimeField(time.Now(), pubDate, iofmt.Gray) + table.AddTimeField(time.Now(), pubDate, cs.Muted) table.EndRow() } err = table.Render() diff --git a/pkg/cmd/repo/deploy-key/list/list.go b/pkg/cmd/repo/deploy-key/list/list.go index 0cf2861c196..79d00c9127f 100644 --- a/pkg/cmd/repo/deploy-key/list/list.go +++ b/pkg/cmd/repo/deploy-key/list/list.go @@ -90,7 +90,7 @@ func listRun(opts *ListOptions) error { } t.AddField(sshType) t.AddField(deployKey.Key, tableprinter.WithTruncate(truncateMiddle)) - t.AddTimeField(now, deployKey.CreatedAt, cs.Gray) + t.AddTimeField(now, deployKey.CreatedAt, cs.Muted) t.EndRow() } diff --git a/pkg/cmd/repo/list/list.go b/pkg/cmd/repo/list/list.go index 2cee2c480f2..1164392909a 100644 --- a/pkg/cmd/repo/list/list.go +++ b/pkg/cmd/repo/list/list.go @@ -181,7 +181,7 @@ func listRun(opts *ListOptions) error { totalMatchCount := len(listResult.Repositories) for _, repo := range listResult.Repositories { info := repoInfo(repo) - infoColor := cs.Gray + infoColor := cs.Muted if repo.IsPrivate { infoColor = cs.Yellow @@ -195,7 +195,7 @@ func listRun(opts *ListOptions) error { tp.AddField(repo.NameWithOwner, tableprinter.WithColor(cs.Bold)) tp.AddField(text.RemoveExcessiveWhitespace(repo.Description)) tp.AddField(info, tableprinter.WithColor(infoColor)) - tp.AddTimeField(opts.Now(), *t, cs.Gray) + tp.AddTimeField(opts.Now(), *t, cs.Muted) tp.EndRow() } diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index 7b18c391d50..7128ae25316 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -176,7 +176,7 @@ func listRun(opts *ListOptions) error { tp.AddField(string(run.Event)) tp.AddField(fmt.Sprintf("%d", run.ID), tableprinter.WithColor(cs.Cyan)) tp.AddField(run.Duration(opts.now).String()) - tp.AddTimeField(opts.now, run.StartedTime(), cs.Gray) + tp.AddTimeField(opts.now, run.StartedTime(), cs.Muted) tp.EndRow() } diff --git a/pkg/cmd/ssh-key/list/list.go b/pkg/cmd/ssh-key/list/list.go index eebc82f908f..f69e57ea367 100644 --- a/pkg/cmd/ssh-key/list/list.go +++ b/pkg/cmd/ssh-key/list/list.go @@ -89,11 +89,11 @@ func listRun(opts *ListOptions) error { t.AddField(id) t.AddField(sshKey.Key, tableprinter.WithTruncate(truncateMiddle)) t.AddField(sshKey.Type) - t.AddTimeField(now, sshKey.CreatedAt, cs.Gray) + t.AddTimeField(now, sshKey.CreatedAt, cs.Muted) } else { t.AddField(sshKey.Title) t.AddField(sshKey.Key) - t.AddTimeField(now, sshKey.CreatedAt, cs.Gray) + t.AddTimeField(now, sshKey.CreatedAt, cs.Muted) t.AddField(id) t.AddField(sshKey.Type) } From 3c38cedaf3949018cbcbab63fc7c280481e39fa0 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 31 Mar 2025 15:33:52 -0400 Subject: [PATCH 032/116] Implement tests for muted logic, standardize reset This commit covers testing around the new ColorScheme.Muted logic based on various situations to gain confidence we get the accessible colors expected when enabled. Additionally, this commit includes a small change to the existing 8-bit color logic to standardize on the same reset sequence for testing purposes. Essentially, `ESC[m` and `ESC[0m` are equivalent but this inconsistency with our other libraries makes setting up tests a little extra confusing and difficult. --- pkg/cmd/gist/list/list_test.go | 6 ++-- pkg/iostreams/color.go | 6 ++-- pkg/iostreams/color_test.go | 64 ++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 4f6c8a9f7e2..ad6d8cdb5f4 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -487,8 +487,8 @@ func Test_listRun(t *testing.T) { }, wantOut: heredoc.Docf(` %[1]s[0;4;39mID %[1]s[0m %[1]s[0;4;39mDESCRIPTION %[1]s[0m %[1]s[0;4;39mFILES %[1]s[0m %[1]s[0;4;39mVISIBILITY%[1]s[0m %[1]s[0;4;39mUPDATED %[1]s[0m - 1234 %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m 1 file %[1]s[0;32mpublic %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[m - 2345 %[1]s[0;1;39mmatch in the file name %[1]s[0m %[1]s[0;30;43m2 files%[1]s[0m %[1]s[0;31msecret %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[m + 1234 %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m 1 file %[1]s[0;32mpublic %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[0m + 2345 %[1]s[0;1;39mmatch in the file name %[1]s[0m %[1]s[0;30;43m2 files%[1]s[0m %[1]s[0;31msecret %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[0m `, "\x1b"), }, { @@ -694,7 +694,7 @@ func Test_highlightMatch(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cs := iostreams.NewColorScheme(tt.color, false, false, iostreams.NoTheme) + cs := iostreams.NewColorScheme(tt.color, false, false, false, iostreams.NoTheme) matched := false got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 3e2d08ac21d..9e6c4f7ca3d 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -37,7 +37,7 @@ var ( noThemeTableHeader = ansi.ColorFunc("default+u") gray256 = func(t string) string { - return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t) + return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[0m", 38, 242, t) } ) @@ -91,8 +91,10 @@ func (c *ColorScheme) Muted(t string) string { switch c.theme { case LightTheme: return lightThemeMuted(t) - default: + case DarkTheme: return darkThemeMuted(t) + default: + return t } } diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index a399e6d589c..b0cf994e395 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -175,3 +175,67 @@ func TestTableHeader(t *testing.T) { }) } } + +func TestMuted(t *testing.T) { + reset := "\x1b[0m" + gray4bit := "\x1b[0;90m" + gray8bit := "\x1b[38;5;242m" + brightBlack4bit := "\x1b[0;90m" + dimBlack4bit := "\x1b[0;2;37m" + + tests := []struct { + name string + cs *ColorScheme + input string + expected string + }{ + { + name: "when color is disabled but accessible colors are disabled, text is not stylized", + cs: NewColorScheme(false, false, false, false, NoTheme), + input: "this should not be stylized", + expected: "this should not be stylized", + }, + { + name: "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used", + cs: NewColorScheme(true, false, false, false, NoTheme), + input: "this should be 4-bit gray", + expected: fmt.Sprintf("%sthis should be 4-bit gray%s", gray4bit, reset), + }, + { + name: "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", + cs: NewColorScheme(true, true, false, false, NoTheme), + input: "this should be 8-bit gray", + expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset), + }, + { + name: "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", + cs: NewColorScheme(true, true, true, false, NoTheme), + input: "this should be 8-bit gray", + expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset), + }, + { + name: "when 4-bit color is enabled and theme is dark, 4-bit light color is used", + cs: NewColorScheme(true, true, true, true, DarkTheme), + input: "this should be 4-bit dim black", + expected: fmt.Sprintf("%sthis should be 4-bit dim black%s", dimBlack4bit, reset), + }, + { + name: "when 4-bit color is enabled and theme is light, 4-bit dark color is used", + cs: NewColorScheme(true, true, true, true, LightTheme), + input: "this should be 4-bit bright black", + expected: fmt.Sprintf("%sthis should be 4-bit bright black%s", brightBlack4bit, reset), + }, + { + name: "when 4-bit color is enabled but no theme, 4-bit default color is used", + cs: NewColorScheme(true, true, true, true, NoTheme), + input: "this should have no explicit color", + expected: "this should have no explicit color", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.cs.Muted(tt.input)) + }) + } +} From de86cc6c150d1ec969f935bf85b3ae8a7046f42f Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 31 Mar 2025 15:57:37 -0400 Subject: [PATCH 033/116] Remove out of scope configuration setting change --- internal/config/config.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index a3849b9afca..350163cba59 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -534,8 +534,6 @@ aliases: http_unix_socket: # What web browser gh should use when opening URLs. If blank, will refer to environment. browser: -# Preference for accessible colors that can be customized. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled -accessible_colors: disabled ` type ConfigOption struct { From 1bf1ad282b5bbdbe8c3ebac55e8632cfc2cf5d09 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 31 Mar 2025 15:58:37 -0400 Subject: [PATCH 034/116] Remove configuration setting vestage --- internal/config/config.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 350163cba59..476154d667e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,11 +12,9 @@ import ( o "github.com/cli/cli/v2/pkg/option" ghauth "github.com/cli/go-gh/v2/pkg/auth" ghConfig "github.com/cli/go-gh/v2/pkg/config" - xcolor "github.com/cli/go-gh/v2/pkg/x/color" ) const ( - accessibleColorsKey = xcolor.AccessibleColorsSetting aliasesKey = "aliases" browserKey = "browser" editorKey = "editor" From e36d795629ba1e3f6a51fdb6a0718037a08b4d0b Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 31 Mar 2025 15:59:18 -0400 Subject: [PATCH 035/116] Regenerate mocks --- internal/gh/mock/config.go | 44 -------------------------------------- 1 file changed, 44 deletions(-) diff --git a/internal/gh/mock/config.go b/internal/gh/mock/config.go index ecb8814b8e7..569af1facb4 100644 --- a/internal/gh/mock/config.go +++ b/internal/gh/mock/config.go @@ -19,9 +19,6 @@ var _ gh.Config = &ConfigMock{} // // // make and configure a mocked gh.Config // mockedConfig := &ConfigMock{ -// AccessibleColorsFunc: func(hostname string) gh.ConfigEntry { -// panic("mock out the AccessibleColors method") -// }, // AliasesFunc: func() gh.AliasConfig { // panic("mock out the Aliases method") // }, @@ -74,9 +71,6 @@ var _ gh.Config = &ConfigMock{} // // } type ConfigMock struct { - // AccessibleColorsFunc mocks the AccessibleColors method. - AccessibleColorsFunc func(hostname string) gh.ConfigEntry - // AliasesFunc mocks the Aliases method. AliasesFunc func() gh.AliasConfig @@ -124,11 +118,6 @@ type ConfigMock struct { // calls tracks calls to the methods. calls struct { - // AccessibleColors holds details about calls to the AccessibleColors method. - AccessibleColors []struct { - // Hostname is the hostname argument value. - Hostname string - } // Aliases holds details about calls to the Aliases method. Aliases []struct { } @@ -201,7 +190,6 @@ type ConfigMock struct { Write []struct { } } - lockAccessibleColors sync.RWMutex lockAliases sync.RWMutex lockAuthentication sync.RWMutex lockBrowser sync.RWMutex @@ -219,38 +207,6 @@ type ConfigMock struct { lockWrite sync.RWMutex } -// AccessibleColors calls AccessibleColorsFunc. -func (mock *ConfigMock) AccessibleColors(hostname string) gh.ConfigEntry { - if mock.AccessibleColorsFunc == nil { - panic("ConfigMock.AccessibleColorsFunc: method is nil but Config.AccessibleColors was just called") - } - callInfo := struct { - Hostname string - }{ - Hostname: hostname, - } - mock.lockAccessibleColors.Lock() - mock.calls.AccessibleColors = append(mock.calls.AccessibleColors, callInfo) - mock.lockAccessibleColors.Unlock() - return mock.AccessibleColorsFunc(hostname) -} - -// AccessibleColorsCalls gets all the calls that were made to AccessibleColors. -// Check the length with: -// -// len(mockedConfig.AccessibleColorsCalls()) -func (mock *ConfigMock) AccessibleColorsCalls() []struct { - Hostname string -} { - var calls []struct { - Hostname string - } - mock.lockAccessibleColors.RLock() - calls = mock.calls.AccessibleColors - mock.lockAccessibleColors.RUnlock() - return calls -} - // Aliases calls AliasesFunc. func (mock *ConfigMock) Aliases() gh.AliasConfig { if mock.AliasesFunc == nil { From b3c8c70cba824ea4d0ded0c47fcb88838d58a5e8 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 31 Mar 2025 17:00:40 -0400 Subject: [PATCH 036/116] Remove deprecated note on gray color functions Without fixing all ColorScheme.Gray and ColorScheme.Grayf usage in this pull request, golangci-lint throws errors for using deprecated functions. As that code should be replaced within github/cli#833, I'm removing the deprecation indicator for now to get this PR passing. --- pkg/iostreams/color.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 9e6c4f7ca3d..58d13e6ef55 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -142,7 +142,7 @@ func (c *ColorScheme) GreenBold(t string) string { return greenBold(t) } -// Deprecated: Use Muted instead for thematically contrasting color. +// Use Muted instead for thematically contrasting color. func (c *ColorScheme) Gray(t string) string { if !c.enabled { return t @@ -153,7 +153,7 @@ func (c *ColorScheme) Gray(t string) string { return gray(t) } -// Deprecated: Use Mutedf instead for thematically contrasting color. +// Use Mutedf instead for thematically contrasting color. func (c *ColorScheme) Grayf(t string, args ...interface{}) string { return c.Gray(fmt.Sprintf(t, args...)) } From 202c1ad16b78aa681cbe552164f2e9a9a7e574fa Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:04:57 -0600 Subject: [PATCH 037/116] feat(prompter): add accessible prompter support --- go.mod | 12 ++ go.sum | 29 +++- internal/prompter/prompter.go | 233 ++++++++++++++++++++++++++++- internal/prompter/prompter_test.go | 63 ++++++++ 4 files changed, 329 insertions(+), 8 deletions(-) create mode 100644 internal/prompter/prompter_test.go diff --git a/go.mod b/go.mod index bea712a2d54..f64430db70d 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,9 @@ require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/briandowns/spinner v1.18.1 github.com/cenkalti/backoff/v4 v4.3.0 + github.com/charmbracelet/bubbletea v1.3.4 github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 + github.com/charmbracelet/huh v0.6.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc github.com/cli/go-gh/v2 v2.12.0 github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 @@ -64,12 +66,16 @@ require ( github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/alessio/shellescape v1.4.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/atotto/clipboard v0.1.4 // 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 + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cli/browser v1.3.0 // indirect github.com/cli/shurcooL-graphql v0.0.4 // indirect @@ -82,6 +88,8 @@ require ( github.com/docker/cli v27.5.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect @@ -118,12 +126,16 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/oklog/ulid v1.3.1 // indirect diff --git a/go.sum b/go.sum index 2b5a3121206..8ee1dcb0949 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 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/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo= @@ -95,22 +97,32 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25SvI2WiZdt/gxINcYBnHD7PE2Vr9auqwg5B05g= github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys= github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= @@ -160,6 +172,10 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -340,6 +356,8 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -356,10 +374,16 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= @@ -550,6 +574,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 1d4b11cbc95..7909bc8fd26 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -2,9 +2,12 @@ package prompter import ( "fmt" + "os" "strings" "github.com/AlecAivazis/survey/v2" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/pkg/surveyext" ghPrompter "github.com/cli/go-gh/v2/pkg/prompter" @@ -27,15 +30,233 @@ type Prompter interface { } func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { - return &surveyPrompter{ - prompter: ghPrompter.New(stdin, stdout, stderr), - stdin: stdin, - stdout: stdout, - stderr: stderr, - editorCmd: editorCmd, + accessiblePrompterValue := os.Getenv("GH_SCREENREADER_FRIENDLY") + switch accessiblePrompterValue { + case "", "false", "0": + return &surveyPrompter{ + prompter: ghPrompter.New(stdin, stdout, stderr), + stdin: stdin, + stdout: stdout, + stderr: stderr, + editorCmd: editorCmd, + } + default: + return &huhPrompter{ + stdin: stdin, + stdout: stdout, + stderr: stderr, + editorCmd: editorCmd, + accessible: true, + } } } +type huhPrompter struct { + stdin ghPrompter.FileReader + stdout ghPrompter.FileWriter + stderr ghPrompter.FileWriter + editorCmd string + accessible bool +} + +// IsAccessible returns true if the huhPrompter was created in accessible mode. +func (p *huhPrompter) IsAccessible() bool { + return p.accessible +} + +func (p *huhPrompter) newForm(groups ...*huh.Group) *huh.Form { + return huh.NewForm(groups...). + WithTheme(huh.ThemeBase16()). + WithAccessible(p.accessible). + WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin)) +} + +func (p *huhPrompter) Select(prompt, _ string, options []string) (int, error) { + var result int + formOptions := []huh.Option[int]{} + for i, o := range options { + formOptions = append(formOptions, huh.NewOption(o, i)) + } + + form := p.newForm( + huh.NewGroup( + huh.NewSelect[int](). + Title(prompt). + Value(&result). + Options(formOptions...), + ), + ) + + err := form.Run() + return result, err +} + +func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { + var result []int + formOptions := make([]huh.Option[int], len(options)) + for i, o := range options { + formOptions[i] = huh.NewOption(o, i) + } + + form := p.newForm( + huh.NewGroup( + huh.NewMultiSelect[int](). + Title(prompt). + Value(&result). + Limit(len(options)). + Options(formOptions...), + ), + ) + + if err := form.Run(); err != nil { + return nil, err + } + + mid := len(result) / 2 + return result[:mid], nil +} + +func (p *huhPrompter) Input(prompt, defaultValue string) (string, error) { + result := defaultValue + form := p.newForm( + huh.NewGroup( + huh.NewInput(). + Title(prompt). + Value(&result), + ), + ) + + err := form.Run() + return result, err +} + +func (p *huhPrompter) Password(prompt string) (string, error) { + var result string + form := p.newForm( + huh.NewGroup( + huh.NewInput(). + Title(prompt). + Value(&result), + // This doesn't have any effect in accessible mode. + // EchoMode(huh.EchoModePassword), + ), + ) + + err := form.Run() + return result, err +} + +func (p *huhPrompter) Confirm(prompt string, _ bool) (bool, error) { + var result bool + form := p.newForm( + huh.NewGroup( + huh.NewConfirm(). + Title(prompt). + Value(&result), + ), + ) + if err := form.Run(); err != nil { + return false, err + } + return result, nil +} + +func (p *huhPrompter) AuthToken() (string, error) { + var result string + form := p.newForm( + huh.NewGroup( + huh.NewInput(). + Title("Paste your authentication token:"). + Validate(func(input string) error { + if input == "" { + return fmt.Errorf("token is required") + } + return nil + }). + Value(&result), + // This doesn't have any effect in accessible mode. + // EchoMode(huh.EchoModePassword), + ), + ) + + err := form.Run() + return result, err +} + +func (p *huhPrompter) ConfirmDeletion(requiredValue string) error { + var result string + form := p.newForm( + huh.NewGroup( + huh.NewInput(). + Title(fmt.Sprintf("Type %q to confirm deletion", requiredValue)). + Validate(func(input string) error { + if input != requiredValue { + return fmt.Errorf("You entered: %q", input) + } + return nil + }). + Value(&result), + // This doesn't have any effect in accessible mode. + // EchoMode(huh.EchoModePassword), + ), + ) + + return form.Run() +} + +func (p *huhPrompter) InputHostname() (string, error) { + var result string + form := p.newForm( + huh.NewGroup( + huh.NewInput(). + Title("Hostname:"). + Validate(ghinstance.HostnameValidator). + Value(&result), + ), + ) + + err := form.Run() + return result, err +} + +func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { + var result string + form := p.newForm( + huh.NewGroup( + huh.NewSelect[string](). + Title(prompt). + Options( + huh.NewOption("Open Editor", "open"), + huh.NewOption("Skip", "skip"), + ). + Value(&result), + ), + ) + if err := form.Run(); err != nil { + return "", err + } + + if result == "skip" { + // TODO: loop if blank not allowed + if !blankAllowed && defaultValue == "" { + panic("blank not allowed and no default value") + } + return "", nil + } + + text, err := surveyext.Edit(p.editorCmd, "*.md", defaultValue, p.stdin, p.stdout, p.stderr) + if err != nil { + return "", err + } + + // TODO: blank not allowed + if !blankAllowed && defaultValue == "" { + panic("blank not allowed and no default value") + } + + return text, nil +} + type surveyPrompter struct { prompter *ghPrompter.Prompter stdin ghPrompter.FileReader diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go new file mode 100644 index 00000000000..99b8996ca3b --- /dev/null +++ b/internal/prompter/prompter_test.go @@ -0,0 +1,63 @@ +package prompter + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" +) + +func TestNewReturnsAccessiblePrompter(t *testing.T) { + editorCmd := "nothing" + ios, _, _, _ := iostreams.Test() + stdin := ios.In + stdout := ios.Out + stderr := ios.ErrOut + + t.Run("returns accessible huhPrompter when GH_SCREENREADER_FRIENDLY is set to true", func(t *testing.T) { + t.Parallel() + t.Setenv("GH_SCREENREADER_FRIENDLY", "true") + + p := New(editorCmd, stdin, stdout, stderr) + + assert.IsType(t, &huhPrompter{}, p, "expected huhPrompter to be returned") + assert.Equal(t, p.(*huhPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") + }) + + t.Run("returns accessible huhPrompter when GH_SCREENREADER_FRIENDLY is set to 1", func(t *testing.T) { + t.Parallel() + t.Setenv("GH_SCREENREADER_FRIENDLY", "1") + + p := New(editorCmd, stdin, stdout, stderr) + + assert.IsType(t, &huhPrompter{}, p, "expected huhPrompter to be returned") + assert.Equal(t, p.(*huhPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") + }) + + t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to false", func(t *testing.T) { + t.Parallel() + t.Setenv("GH_SCREENREADER_FRIENDLY", "false") + + p := New(editorCmd, stdin, stdout, stderr) + + assert.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") + }) + + t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to 0", func(t *testing.T) { + t.Parallel() + t.Setenv("GH_SCREENREADER_FRIENDLY", "0") + + p := New(editorCmd, stdin, stdout, stderr) + + assert.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") + }) + + t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is unset", func(t *testing.T) { + t.Parallel() + t.Setenv("GH_SCREENREADER_FRIENDLY", "") + + p := New(editorCmd, stdin, stdout, stderr) + + assert.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") + }) +} From 7b0c09541ddb41f8b355160bb413ede9eac42b8a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:52:26 -0600 Subject: [PATCH 038/116] feat(md prompter): md prompt respects blankAllowed Accessible prompter now respects blankAllowed and will not prompt for "skip" if blankAllowed is false. --- internal/prompter/prompter.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 7909bc8fd26..c9af35fbf59 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -221,23 +221,27 @@ func (p *huhPrompter) InputHostname() (string, error) { func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { var result string + options := []huh.Option[string]{ + huh.NewOption("Open Editor", "open"), + } + if blankAllowed { + options = append(options, huh.NewOption("Skip", "skip")) + } + form := p.newForm( huh.NewGroup( huh.NewSelect[string](). Title(prompt). - Options( - huh.NewOption("Open Editor", "open"), - huh.NewOption("Skip", "skip"), - ). + Options(options...). Value(&result), ), ) + if err := form.Run(); err != nil { return "", err } if result == "skip" { - // TODO: loop if blank not allowed if !blankAllowed && defaultValue == "" { panic("blank not allowed and no default value") } From e973ee332dff7bcee46a99ab61fe5ff97dd9c12b Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:58:20 -0600 Subject: [PATCH 039/116] fix(md prompter): accessible prompt allows blank Allow the accessible markdownEditor prompt to be blank when the blank comes from the result of an interactive session with an editor, even when blankAllowed is false. This behavior aligns the accessible prompter with the behavior of the current standard prompter. --- internal/prompter/prompter.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index c9af35fbf59..3e37834db69 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -253,11 +253,6 @@ func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed b return "", err } - // TODO: blank not allowed - if !blankAllowed && defaultValue == "" { - panic("blank not allowed and no default value") - } - return text, nil } From 92b1a8e0f04ba7772ef538048a433f27a1a18475 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:01:16 -0600 Subject: [PATCH 040/116] test(prompter): remove t.parallel calls t.Parallel() cannot be used when env vars are being set. --- internal/prompter/prompter_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go index 99b8996ca3b..f0084e8feff 100644 --- a/internal/prompter/prompter_test.go +++ b/internal/prompter/prompter_test.go @@ -15,7 +15,6 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { stderr := ios.ErrOut t.Run("returns accessible huhPrompter when GH_SCREENREADER_FRIENDLY is set to true", func(t *testing.T) { - t.Parallel() t.Setenv("GH_SCREENREADER_FRIENDLY", "true") p := New(editorCmd, stdin, stdout, stderr) @@ -25,7 +24,6 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { }) t.Run("returns accessible huhPrompter when GH_SCREENREADER_FRIENDLY is set to 1", func(t *testing.T) { - t.Parallel() t.Setenv("GH_SCREENREADER_FRIENDLY", "1") p := New(editorCmd, stdin, stdout, stderr) @@ -35,7 +33,6 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to false", func(t *testing.T) { - t.Parallel() t.Setenv("GH_SCREENREADER_FRIENDLY", "false") p := New(editorCmd, stdin, stdout, stderr) @@ -44,7 +41,6 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to 0", func(t *testing.T) { - t.Parallel() t.Setenv("GH_SCREENREADER_FRIENDLY", "0") p := New(editorCmd, stdin, stdout, stderr) @@ -53,7 +49,6 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is unset", func(t *testing.T) { - t.Parallel() t.Setenv("GH_SCREENREADER_FRIENDLY", "") p := New(editorCmd, stdin, stdout, stderr) From 88a98ea63a2694765c73b8887ca2ff905cb4c2c3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:58:10 -0600 Subject: [PATCH 041/116] feat(prompter): include `no` as false-y value Co-authored-by: Andy Feller --- internal/prompter/prompter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 3e37834db69..abadda3dc55 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -32,7 +32,7 @@ type Prompter interface { func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { accessiblePrompterValue := os.Getenv("GH_SCREENREADER_FRIENDLY") switch accessiblePrompterValue { - case "", "false", "0": + case "", "false", "0", "no": return &surveyPrompter{ prompter: ghPrompter.New(stdin, stdout, stderr), stdin: stdin, From f7de9e0c1198ef5cc932a2483d2e4230741a6abb Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 2 Apr 2025 12:53:44 -0600 Subject: [PATCH 042/116] test(prompter): `go-expect` based prompter tests --- go.mod | 2 + internal/prompter/prompter_test.go | 294 +++++++++++++++++++++++++++++ 2 files changed, 296 insertions(+) diff --git a/go.mod b/go.mod index f64430db70d..85b0f88d4b6 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.23.5 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/MakeNowJust/heredoc v1.0.0 + github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/briandowns/spinner v1.18.1 github.com/cenkalti/backoff/v4 v4.3.0 github.com/charmbracelet/bubbletea v1.3.4 @@ -31,6 +32,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 github.com/henvic/httpretty v0.1.4 + github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec github.com/in-toto/attestation v1.1.1 github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go index f0084e8feff..1e66d74716a 100644 --- a/internal/prompter/prompter_test.go +++ b/internal/prompter/prompter_test.go @@ -1,10 +1,19 @@ package prompter import ( + "fmt" + "io" + "os" + "strings" "testing" + "time" + "github.com/Netflix/go-expect" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/creack/pty" + "github.com/hinshun/vt10x" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewReturnsAccessiblePrompter(t *testing.T) { @@ -56,3 +65,288 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { assert.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") }) } + +func TestAccessibleHuhprompter(t *testing.T) { + // Create a PTY and hook up a virtual terminal emulator + ptm, pts, err := pty.Open() + require.NoError(t, err) + + term := vt10x.New(vt10x.WithWriter(pts)) + + // Create a console via Expect that allows scripting against the terminal + consoleOpts := []expect.ConsoleOpt{ + expect.WithStdin(ptm), + expect.WithStdout(term), + expect.WithCloser(ptm, pts), + failOnExpectError(t), + failOnSendError(t), + expect.WithDefaultTimeout(time.Second * 600), + } + + console, err := expect.NewConsole(consoleOpts...) + require.NoError(t, err) + t.Cleanup(func() { testCloser(t, console) }) + + p := &huhPrompter{ + editorCmd: "", // intentionally empty to cause a failure. + accessible: true, + } + + // Using OS here because huh currently ignores configured iostreams + // See https://github.com/charmbracelet/huh/issues/612 + stdIn := os.Stdin + stdOut := os.Stdout + stdErr := os.Stderr + + t.Cleanup(func() { + os.Stdin = stdIn + os.Stdout = stdOut + os.Stderr = stdErr + }) + + os.Stdin = console.Tty() + os.Stdout = console.Tty() + os.Stderr = console.Tty() + + t.Run("Select", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Choose:") + require.NoError(t, err) + + // Select option 1 + _, err = console.SendLine("1") + require.NoError(t, err) + }() + + selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"}) + require.NoError(t, err) + + assert.Equal(t, 0, selectValue) + }) + + t.Run("MultiSelect", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Select a number") + require.NoError(t, err) + + // Select options 1 and 2 + _, err = console.SendLine("1") + require.NoError(t, err) + _, err = console.SendLine("2") + require.NoError(t, err) + + // This confirms selections + _, err = console.SendLine("0") + require.NoError(t, err) + }() + + multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"}) + require.NoError(t, err) + + assert.Equal(t, []int{0, 1}, multiSelectValue) + }) + + t.Run("Input", func(t *testing.T) { + dummyText := "12345abcdefg" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Enter some characters") + require.NoError(t, err) + + // Enter a number + _, err = console.SendLine(dummyText) + require.NoError(t, err) + }() + + inputValue, err := p.Input("Enter some characters", "") + require.NoError(t, err) + + assert.Equal(t, dummyText, inputValue) + }) + + t.Run("Password", func(t *testing.T) { + dummyPassword := "12345abcdefg" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Enter password") + require.NoError(t, err) + + // Enter a number + _, err = console.SendLine(dummyPassword) + require.NoError(t, err) + }() + + passwordValue, err := p.Password("Enter password") + require.NoError(t, err) + require.Equal(t, dummyPassword, passwordValue) + }) + + t.Run("Confirm", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Are you sure") + require.NoError(t, err) + + // Confirm + _, err = console.SendLine("y") + require.NoError(t, err) + }() + + confirmValue, err := p.Confirm("Are you sure", false) + require.NoError(t, err) + require.Equal(t, true, confirmValue) + }) + + t.Run("AuthToken", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Paste your authentication token:") + require.NoError(t, err) + + // Enter a number + _, err = console.SendLine("12345abcdefg") + require.NoError(t, err) + }() + + authValue, err := p.AuthToken() + require.NoError(t, err) + require.Equal(t, "12345abcdefg", authValue) + }) + + t.Run("ConfirmDeletion", func(t *testing.T) { + requiredValue := "test" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue)) + require.NoError(t, err) + + // Confirm + _, err = console.SendLine(requiredValue) + require.NoError(t, err) + }() + + // An err indicates that the confirmation text sent did not match + err := p.ConfirmDeletion(requiredValue) + require.NoError(t, err) + }) + + t.Run("InputHostname", func(t *testing.T) { + var inputValue string + hostname := "somethingdoesnotmatter.com" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Hostname:") + require.NoError(t, err) + + // Enter the hostname + _, err = console.SendLine(hostname) + require.NoError(t, err) + }() + + inputValue, err := p.InputHostname() + require.NoError(t, err) + require.Equal(t, hostname, inputValue) + }) + + t.Run("MarkdownEditor - blank allowed", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("How to edit?") + require.NoError(t, err) + + // Enter 2, to select "skip" + _, err = console.SendLine("2") + require.NoError(t, err) + }() + + inputValue, err := p.MarkdownEditor("How to edit?", "", true) + require.NoError(t, err) + require.Equal(t, "", inputValue) + }) + + t.Run("MarkdownEditor - blank disallowed", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("How to edit?") + require.NoError(t, err) + + // Enter number 2 to select "skip". This shoudln't be allowed. + _, err = console.SendLine("2") + require.NoError(t, err) + + // Expect a notice to enter something valid since blank is disallowed. + _, err = console.ExpectString("invalid input. please try again") + require.NoError(t, err) + + // Send a 1 to select to open the editor. + // Sending the input won't fail, so we expect no error here. + // See below though, since we expect the editor to fail to open. + _, err = console.SendLine("1") + require.NoError(t, err) + }() + + // However, here we do expect an error because the editor program + // is intentionally empty and will fail. + inputValue, err := p.MarkdownEditor("How to edit?", "", false) + require.Error(t, err) + require.Equal(t, "", inputValue) + }) +} + +// failOnExpectError adds an observer that will fail the test in a standardised way +// if any expectation on the command output fails, without requiring an explicit +// assertion. +// +// Use WithRelaxedIO to disable this behaviour. +func failOnExpectError(t testing.TB) expect.ConsoleOpt { + t.Helper() + return expect.WithExpectObserver( + func(matchers []expect.Matcher, buf string, err error) { + t.Helper() + + if err == nil { + return + } + + if len(matchers) == 0 { + t.Fatalf("Error occurred while matching %q: %s\n", buf, err) + } + + var criteria []string + for _, matcher := range matchers { + criteria = append(criteria, fmt.Sprintf("%q", matcher.Criteria())) + } + t.Fatalf("Failed to find [%s] in %q: %s\n", strings.Join(criteria, ", "), buf, err) + }, + ) +} + +// failOnSendError adds an observer that will fail the test in a standardised way +// if any sending of input fails, without requiring an explicit assertion. +// +// Use WithRelaxedIO to disable this behaviour. +func failOnSendError(t testing.TB) expect.ConsoleOpt { + t.Helper() + return expect.WithSendObserver( + func(msg string, n int, err error) { + t.Helper() + + if err != nil { + t.Fatalf("Failed to send %q: %s\n", msg, err) + } + if len(msg) != n { + t.Fatalf("Only sent %d of %d bytes for %q\n", n, len(msg), msg) + } + }, + ) +} + +// testCloser is a helper to fail the test if a Closer fails to close. +func testCloser(t testing.TB, closer io.Closer) { + t.Helper() + if err := closer.Close(); err != nil { + t.Errorf("Close failed: %s", err) + } +} From 94bbd26aab390c3f8929b040ec229484b9685bd3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 2 Apr 2025 12:54:35 -0600 Subject: [PATCH 043/116] fix(prompter): rename huhprompter --- internal/prompter/prompter.go | 26 +++++++++++++------------- internal/prompter/prompter_test.go | 10 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index abadda3dc55..93d1d34c009 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -41,7 +41,7 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr editorCmd: editorCmd, } default: - return &huhPrompter{ + return &SpeechSynthesizerFriendlyPrompter{ stdin: stdin, stdout: stdout, stderr: stderr, @@ -51,7 +51,7 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr } } -type huhPrompter struct { +type SpeechSynthesizerFriendlyPrompter struct { stdin ghPrompter.FileReader stdout ghPrompter.FileWriter stderr ghPrompter.FileWriter @@ -60,18 +60,18 @@ type huhPrompter struct { } // IsAccessible returns true if the huhPrompter was created in accessible mode. -func (p *huhPrompter) IsAccessible() bool { +func (p *SpeechSynthesizerFriendlyPrompter) IsAccessible() bool { return p.accessible } -func (p *huhPrompter) newForm(groups ...*huh.Group) *huh.Form { +func (p *SpeechSynthesizerFriendlyPrompter) newForm(groups ...*huh.Group) *huh.Form { return huh.NewForm(groups...). WithTheme(huh.ThemeBase16()). WithAccessible(p.accessible). WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin)) } -func (p *huhPrompter) Select(prompt, _ string, options []string) (int, error) { +func (p *SpeechSynthesizerFriendlyPrompter) Select(prompt, _ string, options []string) (int, error) { var result int formOptions := []huh.Option[int]{} for i, o := range options { @@ -91,7 +91,7 @@ func (p *huhPrompter) Select(prompt, _ string, options []string) (int, error) { return result, err } -func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { +func (p *SpeechSynthesizerFriendlyPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { var result []int formOptions := make([]huh.Option[int], len(options)) for i, o := range options { @@ -116,7 +116,7 @@ func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []st return result[:mid], nil } -func (p *huhPrompter) Input(prompt, defaultValue string) (string, error) { +func (p *SpeechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) (string, error) { result := defaultValue form := p.newForm( huh.NewGroup( @@ -130,7 +130,7 @@ func (p *huhPrompter) Input(prompt, defaultValue string) (string, error) { return result, err } -func (p *huhPrompter) Password(prompt string) (string, error) { +func (p *SpeechSynthesizerFriendlyPrompter) Password(prompt string) (string, error) { var result string form := p.newForm( huh.NewGroup( @@ -146,7 +146,7 @@ func (p *huhPrompter) Password(prompt string) (string, error) { return result, err } -func (p *huhPrompter) Confirm(prompt string, _ bool) (bool, error) { +func (p *SpeechSynthesizerFriendlyPrompter) Confirm(prompt string, _ bool) (bool, error) { var result bool form := p.newForm( huh.NewGroup( @@ -161,7 +161,7 @@ func (p *huhPrompter) Confirm(prompt string, _ bool) (bool, error) { return result, nil } -func (p *huhPrompter) AuthToken() (string, error) { +func (p *SpeechSynthesizerFriendlyPrompter) AuthToken() (string, error) { var result string form := p.newForm( huh.NewGroup( @@ -183,7 +183,7 @@ func (p *huhPrompter) AuthToken() (string, error) { return result, err } -func (p *huhPrompter) ConfirmDeletion(requiredValue string) error { +func (p *SpeechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string) error { var result string form := p.newForm( huh.NewGroup( @@ -204,7 +204,7 @@ func (p *huhPrompter) ConfirmDeletion(requiredValue string) error { return form.Run() } -func (p *huhPrompter) InputHostname() (string, error) { +func (p *SpeechSynthesizerFriendlyPrompter) InputHostname() (string, error) { var result string form := p.newForm( huh.NewGroup( @@ -219,7 +219,7 @@ func (p *huhPrompter) InputHostname() (string, error) { return result, err } -func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { +func (p *SpeechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { var result string options := []huh.Option[string]{ huh.NewOption("Open Editor", "open"), diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go index 1e66d74716a..e610b3a7c33 100644 --- a/internal/prompter/prompter_test.go +++ b/internal/prompter/prompter_test.go @@ -28,8 +28,8 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - assert.IsType(t, &huhPrompter{}, p, "expected huhPrompter to be returned") - assert.Equal(t, p.(*huhPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") + assert.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected huhPrompter to be returned") + assert.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") }) t.Run("returns accessible huhPrompter when GH_SCREENREADER_FRIENDLY is set to 1", func(t *testing.T) { @@ -37,8 +37,8 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - assert.IsType(t, &huhPrompter{}, p, "expected huhPrompter to be returned") - assert.Equal(t, p.(*huhPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") + assert.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected huhPrompter to be returned") + assert.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to false", func(t *testing.T) { @@ -87,7 +87,7 @@ func TestAccessibleHuhprompter(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { testCloser(t, console) }) - p := &huhPrompter{ + p := &SpeechSynthesizerFriendlyPrompter{ editorCmd: "", // intentionally empty to cause a failure. accessible: true, } From 0d7fd36f11ead4d35d09b51a00ba70ac54d5131d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 2 Apr 2025 12:58:44 -0600 Subject: [PATCH 044/116] test(prompter): replace assert with require --- internal/prompter/prompter_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go index e610b3a7c33..debaa449602 100644 --- a/internal/prompter/prompter_test.go +++ b/internal/prompter/prompter_test.go @@ -28,8 +28,8 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - assert.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected huhPrompter to be returned") - assert.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") + require.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected huhPrompter to be returned") + require.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") }) t.Run("returns accessible huhPrompter when GH_SCREENREADER_FRIENDLY is set to 1", func(t *testing.T) { @@ -37,8 +37,8 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - assert.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected huhPrompter to be returned") - assert.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") + require.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected huhPrompter to be returned") + require.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to false", func(t *testing.T) { @@ -46,7 +46,7 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - assert.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") + require.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to 0", func(t *testing.T) { @@ -54,7 +54,7 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - assert.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") + require.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is unset", func(t *testing.T) { @@ -62,7 +62,7 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - assert.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") + require.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") }) } From e42af358392fae34eab89053c76e286a10653815 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:00:46 -0600 Subject: [PATCH 045/116] tests(prompter): rename huhprompter --- internal/prompter/prompter_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go index debaa449602..c8f6dd21355 100644 --- a/internal/prompter/prompter_test.go +++ b/internal/prompter/prompter_test.go @@ -23,22 +23,22 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { stdout := ios.Out stderr := ios.ErrOut - t.Run("returns accessible huhPrompter when GH_SCREENREADER_FRIENDLY is set to true", func(t *testing.T) { + t.Run("returns SpeechSynthesizerFriendlyPrompter when GH_SCREENREADER_FRIENDLY is set to true", func(t *testing.T) { t.Setenv("GH_SCREENREADER_FRIENDLY", "true") p := New(editorCmd, stdin, stdout, stderr) - require.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected huhPrompter to be returned") - require.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") + require.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected SpeechSynthesizerFriendlyPrompter to be returned") + require.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected SpeechSynthesizerFriendlyPrompter to be accessible") }) - t.Run("returns accessible huhPrompter when GH_SCREENREADER_FRIENDLY is set to 1", func(t *testing.T) { + t.Run("returns SpeechSynthesizerFriendlyPrompter when GH_SCREENREADER_FRIENDLY is set to 1", func(t *testing.T) { t.Setenv("GH_SCREENREADER_FRIENDLY", "1") p := New(editorCmd, stdin, stdout, stderr) - require.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected huhPrompter to be returned") - require.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected huhPrompter to be accessible") + require.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected SpeechSynthesizerFriendlyPrompter to be returned") + require.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected SpeechSynthesizerFriendlyPrompter to be accessible") }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to false", func(t *testing.T) { @@ -66,7 +66,7 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { }) } -func TestAccessibleHuhprompter(t *testing.T) { +func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { // Create a PTY and hook up a virtual terminal emulator ptm, pts, err := pty.Open() require.NoError(t, err) From e299b56c0f29713c853df03abc5ff5f1f306c155 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:08:36 -0600 Subject: [PATCH 046/116] test(prompter): remove needless variable declaration --- internal/prompter/prompter_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go index c8f6dd21355..5c4a45a2c1a 100644 --- a/internal/prompter/prompter_test.go +++ b/internal/prompter/prompter_test.go @@ -233,7 +233,6 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { }) t.Run("InputHostname", func(t *testing.T) { - var inputValue string hostname := "somethingdoesnotmatter.com" go func() { // Wait for prompt to appear From 3eca268a7f60f909afe8ad4cb77b2708d94a2525 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 2 Apr 2025 18:24:20 -0400 Subject: [PATCH 047/116] Introduce `color_labels` support, update commands This commit implements the actual changes around configuration setting / environment variable logic for displaying labels using their RGB hex color code in terminals with truecolor support. One of the subtler changes in this commit is renaming generic ColorScheme.HexToRGB logic to render truecolor to ColorScheme.Label as this feature was being used exclusively for labels. This is due to confusion about introducing the new `color_labels` config on top of generic coloring logic. --- internal/config/config.go | 17 ++++++++ internal/config/config_test.go | 2 + internal/config/stub.go | 3 ++ internal/gh/gh.go | 2 + internal/gh/mock/config.go | 44 +++++++++++++++++++ pkg/cmd/config/list/list_test.go | 19 +++++---- pkg/cmd/factory/default.go | 6 +++ pkg/cmd/factory/default_test.go | 47 ++++++++++++++++++++ pkg/cmd/gist/list/list_test.go | 2 +- pkg/cmd/issue/shared/display.go | 2 +- pkg/cmd/issue/view/view.go | 2 +- pkg/cmd/label/list.go | 7 ++- pkg/cmd/pr/view/view.go | 2 +- pkg/cmd/search/shared/shared.go | 2 +- pkg/iostreams/color.go | 20 +++------ pkg/iostreams/color_test.go | 73 ++++++++------------------------ pkg/iostreams/iostreams.go | 11 ++++- 17 files changed, 178 insertions(+), 83 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 476154d667e..e7534dfdb5f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,6 +17,7 @@ import ( const ( aliasesKey = "aliases" browserKey = "browser" + colorLabelsKey = "color_labels" editorKey = "editor" gitProtocolKey = "git_protocol" hostsKey = "hosts" @@ -113,6 +114,11 @@ func (c *cfg) Browser(hostname string) gh.ConfigEntry { return c.GetOrDefault(hostname, browserKey).Unwrap() } +func (c *cfg) ColorLabels(hostname string) gh.ConfigEntry { + // Intentionally panic if there is no user provided value or default value (which would be a programmer error) + return c.GetOrDefault(hostname, colorLabelsKey).Unwrap() +} + func (c *cfg) Editor(hostname string) gh.ConfigEntry { // Intentionally panic if there is no user provided value or default value (which would be a programmer error) return c.GetOrDefault(hostname, editorKey).Unwrap() @@ -532,6 +538,8 @@ aliases: http_unix_socket: # What web browser gh should use when opening URLs. If blank, will refer to environment. browser: +# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled +color_labels: disabled ` type ConfigOption struct { @@ -602,6 +610,15 @@ var Options = []ConfigOption{ return c.Browser(hostname).Value }, }, + { + Key: colorLabelsKey, + Description: "whether to display labels using their RGB hex color codes in terminals that support truecolor", + DefaultValue: "disabled", + AllowedValues: []string{"enabled", "disabled"}, + CurrentValue: func(c gh.Config, hostname string) string { + return c.ColorLabels(hostname).Value + }, + }, } func HomeDirPath(subdir string) (string, error) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index fef87ddc644..67a9a98d1ab 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -32,6 +32,7 @@ func TestNewConfigProvidesFallback(t *testing.T) { requireKeyWithValue(t, spiedCfg, []string{aliasesKey, "co"}, "pr checkout") requireKeyWithValue(t, spiedCfg, []string{httpUnixSocketKey}, "") requireKeyWithValue(t, spiedCfg, []string{browserKey}, "") + requireKeyWithValue(t, spiedCfg, []string{colorLabelsKey}, "disabled") } func TestGetOrDefaultApplicationDefaults(t *testing.T) { @@ -137,6 +138,7 @@ func TestFallbackConfig(t *testing.T) { requireKeyWithValue(t, cfg, []string{aliasesKey, "co"}, "pr checkout") requireKeyWithValue(t, cfg, []string{httpUnixSocketKey}, "") requireKeyWithValue(t, cfg, []string{browserKey}, "") + requireKeyWithValue(t, cfg, []string{colorLabelsKey}, "disabled") requireNoKey(t, cfg, []string{"unknown"}) } diff --git a/internal/config/stub.go b/internal/config/stub.go index 71d44556dc1..78073da4a17 100644 --- a/internal/config/stub.go +++ b/internal/config/stub.go @@ -55,6 +55,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock { mock.BrowserFunc = func(hostname string) gh.ConfigEntry { return cfg.Browser(hostname) } + mock.ColorLabelsFunc = func(hostname string) gh.ConfigEntry { + return cfg.ColorLabels(hostname) + } mock.EditorFunc = func(hostname string) gh.ConfigEntry { return cfg.Editor(hostname) } diff --git a/internal/gh/gh.go b/internal/gh/gh.go index 8e640c41a88..8f94d7f0d88 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -37,6 +37,8 @@ type Config interface { // Browser returns the configured browser, optionally scoped by host. Browser(hostname string) ConfigEntry + // ColorLabels returns the configured colorize labels setting, optionally scoped by host. + ColorLabels(hostname string) ConfigEntry // Editor returns the configured editor, optionally scoped by host. Editor(hostname string) ConfigEntry // GitProtocol returns the configured git protocol, optionally scoped by host. diff --git a/internal/gh/mock/config.go b/internal/gh/mock/config.go index 569af1facb4..b94cb084dc3 100644 --- a/internal/gh/mock/config.go +++ b/internal/gh/mock/config.go @@ -31,6 +31,9 @@ var _ gh.Config = &ConfigMock{} // CacheDirFunc: func() string { // panic("mock out the CacheDir method") // }, +// ColorLabelsFunc: func(hostname string) gh.ConfigEntry { +// panic("mock out the ColorLabels method") +// }, // EditorFunc: func(hostname string) gh.ConfigEntry { // panic("mock out the Editor method") // }, @@ -83,6 +86,9 @@ type ConfigMock struct { // CacheDirFunc mocks the CacheDir method. CacheDirFunc func() string + // ColorLabelsFunc mocks the ColorLabels method. + ColorLabelsFunc func(hostname string) gh.ConfigEntry + // EditorFunc mocks the Editor method. EditorFunc func(hostname string) gh.ConfigEntry @@ -132,6 +138,11 @@ type ConfigMock struct { // CacheDir holds details about calls to the CacheDir method. CacheDir []struct { } + // ColorLabels holds details about calls to the ColorLabels method. + ColorLabels []struct { + // Hostname is the hostname argument value. + Hostname string + } // Editor holds details about calls to the Editor method. Editor []struct { // Hostname is the hostname argument value. @@ -194,6 +205,7 @@ type ConfigMock struct { lockAuthentication sync.RWMutex lockBrowser sync.RWMutex lockCacheDir sync.RWMutex + lockColorLabels sync.RWMutex lockEditor sync.RWMutex lockGetOrDefault sync.RWMutex lockGitProtocol sync.RWMutex @@ -320,6 +332,38 @@ func (mock *ConfigMock) CacheDirCalls() []struct { return calls } +// ColorLabels calls ColorLabelsFunc. +func (mock *ConfigMock) ColorLabels(hostname string) gh.ConfigEntry { + if mock.ColorLabelsFunc == nil { + panic("ConfigMock.ColorLabelsFunc: method is nil but Config.ColorLabels was just called") + } + callInfo := struct { + Hostname string + }{ + Hostname: hostname, + } + mock.lockColorLabels.Lock() + mock.calls.ColorLabels = append(mock.calls.ColorLabels, callInfo) + mock.lockColorLabels.Unlock() + return mock.ColorLabelsFunc(hostname) +} + +// ColorLabelsCalls gets all the calls that were made to ColorLabels. +// Check the length with: +// +// len(mockedConfig.ColorLabelsCalls()) +func (mock *ConfigMock) ColorLabelsCalls() []struct { + Hostname string +} { + var calls []struct { + Hostname string + } + mock.lockColorLabels.RLock() + calls = mock.calls.ColorLabels + mock.lockColorLabels.RUnlock() + return calls +} + // Editor calls EditorFunc. func (mock *ConfigMock) Editor(hostname string) gh.ConfigEntry { if mock.EditorFunc == nil { diff --git a/pkg/cmd/config/list/list_test.go b/pkg/cmd/config/list/list_test.go index 65f83d659c3..2184d0f1659 100644 --- a/pkg/cmd/config/list/list_test.go +++ b/pkg/cmd/config/list/list_test.go @@ -4,6 +4,7 @@ import ( "bytes" "testing" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/pkg/cmdutil" @@ -91,14 +92,16 @@ func Test_listRun(t *testing.T) { return cfg }(), input: &ListOptions{Hostname: "HOST"}, - stdout: `git_protocol=ssh -editor=/usr/bin/vim -prompt=disabled -prefer_editor_prompt=enabled -pager=less -http_unix_socket= -browser=brave -`, + stdout: heredoc.Doc(` + git_protocol=ssh + editor=/usr/bin/vim + prompt=disabled + prefer_editor_prompt=enabled + pager=less + http_unix_socket= + browser=brave + color_labels=disabled + `), }, } diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index 9236514877c..5e9c25ab29b 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -292,6 +292,12 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { io.SetPager(pager.Value) } + if _, ghColorLabels := os.LookupEnv("GH_COLOR_LABELS"); ghColorLabels { + io.SetColorLabels(true) // TODO: should this be a truthy value? + } else if prompt := cfg.ColorLabels(""); prompt.Value == "enabled" { + io.SetColorLabels(true) + } + return io } diff --git a/pkg/cmd/factory/default_test.go b/pkg/cmd/factory/default_test.go index 94955bb30ec..b1730d6e615 100644 --- a/pkg/cmd/factory/default_test.go +++ b/pkg/cmd/factory/default_test.go @@ -432,6 +432,49 @@ func Test_ioStreams_prompt(t *testing.T) { } } +func Test_ioStreams_colorLabels(t *testing.T) { + tests := []struct { + name string + config gh.Config + colorLabelsEnabled bool + env map[string]string + }{ + { + name: "default config", + colorLabelsEnabled: false, + }, + { + name: "config with colorLabels enabled", + config: enableColorLabelsConfig(), + colorLabelsEnabled: true, + }, + { + name: "colorLabels enabled via GH_COLOR_LABELS env var", + env: map[string]string{"GH_COLOR_LABELS": "1"}, + colorLabelsEnabled: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.env != nil { + for k, v := range tt.env { + t.Setenv(k, v) + } + } + f := New("1") + f.Config = func() (gh.Config, error) { + if tt.config == nil { + return config.NewBlankConfig(), nil + } else { + return tt.config, nil + } + } + io := ioStreams(f) + assert.Equal(t, tt.colorLabelsEnabled, io.ColorLabels()) + }) + } +} + func TestSSOURL(t *testing.T) { tests := []struct { name string @@ -537,3 +580,7 @@ func pagerConfig() gh.Config { func disablePromptConfig() gh.Config { return config.NewFromString("prompt: disabled") } + +func enableColorLabelsConfig() gh.Config { + return config.NewFromString("color_labels: enabled") +} diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 4f6c8a9f7e2..1b55478b300 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -694,7 +694,7 @@ func Test_highlightMatch(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cs := iostreams.NewColorScheme(tt.color, false, false, iostreams.NoTheme) + cs := iostreams.NewColorScheme(tt.color, false, false, false, iostreams.NoTheme) matched := false got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight) diff --git a/pkg/cmd/issue/shared/display.go b/pkg/cmd/issue/shared/display.go index 0c56ffd2cae..bfb9fc2c646 100644 --- a/pkg/cmd/issue/shared/display.go +++ b/pkg/cmd/issue/shared/display.go @@ -56,7 +56,7 @@ func issueLabelList(issue *api.Issue, cs *iostreams.ColorScheme, colorize bool) labelNames := make([]string, 0, len(issue.Labels.Nodes)) for _, label := range issue.Labels.Nodes { if colorize { - labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name)) + labelNames = append(labelNames, cs.Label(label.Color, label.Name)) } else { labelNames = append(labelNames, label.Name) } diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go index b188c6a4ca3..e2420a820e5 100644 --- a/pkg/cmd/issue/view/view.go +++ b/pkg/cmd/issue/view/view.go @@ -317,7 +317,7 @@ func issueLabelList(issue *api.Issue, cs *iostreams.ColorScheme) string { if cs == nil { labelNames[i] = label.Name } else { - labelNames[i] = cs.HexToRGB(label.Color, label.Name) + labelNames[i] = cs.Label(label.Color, label.Name) } } diff --git a/pkg/cmd/label/list.go b/pkg/cmd/label/list.go index fe1e9cdb7f6..4fe2f5ec9e3 100644 --- a/pkg/cmd/label/list.go +++ b/pkg/cmd/label/list.go @@ -137,7 +137,12 @@ func printLabels(io *iostreams.IOStreams, labels []label) error { table := tableprinter.New(io, tableprinter.WithHeader("NAME", "DESCRIPTION", "COLOR")) for _, label := range labels { - table.AddField(label.Name, tableprinter.WithColor(cs.ColorFromRGB(label.Color))) + // Colorize the label using tableprinter's WithColor function for it to handle non-TTY situations + labelColor := tableprinter.WithColor(func(s string) string { + return cs.Label(label.Color, s) + }) + + table.AddField(label.Name, labelColor) table.AddField(label.Description) table.AddField("#" + label.Color) diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go index ccad4fa77cf..719a8fc7b66 100644 --- a/pkg/cmd/pr/view/view.go +++ b/pkg/cmd/pr/view/view.go @@ -423,7 +423,7 @@ func prLabelList(pr api.PullRequest, cs *iostreams.ColorScheme) string { labelNames := make([]string, 0, len(pr.Labels.Nodes)) for _, label := range pr.Labels.Nodes { - labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name)) + labelNames = append(labelNames, cs.Label(label.Color, label.Name)) } list := strings.Join(labelNames, ", ") diff --git a/pkg/cmd/search/shared/shared.go b/pkg/cmd/search/shared/shared.go index f0a346fc878..1e3d0069ba2 100644 --- a/pkg/cmd/search/shared/shared.go +++ b/pkg/cmd/search/shared/shared.go @@ -158,7 +158,7 @@ func listIssueLabels(issue *search.Issue, cs *iostreams.ColorScheme, colorize bo labelNames := make([]string, 0, len(issue.Labels)) for _, label := range issue.Labels { if colorize { - labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name)) + labelNames = append(labelNames, cs.Label(label.Color, label.Name)) } else { labelNames = append(labelNames, label.Name) } diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 07fdcd79fa6..49f9496f5dc 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -41,12 +41,13 @@ var ( // NewColorScheme initializes color logic based on provided terminal capabilities. // Logic dealing with terminal theme detected, such as whether color is enabled, 8-bit color supported, true color supported, -// and terminal theme detected. -func NewColorScheme(enabled, is256enabled, trueColor bool, theme string) *ColorScheme { +// labels are colored, and terminal theme detected. +func NewColorScheme(enabled, is256enabled, trueColor, colorLabels bool, theme string) *ColorScheme { return &ColorScheme{ enabled: enabled, is256enabled: is256enabled, hasTrueColor: trueColor, + colorLabels: colorLabels, theme: theme, } } @@ -55,6 +56,7 @@ type ColorScheme struct { enabled bool is256enabled bool hasTrueColor bool + colorLabels bool theme string } @@ -240,17 +242,9 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string { return fn } -// ColorFromRGB returns a function suitable for TablePrinter.AddField -// that calls HexToRGB, coloring text if supported by the terminal. -func (c *ColorScheme) ColorFromRGB(hex string) func(string) string { - return func(s string) string { - return c.HexToRGB(hex, s) - } -} - -// HexToRGB uses the given hex to color x if supported by the terminal. -func (c *ColorScheme) HexToRGB(hex string, x string) string { - if !c.enabled || !c.hasTrueColor || len(hex) != 6 { +// Label stylizes text based on label's RGB hex color. +func (c *ColorScheme) Label(hex string, x string) string { + if !c.enabled || !c.hasTrueColor || !c.colorLabels || len(hex) != 6 { return x } diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index b35c2eb73a8..c477891ce29 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestColorFromRGB(t *testing.T) { +func TestLabel(t *testing.T) { tests := []struct { name string hex string @@ -20,77 +20,40 @@ func TestColorFromRGB(t *testing.T) { hex: "fc0303", text: "red", wants: "\033[38;2;252;3;3mred\033[0m", - cs: NewColorScheme(true, true, true, NoTheme), + cs: NewColorScheme(true, true, true, true, NoTheme), }, { name: "no truecolor", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, false, NoTheme), + cs: NewColorScheme(true, true, false, true, NoTheme), }, { name: "no color", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, NoTheme), + cs: NewColorScheme(false, false, false, true, NoTheme), }, { name: "invalid hex", hex: "fc0", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, NoTheme), - }, - } - - for _, tt := range tests { - fn := tt.cs.ColorFromRGB(tt.hex) - assert.Equal(t, tt.wants, fn(tt.text)) - } -} - -func TestHexToRGB(t *testing.T) { - tests := []struct { - name string - hex string - text string - wants string - cs *ColorScheme - }{ - { - name: "truecolor", - hex: "fc0303", - text: "red", - wants: "\033[38;2;252;3;3mred\033[0m", - cs: NewColorScheme(true, true, true, NoTheme), - }, - { - name: "no truecolor", - hex: "fc0303", - text: "red", - wants: "red", - cs: NewColorScheme(true, true, false, NoTheme), + cs: NewColorScheme(false, false, false, true, NoTheme), }, { - name: "no color", + name: "no color labels", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, NoTheme), - }, - { - name: "invalid hex", - hex: "fc0", - text: "red", - wants: "red", - cs: NewColorScheme(false, false, false, NoTheme), + cs: NewColorScheme(true, true, true, false, NoTheme), }, } for _, tt := range tests { - output := tt.cs.HexToRGB(tt.hex, tt.text) + output := tt.cs.Label(tt.hex, tt.text) assert.Equal(t, tt.wants, output) } } @@ -109,61 +72,61 @@ func TestTableHeader(t *testing.T) { }{ { name: "when color is disabled, text is not stylized", - cs: NewColorScheme(false, false, false, NoTheme), + cs: NewColorScheme(false, false, false, false, NoTheme), input: "this should not be stylized", expected: "this should not be stylized", }, { name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, false, false, NoTheme), + cs: NewColorScheme(true, false, false, false, NoTheme), input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, false, false, LightTheme), + cs: NewColorScheme(true, false, false, false, LightTheme), input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, false, false, DarkTheme), + cs: NewColorScheme(true, false, false, false, DarkTheme), input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, { name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, true, false, NoTheme), + cs: NewColorScheme(true, true, false, false, NoTheme), input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, true, false, LightTheme), + cs: NewColorScheme(true, true, false, false, LightTheme), input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, true, false, DarkTheme), + cs: NewColorScheme(true, true, false, false, DarkTheme), input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, { name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, true, true, NoTheme), + cs: NewColorScheme(true, true, true, false, NoTheme), input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, true, true, LightTheme), + cs: NewColorScheme(true, true, true, false, LightTheme), input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, true, true, DarkTheme), + cs: NewColorScheme(true, true, true, false, DarkTheme), input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 6c12d7911a6..e78575b9ce8 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -72,6 +72,7 @@ type IOStreams struct { colorOverride bool colorEnabled bool + colorLabels bool pagerCommand string pagerProcess *os.Process @@ -102,6 +103,10 @@ func (s *IOStreams) HasTrueColor() bool { return s.term.IsTrueColorSupported() } +func (s *IOStreams) ColorLabels() bool { + return s.colorLabels +} + // DetectTerminalTheme is a utility to call before starting the output pager so that the terminal background // can be reliably detected. func (s *IOStreams) DetectTerminalTheme() { @@ -134,6 +139,10 @@ func (s *IOStreams) SetColorEnabled(colorEnabled bool) { s.colorEnabled = colorEnabled } +func (s *IOStreams) SetColorLabels(colorLabels bool) { + s.colorLabels = colorLabels +} + func (s *IOStreams) SetStdinTTY(isTTY bool) { s.stdinTTYOverride = true s.stdinIsTTY = isTTY @@ -366,7 +375,7 @@ func (s *IOStreams) TerminalWidth() int { } func (s *IOStreams) ColorScheme() *ColorScheme { - return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.TerminalTheme()) + return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.ColorLabels(), s.TerminalTheme()) } func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) { From 5f03c208a1d1dcd862b62d4497fdbfe91f486e77 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 2 Apr 2025 18:32:37 -0400 Subject: [PATCH 048/116] Fix comment language --- internal/gh/gh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/gh/gh.go b/internal/gh/gh.go index 8f94d7f0d88..b17c6bd67fb 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -37,7 +37,7 @@ type Config interface { // Browser returns the configured browser, optionally scoped by host. Browser(hostname string) ConfigEntry - // ColorLabels returns the configured colorize labels setting, optionally scoped by host. + // ColorLabels returns the configured color_label setting, optionally scoped by host. ColorLabels(hostname string) ConfigEntry // Editor returns the configured editor, optionally scoped by host. Editor(hostname string) ConfigEntry From 8827803bd1d864f2a39076e60052c6dcdea4cb84 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:17:53 -0600 Subject: [PATCH 049/116] test(prompter): skip vt10x tests on Windows --- internal/prompter/prompter.go | 26 +- internal/prompter/prompter_test.go | 301 +---------------- ...eech_synthesizer_friendly_prompter_test.go | 302 ++++++++++++++++++ 3 files changed, 319 insertions(+), 310 deletions(-) create mode 100644 internal/prompter/speech_synthesizer_friendly_prompter_test.go diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 93d1d34c009..b9e832bd065 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -41,7 +41,7 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr editorCmd: editorCmd, } default: - return &SpeechSynthesizerFriendlyPrompter{ + return &speechSynthesizerFriendlyPrompter{ stdin: stdin, stdout: stdout, stderr: stderr, @@ -51,7 +51,7 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr } } -type SpeechSynthesizerFriendlyPrompter struct { +type speechSynthesizerFriendlyPrompter struct { stdin ghPrompter.FileReader stdout ghPrompter.FileWriter stderr ghPrompter.FileWriter @@ -60,18 +60,18 @@ type SpeechSynthesizerFriendlyPrompter struct { } // IsAccessible returns true if the huhPrompter was created in accessible mode. -func (p *SpeechSynthesizerFriendlyPrompter) IsAccessible() bool { +func (p *speechSynthesizerFriendlyPrompter) IsAccessible() bool { return p.accessible } -func (p *SpeechSynthesizerFriendlyPrompter) newForm(groups ...*huh.Group) *huh.Form { +func (p *speechSynthesizerFriendlyPrompter) newForm(groups ...*huh.Group) *huh.Form { return huh.NewForm(groups...). WithTheme(huh.ThemeBase16()). WithAccessible(p.accessible). WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin)) } -func (p *SpeechSynthesizerFriendlyPrompter) Select(prompt, _ string, options []string) (int, error) { +func (p *speechSynthesizerFriendlyPrompter) Select(prompt, _ string, options []string) (int, error) { var result int formOptions := []huh.Option[int]{} for i, o := range options { @@ -91,7 +91,7 @@ func (p *SpeechSynthesizerFriendlyPrompter) Select(prompt, _ string, options []s return result, err } -func (p *SpeechSynthesizerFriendlyPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { +func (p *speechSynthesizerFriendlyPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { var result []int formOptions := make([]huh.Option[int], len(options)) for i, o := range options { @@ -116,7 +116,7 @@ func (p *SpeechSynthesizerFriendlyPrompter) MultiSelect(prompt string, defaults return result[:mid], nil } -func (p *SpeechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) (string, error) { +func (p *speechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) (string, error) { result := defaultValue form := p.newForm( huh.NewGroup( @@ -130,7 +130,7 @@ func (p *SpeechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) ( return result, err } -func (p *SpeechSynthesizerFriendlyPrompter) Password(prompt string) (string, error) { +func (p *speechSynthesizerFriendlyPrompter) Password(prompt string) (string, error) { var result string form := p.newForm( huh.NewGroup( @@ -146,7 +146,7 @@ func (p *SpeechSynthesizerFriendlyPrompter) Password(prompt string) (string, err return result, err } -func (p *SpeechSynthesizerFriendlyPrompter) Confirm(prompt string, _ bool) (bool, error) { +func (p *speechSynthesizerFriendlyPrompter) Confirm(prompt string, _ bool) (bool, error) { var result bool form := p.newForm( huh.NewGroup( @@ -161,7 +161,7 @@ func (p *SpeechSynthesizerFriendlyPrompter) Confirm(prompt string, _ bool) (bool return result, nil } -func (p *SpeechSynthesizerFriendlyPrompter) AuthToken() (string, error) { +func (p *speechSynthesizerFriendlyPrompter) AuthToken() (string, error) { var result string form := p.newForm( huh.NewGroup( @@ -183,7 +183,7 @@ func (p *SpeechSynthesizerFriendlyPrompter) AuthToken() (string, error) { return result, err } -func (p *SpeechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string) error { +func (p *speechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string) error { var result string form := p.newForm( huh.NewGroup( @@ -204,7 +204,7 @@ func (p *SpeechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string return form.Run() } -func (p *SpeechSynthesizerFriendlyPrompter) InputHostname() (string, error) { +func (p *speechSynthesizerFriendlyPrompter) InputHostname() (string, error) { var result string form := p.newForm( huh.NewGroup( @@ -219,7 +219,7 @@ func (p *SpeechSynthesizerFriendlyPrompter) InputHostname() (string, error) { return result, err } -func (p *SpeechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { +func (p *speechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { var result string options := []huh.Option[string]{ huh.NewOption("Open Editor", "open"), diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go index 5c4a45a2c1a..b83a3e890a6 100644 --- a/internal/prompter/prompter_test.go +++ b/internal/prompter/prompter_test.go @@ -1,18 +1,9 @@ package prompter import ( - "fmt" - "io" - "os" - "strings" "testing" - "time" - "github.com/Netflix/go-expect" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/creack/pty" - "github.com/hinshun/vt10x" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -28,8 +19,8 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - require.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected SpeechSynthesizerFriendlyPrompter to be returned") - require.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected SpeechSynthesizerFriendlyPrompter to be accessible") + require.IsType(t, &speechSynthesizerFriendlyPrompter{}, p, "expected SpeechSynthesizerFriendlyPrompter to be returned") + require.Equal(t, p.(*speechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected SpeechSynthesizerFriendlyPrompter to be accessible") }) t.Run("returns SpeechSynthesizerFriendlyPrompter when GH_SCREENREADER_FRIENDLY is set to 1", func(t *testing.T) { @@ -37,8 +28,8 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { p := New(editorCmd, stdin, stdout, stderr) - require.IsType(t, &SpeechSynthesizerFriendlyPrompter{}, p, "expected SpeechSynthesizerFriendlyPrompter to be returned") - require.Equal(t, p.(*SpeechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected SpeechSynthesizerFriendlyPrompter to be accessible") + require.IsType(t, &speechSynthesizerFriendlyPrompter{}, p, "expected SpeechSynthesizerFriendlyPrompter to be returned") + require.Equal(t, p.(*speechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected SpeechSynthesizerFriendlyPrompter to be accessible") }) t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to false", func(t *testing.T) { @@ -65,287 +56,3 @@ func TestNewReturnsAccessiblePrompter(t *testing.T) { require.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") }) } - -func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { - // Create a PTY and hook up a virtual terminal emulator - ptm, pts, err := pty.Open() - require.NoError(t, err) - - term := vt10x.New(vt10x.WithWriter(pts)) - - // Create a console via Expect that allows scripting against the terminal - consoleOpts := []expect.ConsoleOpt{ - expect.WithStdin(ptm), - expect.WithStdout(term), - expect.WithCloser(ptm, pts), - failOnExpectError(t), - failOnSendError(t), - expect.WithDefaultTimeout(time.Second * 600), - } - - console, err := expect.NewConsole(consoleOpts...) - require.NoError(t, err) - t.Cleanup(func() { testCloser(t, console) }) - - p := &SpeechSynthesizerFriendlyPrompter{ - editorCmd: "", // intentionally empty to cause a failure. - accessible: true, - } - - // Using OS here because huh currently ignores configured iostreams - // See https://github.com/charmbracelet/huh/issues/612 - stdIn := os.Stdin - stdOut := os.Stdout - stdErr := os.Stderr - - t.Cleanup(func() { - os.Stdin = stdIn - os.Stdout = stdOut - os.Stderr = stdErr - }) - - os.Stdin = console.Tty() - os.Stdout = console.Tty() - os.Stderr = console.Tty() - - t.Run("Select", func(t *testing.T) { - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("Choose:") - require.NoError(t, err) - - // Select option 1 - _, err = console.SendLine("1") - require.NoError(t, err) - }() - - selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"}) - require.NoError(t, err) - - assert.Equal(t, 0, selectValue) - }) - - t.Run("MultiSelect", func(t *testing.T) { - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("Select a number") - require.NoError(t, err) - - // Select options 1 and 2 - _, err = console.SendLine("1") - require.NoError(t, err) - _, err = console.SendLine("2") - require.NoError(t, err) - - // This confirms selections - _, err = console.SendLine("0") - require.NoError(t, err) - }() - - multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"}) - require.NoError(t, err) - - assert.Equal(t, []int{0, 1}, multiSelectValue) - }) - - t.Run("Input", func(t *testing.T) { - dummyText := "12345abcdefg" - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("Enter some characters") - require.NoError(t, err) - - // Enter a number - _, err = console.SendLine(dummyText) - require.NoError(t, err) - }() - - inputValue, err := p.Input("Enter some characters", "") - require.NoError(t, err) - - assert.Equal(t, dummyText, inputValue) - }) - - t.Run("Password", func(t *testing.T) { - dummyPassword := "12345abcdefg" - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("Enter password") - require.NoError(t, err) - - // Enter a number - _, err = console.SendLine(dummyPassword) - require.NoError(t, err) - }() - - passwordValue, err := p.Password("Enter password") - require.NoError(t, err) - require.Equal(t, dummyPassword, passwordValue) - }) - - t.Run("Confirm", func(t *testing.T) { - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("Are you sure") - require.NoError(t, err) - - // Confirm - _, err = console.SendLine("y") - require.NoError(t, err) - }() - - confirmValue, err := p.Confirm("Are you sure", false) - require.NoError(t, err) - require.Equal(t, true, confirmValue) - }) - - t.Run("AuthToken", func(t *testing.T) { - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("Paste your authentication token:") - require.NoError(t, err) - - // Enter a number - _, err = console.SendLine("12345abcdefg") - require.NoError(t, err) - }() - - authValue, err := p.AuthToken() - require.NoError(t, err) - require.Equal(t, "12345abcdefg", authValue) - }) - - t.Run("ConfirmDeletion", func(t *testing.T) { - requiredValue := "test" - go func() { - // Wait for prompt to appear - _, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue)) - require.NoError(t, err) - - // Confirm - _, err = console.SendLine(requiredValue) - require.NoError(t, err) - }() - - // An err indicates that the confirmation text sent did not match - err := p.ConfirmDeletion(requiredValue) - require.NoError(t, err) - }) - - t.Run("InputHostname", func(t *testing.T) { - hostname := "somethingdoesnotmatter.com" - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("Hostname:") - require.NoError(t, err) - - // Enter the hostname - _, err = console.SendLine(hostname) - require.NoError(t, err) - }() - - inputValue, err := p.InputHostname() - require.NoError(t, err) - require.Equal(t, hostname, inputValue) - }) - - t.Run("MarkdownEditor - blank allowed", func(t *testing.T) { - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("How to edit?") - require.NoError(t, err) - - // Enter 2, to select "skip" - _, err = console.SendLine("2") - require.NoError(t, err) - }() - - inputValue, err := p.MarkdownEditor("How to edit?", "", true) - require.NoError(t, err) - require.Equal(t, "", inputValue) - }) - - t.Run("MarkdownEditor - blank disallowed", func(t *testing.T) { - go func() { - // Wait for prompt to appear - _, err := console.ExpectString("How to edit?") - require.NoError(t, err) - - // Enter number 2 to select "skip". This shoudln't be allowed. - _, err = console.SendLine("2") - require.NoError(t, err) - - // Expect a notice to enter something valid since blank is disallowed. - _, err = console.ExpectString("invalid input. please try again") - require.NoError(t, err) - - // Send a 1 to select to open the editor. - // Sending the input won't fail, so we expect no error here. - // See below though, since we expect the editor to fail to open. - _, err = console.SendLine("1") - require.NoError(t, err) - }() - - // However, here we do expect an error because the editor program - // is intentionally empty and will fail. - inputValue, err := p.MarkdownEditor("How to edit?", "", false) - require.Error(t, err) - require.Equal(t, "", inputValue) - }) -} - -// failOnExpectError adds an observer that will fail the test in a standardised way -// if any expectation on the command output fails, without requiring an explicit -// assertion. -// -// Use WithRelaxedIO to disable this behaviour. -func failOnExpectError(t testing.TB) expect.ConsoleOpt { - t.Helper() - return expect.WithExpectObserver( - func(matchers []expect.Matcher, buf string, err error) { - t.Helper() - - if err == nil { - return - } - - if len(matchers) == 0 { - t.Fatalf("Error occurred while matching %q: %s\n", buf, err) - } - - var criteria []string - for _, matcher := range matchers { - criteria = append(criteria, fmt.Sprintf("%q", matcher.Criteria())) - } - t.Fatalf("Failed to find [%s] in %q: %s\n", strings.Join(criteria, ", "), buf, err) - }, - ) -} - -// failOnSendError adds an observer that will fail the test in a standardised way -// if any sending of input fails, without requiring an explicit assertion. -// -// Use WithRelaxedIO to disable this behaviour. -func failOnSendError(t testing.TB) expect.ConsoleOpt { - t.Helper() - return expect.WithSendObserver( - func(msg string, n int, err error) { - t.Helper() - - if err != nil { - t.Fatalf("Failed to send %q: %s\n", msg, err) - } - if len(msg) != n { - t.Fatalf("Only sent %d of %d bytes for %q\n", n, len(msg), msg) - } - }, - ) -} - -// testCloser is a helper to fail the test if a Closer fails to close. -func testCloser(t testing.TB, closer io.Closer) { - t.Helper() - if err := closer.Close(); err != nil { - t.Errorf("Close failed: %s", err) - } -} diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go new file mode 100644 index 00000000000..155258719a8 --- /dev/null +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -0,0 +1,302 @@ +//go:build !windows + +package prompter + +import ( + "fmt" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/Netflix/go-expect" + "github.com/creack/pty" + "github.com/hinshun/vt10x" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { + // Create a PTY and hook up a virtual terminal emulator + ptm, pts, err := pty.Open() + require.NoError(t, err) + + term := vt10x.New(vt10x.WithWriter(pts)) + + // Create a console via Expect that allows scripting against the terminal + consoleOpts := []expect.ConsoleOpt{ + expect.WithStdin(ptm), + expect.WithStdout(term), + expect.WithCloser(ptm, pts), + failOnExpectError(t), + failOnSendError(t), + expect.WithDefaultTimeout(time.Second * 600), + } + + console, err := expect.NewConsole(consoleOpts...) + require.NoError(t, err) + t.Cleanup(func() { testCloser(t, console) }) + + p := &speechSynthesizerFriendlyPrompter{ + editorCmd: "", // intentionally empty to cause a failure. + accessible: true, + } + + // Using OS here because huh currently ignores configured iostreams + // See https://github.com/charmbracelet/huh/issues/612 + stdIn := os.Stdin + stdOut := os.Stdout + stdErr := os.Stderr + + t.Cleanup(func() { + os.Stdin = stdIn + os.Stdout = stdOut + os.Stderr = stdErr + }) + + os.Stdin = console.Tty() + os.Stdout = console.Tty() + os.Stderr = console.Tty() + + t.Run("Select", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Choose:") + require.NoError(t, err) + + // Select option 1 + _, err = console.SendLine("1") + require.NoError(t, err) + }() + + selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"}) + require.NoError(t, err) + + assert.Equal(t, 0, selectValue) + }) + + t.Run("MultiSelect", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Select a number") + require.NoError(t, err) + + // Select options 1 and 2 + _, err = console.SendLine("1") + require.NoError(t, err) + _, err = console.SendLine("2") + require.NoError(t, err) + + // This confirms selections + _, err = console.SendLine("0") + require.NoError(t, err) + }() + + multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"}) + require.NoError(t, err) + + assert.Equal(t, []int{0, 1}, multiSelectValue) + }) + + t.Run("Input", func(t *testing.T) { + dummyText := "12345abcdefg" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Enter some characters") + require.NoError(t, err) + + // Enter a number + _, err = console.SendLine(dummyText) + require.NoError(t, err) + }() + + inputValue, err := p.Input("Enter some characters", "") + require.NoError(t, err) + + assert.Equal(t, dummyText, inputValue) + }) + + t.Run("Password", func(t *testing.T) { + dummyPassword := "12345abcdefg" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Enter password") + require.NoError(t, err) + + // Enter a number + _, err = console.SendLine(dummyPassword) + require.NoError(t, err) + }() + + passwordValue, err := p.Password("Enter password") + require.NoError(t, err) + require.Equal(t, dummyPassword, passwordValue) + }) + + t.Run("Confirm", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Are you sure") + require.NoError(t, err) + + // Confirm + _, err = console.SendLine("y") + require.NoError(t, err) + }() + + confirmValue, err := p.Confirm("Are you sure", false) + require.NoError(t, err) + require.Equal(t, true, confirmValue) + }) + + t.Run("AuthToken", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Paste your authentication token:") + require.NoError(t, err) + + // Enter a number + _, err = console.SendLine("12345abcdefg") + require.NoError(t, err) + }() + + authValue, err := p.AuthToken() + require.NoError(t, err) + require.Equal(t, "12345abcdefg", authValue) + }) + + t.Run("ConfirmDeletion", func(t *testing.T) { + requiredValue := "test" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue)) + require.NoError(t, err) + + // Confirm + _, err = console.SendLine(requiredValue) + require.NoError(t, err) + }() + + // An err indicates that the confirmation text sent did not match + err := p.ConfirmDeletion(requiredValue) + require.NoError(t, err) + }) + + t.Run("InputHostname", func(t *testing.T) { + hostname := "somethingdoesnotmatter.com" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Hostname:") + require.NoError(t, err) + + // Enter the hostname + _, err = console.SendLine(hostname) + require.NoError(t, err) + }() + + inputValue, err := p.InputHostname() + require.NoError(t, err) + require.Equal(t, hostname, inputValue) + }) + + t.Run("MarkdownEditor - blank allowed", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("How to edit?") + require.NoError(t, err) + + // Enter 2, to select "skip" + _, err = console.SendLine("2") + require.NoError(t, err) + }() + + inputValue, err := p.MarkdownEditor("How to edit?", "", true) + require.NoError(t, err) + require.Equal(t, "", inputValue) + }) + + t.Run("MarkdownEditor - blank disallowed", func(t *testing.T) { + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("How to edit?") + require.NoError(t, err) + + // Enter number 2 to select "skip". This shoudln't be allowed. + _, err = console.SendLine("2") + require.NoError(t, err) + + // Expect a notice to enter something valid since blank is disallowed. + _, err = console.ExpectString("invalid input. please try again") + require.NoError(t, err) + + // Send a 1 to select to open the editor. + // Sending the input won't fail, so we expect no error here. + // See below though, since we expect the editor to fail to open. + _, err = console.SendLine("1") + require.NoError(t, err) + }() + + // However, here we do expect an error because the editor program + // is intentionally empty and will fail. + inputValue, err := p.MarkdownEditor("How to edit?", "", false) + require.Error(t, err) + require.Equal(t, "", inputValue) + }) +} + +// failOnExpectError adds an observer that will fail the test in a standardised way +// if any expectation on the command output fails, without requiring an explicit +// assertion. +// +// Use WithRelaxedIO to disable this behaviour. +func failOnExpectError(t testing.TB) expect.ConsoleOpt { + t.Helper() + return expect.WithExpectObserver( + func(matchers []expect.Matcher, buf string, err error) { + t.Helper() + + if err == nil { + return + } + + if len(matchers) == 0 { + t.Fatalf("Error occurred while matching %q: %s\n", buf, err) + } + + var criteria []string + for _, matcher := range matchers { + criteria = append(criteria, fmt.Sprintf("%q", matcher.Criteria())) + } + t.Fatalf("Failed to find [%s] in %q: %s\n", strings.Join(criteria, ", "), buf, err) + }, + ) +} + +// failOnSendError adds an observer that will fail the test in a standardised way +// if any sending of input fails, without requiring an explicit assertion. +// +// Use WithRelaxedIO to disable this behaviour. +func failOnSendError(t testing.TB) expect.ConsoleOpt { + t.Helper() + return expect.WithSendObserver( + func(msg string, n int, err error) { + t.Helper() + + if err != nil { + t.Fatalf("Failed to send %q: %s\n", msg, err) + } + if len(msg) != n { + t.Fatalf("Only sent %d of %d bytes for %q\n", n, len(msg), msg) + } + }, + ) +} + +// testCloser is a helper to fail the test if a Closer fails to close. +func testCloser(t testing.TB, closer io.Closer) { + t.Helper() + if err := closer.Close(); err != nil { + t.Errorf("Close failed: %s", err) + } +} From 88e6285b49d5a476e3e90c7d6867a91d346cb94a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 3 Apr 2025 08:45:58 -0600 Subject: [PATCH 050/116] test(prompter): move to external package --- internal/prompter/prompter.go | 29 ++++------ internal/prompter/prompter_test.go | 58 ------------------- ...eech_synthesizer_friendly_prompter_test.go | 16 ++--- 3 files changed, 19 insertions(+), 84 deletions(-) delete mode 100644 internal/prompter/prompter_test.go diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index b9e832bd065..e764f713837 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/AlecAivazis/survey/v2" - tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/pkg/surveyext" @@ -42,33 +41,27 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr } default: return &speechSynthesizerFriendlyPrompter{ - stdin: stdin, - stdout: stdout, - stderr: stderr, - editorCmd: editorCmd, - accessible: true, + stdin: stdin, + stdout: stdout, + stderr: stderr, + editorCmd: editorCmd, } } } type speechSynthesizerFriendlyPrompter struct { - stdin ghPrompter.FileReader - stdout ghPrompter.FileWriter - stderr ghPrompter.FileWriter - editorCmd string - accessible bool -} - -// IsAccessible returns true if the huhPrompter was created in accessible mode. -func (p *speechSynthesizerFriendlyPrompter) IsAccessible() bool { - return p.accessible + stdin ghPrompter.FileReader + stdout ghPrompter.FileWriter + stderr ghPrompter.FileWriter + editorCmd string } func (p *speechSynthesizerFriendlyPrompter) newForm(groups ...*huh.Group) *huh.Form { return huh.NewForm(groups...). WithTheme(huh.ThemeBase16()). - WithAccessible(p.accessible). - WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin)) + WithAccessible(true) + // Commented out because https://github.com/charmbracelet/huh/issues/612 + // WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin)) } func (p *speechSynthesizerFriendlyPrompter) Select(prompt, _ string, options []string) (int, error) { diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go deleted file mode 100644 index b83a3e890a6..00000000000 --- a/internal/prompter/prompter_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package prompter - -import ( - "testing" - - "github.com/cli/cli/v2/pkg/iostreams" - "github.com/stretchr/testify/require" -) - -func TestNewReturnsAccessiblePrompter(t *testing.T) { - editorCmd := "nothing" - ios, _, _, _ := iostreams.Test() - stdin := ios.In - stdout := ios.Out - stderr := ios.ErrOut - - t.Run("returns SpeechSynthesizerFriendlyPrompter when GH_SCREENREADER_FRIENDLY is set to true", func(t *testing.T) { - t.Setenv("GH_SCREENREADER_FRIENDLY", "true") - - p := New(editorCmd, stdin, stdout, stderr) - - require.IsType(t, &speechSynthesizerFriendlyPrompter{}, p, "expected SpeechSynthesizerFriendlyPrompter to be returned") - require.Equal(t, p.(*speechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected SpeechSynthesizerFriendlyPrompter to be accessible") - }) - - t.Run("returns SpeechSynthesizerFriendlyPrompter when GH_SCREENREADER_FRIENDLY is set to 1", func(t *testing.T) { - t.Setenv("GH_SCREENREADER_FRIENDLY", "1") - - p := New(editorCmd, stdin, stdout, stderr) - - require.IsType(t, &speechSynthesizerFriendlyPrompter{}, p, "expected SpeechSynthesizerFriendlyPrompter to be returned") - require.Equal(t, p.(*speechSynthesizerFriendlyPrompter).IsAccessible(), true, "expected SpeechSynthesizerFriendlyPrompter to be accessible") - }) - - t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to false", func(t *testing.T) { - t.Setenv("GH_SCREENREADER_FRIENDLY", "false") - - p := New(editorCmd, stdin, stdout, stderr) - - require.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") - }) - - t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is set to 0", func(t *testing.T) { - t.Setenv("GH_SCREENREADER_FRIENDLY", "0") - - p := New(editorCmd, stdin, stdout, stderr) - - require.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") - }) - - t.Run("returns surveyPrompter when GH_SCREENREADER_FRIENDLY is unset", func(t *testing.T) { - t.Setenv("GH_SCREENREADER_FRIENDLY", "") - - p := New(editorCmd, stdin, stdout, stderr) - - require.IsType(t, &surveyPrompter{}, p, "expected surveyPrompter to be returned") - }) -} diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 155258719a8..2ccea38a45a 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -1,6 +1,6 @@ //go:build !windows -package prompter +package prompter_test import ( "fmt" @@ -11,6 +11,7 @@ import ( "time" "github.com/Netflix/go-expect" + "github.com/cli/cli/v2/internal/prompter" "github.com/creack/pty" "github.com/hinshun/vt10x" "github.com/stretchr/testify/assert" @@ -38,13 +39,6 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { testCloser(t, console) }) - p := &speechSynthesizerFriendlyPrompter{ - editorCmd: "", // intentionally empty to cause a failure. - accessible: true, - } - - // Using OS here because huh currently ignores configured iostreams - // See https://github.com/charmbracelet/huh/issues/612 stdIn := os.Stdin stdOut := os.Stdout stdErr := os.Stderr @@ -59,6 +53,11 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { os.Stdout = console.Tty() os.Stderr = console.Tty() + // Using OS here because huh currently ignores configured iostreams + // See https://github.com/charmbracelet/huh/issues/612 + t.Setenv("GH_SCREENREADER_FRIENDLY", "true") + p := prompter.New("", nil, nil, nil) + t.Run("Select", func(t *testing.T) { go func() { // Wait for prompt to appear @@ -150,6 +149,7 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { require.Equal(t, true, confirmValue) }) + // Need one that enters invalid input t.Run("AuthToken", func(t *testing.T) { go func() { // Wait for prompt to appear From 02fc12e7b74d3aab2060bdfd415f53b2b733dc7a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 3 Apr 2025 08:51:54 -0600 Subject: [PATCH 051/116] fix(linter): linter errors --- go.mod | 2 +- internal/prompter/speech_synthesizer_friendly_prompter_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 85b0f88d4b6..9e2a2915b8e 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/briandowns/spinner v1.18.1 github.com/cenkalti/backoff/v4 v4.3.0 - github.com/charmbracelet/bubbletea v1.3.4 github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 github.com/charmbracelet/huh v0.6.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc @@ -74,6 +73,7 @@ require ( github.com/blang/semver v3.5.1+incompatible // indirect github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.3.4 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 2ccea38a45a..b8d4571f6a5 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -149,7 +149,8 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { require.Equal(t, true, confirmValue) }) - // Need one that enters invalid input + // TODO: Need one that enters invalid input + // TODO: write tests for control-c t.Run("AuthToken", func(t *testing.T) { go func() { // Wait for prompt to appear From 49ddacf5b85cff6d72faded445047a9e4485f298 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:06:31 -0600 Subject: [PATCH 052/116] docs(prompter): doc prompter interface --- internal/prompter/prompter.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index e764f713837..317e9094a0a 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -15,17 +15,31 @@ import ( //go:generate moq -rm -out prompter_mock.go . Prompter type Prompter interface { // generic prompts from go-gh - Select(string, string, []string) (int, error) + + // Select prompts the user to select an option from a list of options. + Select(prompt string, defaultValue string, options []string) (int, error) + // MultiSelect prompts the user to select one or more options from a list of options. MultiSelect(prompt string, defaults []string, options []string) ([]int, error) - Input(string, string) (string, error) - Password(string) (string, error) - Confirm(string, bool) (bool, error) + // Input prompts the user to enter a string value. + Input(prompt string, defaultValue string) (string, error) + // Password prompts the user to enter a password. + Password(prompt string) (string, error) + // Confirm prompts the user to confirm an action. + Confirm(prompt string, defaultValue bool) (bool, error) // gh specific prompts + + // AuthToken prompts the user to enter an authentication token. AuthToken() (string, error) - ConfirmDeletion(string) error + // ConfirmDeletion prompts the user to confirm deletion of a resource by + // typing the requiredValue. + ConfirmDeletion(requiredValue string) error + // InputHostname prompts the user to enter a hostname. InputHostname() (string, error) - MarkdownEditor(string, string, bool) (string, error) + // MarkdownEditor prompts the user to edit a markdown document in an editor. + // If blankAllowed is true, the user can skip the editor and an empty string + // will be returned. + MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error) } func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { From a30df14b6abac659f2633b50b03e3b1de665598b Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:08:16 -0600 Subject: [PATCH 053/116] refactor(prompter): rename env var for speech synthesizer friendly prompter --- internal/prompter/prompter.go | 2 +- .../prompter/speech_synthesizer_friendly_prompter_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 317e9094a0a..86408c64520 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -43,7 +43,7 @@ type Prompter interface { } func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { - accessiblePrompterValue := os.Getenv("GH_SCREENREADER_FRIENDLY") + accessiblePrompterValue := os.Getenv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER") switch accessiblePrompterValue { case "", "false", "0", "no": return &surveyPrompter{ diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index b8d4571f6a5..21d126f75f3 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -39,6 +39,8 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { testCloser(t, console) }) + // Using OS here because huh currently ignores configured iostreams + // See https://github.com/charmbracelet/huh/issues/612 stdIn := os.Stdin stdOut := os.Stdout stdErr := os.Stderr @@ -53,9 +55,7 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { os.Stdout = console.Tty() os.Stderr = console.Tty() - // Using OS here because huh currently ignores configured iostreams - // See https://github.com/charmbracelet/huh/issues/612 - t.Setenv("GH_SCREENREADER_FRIENDLY", "true") + t.Setenv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER", "true") p := prompter.New("", nil, nil, nil) t.Run("Select", func(t *testing.T) { From addbc6ac5c0dc3ed4611904972d5b122e4426756 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 4 Apr 2025 11:02:44 -0400 Subject: [PATCH 054/116] Add label color env var to help topic, unused fix This commit adds the new environment variable to the `gh environment` help topic. Additionally, there is a small fix for Go linter for an unused variable raised as a problem. --- pkg/cmd/root/help.go | 2 +- pkg/cmd/root/help_topic.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index a7daa7b847f..7f8fb1c2e86 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -87,7 +87,7 @@ func isRootCmd(command *cobra.Command) bool { return command != nil && !command.HasParent() } -func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) { +func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) { flags := command.Flags() if isRootCmd(command) { diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index db0ef098d18..51fb0662a17 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -81,6 +81,9 @@ var HelpTopics = []helpTopic{ %[1]sCLICOLOR_FORCE%[1]s: set to a value other than %[1]s0%[1]s to keep ANSI colors in output even when the output is piped. + %[1]sGH_COLOR_LABELS%[1]s: set to any value to display labels using their RGB hex color codes in terminals that + support truecolor. + %[1]sGH_FORCE_TTY%[1]s: set to any value to force terminal-style output even when the output is redirected. When the value is a number, it is interpreted as the number of columns available in the viewport. When the value is a percentage, it will be applied against From e067eacd8114e0880c79e256eb11a119b647a9de Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 4 Apr 2025 11:57:37 -0400 Subject: [PATCH 055/116] Refactor ColorScheme initializer This commit completely removes the iostreams.NewColorScheme() initializer function in favor of exporting the type fields for greater clarity in its use. The result being code specifying only the fields that matter to test cases. --- internal/tableprinter/table_printer.go | 2 +- pkg/cmd/gist/list/list_test.go | 27 ++-- pkg/iostreams/color.go | 77 +++++------- pkg/iostreams/color_test.go | 168 +++++++++++++++++++------ pkg/iostreams/iostreams.go | 9 +- 5 files changed, 189 insertions(+), 94 deletions(-) diff --git a/internal/tableprinter/table_printer.go b/internal/tableprinter/table_printer.go index 69b22be122c..47128afb4bd 100644 --- a/internal/tableprinter/table_printer.go +++ b/internal/tableprinter/table_printer.go @@ -73,7 +73,7 @@ func NewWithWriter(w io.Writer, isTTY bool, maxWidth int, cs *iostreams.ColorSch // was not padded. In tests cs.Enabled() is false which allows us to avoid having to fix up // numerous tests that verify header padding. var paddingFunc func(int, string) string - if cs.Enabled() { + if cs.Enabled { paddingFunc = text.PadRight } diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 8fbf4d6c920..14351418f51 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -654,50 +654,57 @@ func Test_highlightMatch(t *testing.T) { tests := []struct { name string input string - color bool + cs *iostreams.ColorScheme want string }{ { name: "single match", input: "Octo", + cs: &iostreams.ColorScheme{}, want: "Octo", }, { name: "single match (color)", input: "Octo", - color: true, - want: "\x1b[0;30;43mOcto\x1b[0m", + cs: &iostreams.ColorScheme{ + Enabled: true, + }, + want: "\x1b[0;30;43mOcto\x1b[0m", }, { name: "single match with extra", input: "Hello, Octocat!", + cs: &iostreams.ColorScheme{}, want: "Hello, Octocat!", }, { name: "single match with extra (color)", input: "Hello, Octocat!", - color: true, - want: "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m", + cs: &iostreams.ColorScheme{ + Enabled: true, + }, + want: "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m", }, { name: "multiple matches", input: "Octocat/octo", + cs: &iostreams.ColorScheme{}, want: "Octocat/octo", }, { name: "multiple matches (color)", input: "Octocat/octo", - color: true, - want: "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m", + cs: &iostreams.ColorScheme{ + Enabled: true, + }, + want: "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cs := iostreams.NewColorScheme(tt.color, false, false, false, false, iostreams.NoTheme) - matched := false - got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight) + got, err := highlightMatch(tt.input, regex, &matched, tt.cs.Blue, tt.cs.Highlight) assert.NoError(t, err) assert.True(t, matched) assert.Equal(t, tt.want, got) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 92b94c3609a..86512df4e5c 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -41,35 +41,24 @@ var ( } ) -// NewColorScheme initializes color logic based on provided terminal capabilities. -// Logic dealing with terminal theme detected, such as whether color is enabled, 8-bit color supported, true color supported, -// labels are colored, and terminal theme detected. -func NewColorScheme(enabled, is256enabled, trueColor, accessibleColors, colorLabels bool, theme string) *ColorScheme { - return &ColorScheme{ - enabled: enabled, - is256enabled: is256enabled, - hasTrueColor: trueColor, - accessibleColors: accessibleColors, - colorLabels: colorLabels, - theme: theme, - } -} - +// ColorScheme controls how text is colored based upon terminal capabilities and user preferences. type ColorScheme struct { - enabled bool - is256enabled bool - hasTrueColor bool - accessibleColors bool - colorLabels bool - theme string -} - -func (c *ColorScheme) Enabled() bool { - return c.enabled + // Enabled is whether color is used at all. + Enabled bool + // EightBitColor is whether the terminal supports 8-bit, 256 colors. + EightBitColor bool + // TrueColor is whether the terminal supports 24-bit, 16 million colors. + TrueColor bool + // Accessible is whether colors must be base 16 colors that users can customize in terminal preferences. + Accessible bool + // ColorLabels is whether labels are colored based on their truecolor RGB hex color. + ColorLabels bool + // Theme is the terminal background color theme used to contextually color text for light, dark, or none at all. + Theme string } func (c *ColorScheme) Bold(t string) string { - if !c.enabled { + if !c.Enabled { return t } return bold(t) @@ -81,16 +70,16 @@ func (c *ColorScheme) Boldf(t string, args ...interface{}) string { func (c *ColorScheme) Muted(t string) string { // Fallback to previous logic if accessible colors preview is disabled. - if !c.accessibleColors { + if !c.Accessible { return c.Gray(t) } // Muted text is only stylized if color is enabled. - if !c.enabled { + if !c.Enabled { return t } - switch c.theme { + switch c.Theme { case LightTheme: return lightThemeMuted(t) case DarkTheme: @@ -105,7 +94,7 @@ func (c *ColorScheme) Mutedf(t string, args ...interface{}) string { } func (c *ColorScheme) Red(t string) string { - if !c.enabled { + if !c.Enabled { return t } return red(t) @@ -116,7 +105,7 @@ func (c *ColorScheme) Redf(t string, args ...interface{}) string { } func (c *ColorScheme) Yellow(t string) string { - if !c.enabled { + if !c.Enabled { return t } return yellow(t) @@ -127,7 +116,7 @@ func (c *ColorScheme) Yellowf(t string, args ...interface{}) string { } func (c *ColorScheme) Green(t string) string { - if !c.enabled { + if !c.Enabled { return t } return green(t) @@ -138,7 +127,7 @@ func (c *ColorScheme) Greenf(t string, args ...interface{}) string { } func (c *ColorScheme) GreenBold(t string) string { - if !c.enabled { + if !c.Enabled { return t } return greenBold(t) @@ -146,10 +135,10 @@ func (c *ColorScheme) GreenBold(t string) string { // Use Muted instead for thematically contrasting color. func (c *ColorScheme) Gray(t string) string { - if !c.enabled { + if !c.Enabled { return t } - if c.is256enabled { + if c.EightBitColor { return gray256(t) } return gray(t) @@ -161,7 +150,7 @@ func (c *ColorScheme) Grayf(t string, args ...interface{}) string { } func (c *ColorScheme) Magenta(t string) string { - if !c.enabled { + if !c.Enabled { return t } return magenta(t) @@ -172,7 +161,7 @@ func (c *ColorScheme) Magentaf(t string, args ...interface{}) string { } func (c *ColorScheme) Cyan(t string) string { - if !c.enabled { + if !c.Enabled { return t } return cyan(t) @@ -183,14 +172,14 @@ func (c *ColorScheme) Cyanf(t string, args ...interface{}) string { } func (c *ColorScheme) CyanBold(t string) string { - if !c.enabled { + if !c.Enabled { return t } return cyanBold(t) } func (c *ColorScheme) Blue(t string) string { - if !c.enabled { + if !c.Enabled { return t } return blue(t) @@ -221,7 +210,7 @@ func (c *ColorScheme) FailureIconWithColor(colo func(string) string) string { } func (c *ColorScheme) HighlightStart() string { - if !c.enabled { + if !c.Enabled { return "" } @@ -229,7 +218,7 @@ func (c *ColorScheme) HighlightStart() string { } func (c *ColorScheme) Highlight(t string) string { - if !c.enabled { + if !c.Enabled { return t } @@ -237,7 +226,7 @@ func (c *ColorScheme) Highlight(t string) string { } func (c *ColorScheme) Reset() string { - if !c.enabled { + if !c.Enabled { return "" } @@ -275,7 +264,7 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string { // Label stylizes text based on label's RGB hex color. func (c *ColorScheme) Label(hex string, x string) string { - if !c.enabled || !c.hasTrueColor || !c.colorLabels || len(hex) != 6 { + if !c.Enabled || !c.TrueColor || !c.ColorLabels || len(hex) != 6 { return x } @@ -287,11 +276,11 @@ func (c *ColorScheme) Label(hex string, x string) string { func (c *ColorScheme) TableHeader(t string) string { // Table headers are only stylized if color is enabled including underline modifier. - if !c.enabled { + if !c.Enabled { return t } - switch c.theme { + switch c.Theme { case DarkTheme: return darkThemeTableHeader(t) case LightTheme: diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index 2adacd63fed..f6a72e2a71c 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -20,35 +20,52 @@ func TestLabel(t *testing.T) { hex: "fc0303", text: "red", wants: "\033[38;2;252;3;3mred\033[0m", - cs: NewColorScheme(true, true, true, false, true, NoTheme), + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + ColorLabels: true, + }, }, { name: "no truecolor", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, false, false, true, NoTheme), + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + ColorLabels: true, + }, }, { name: "no color", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, false, true, NoTheme), + cs: &ColorScheme{ + ColorLabels: true, + }, }, { name: "invalid hex", hex: "fc0", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, false, true, NoTheme), + cs: &ColorScheme{ + ColorLabels: true, + }, }, { name: "no color labels", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, true, false, false, NoTheme), + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + ColorLabels: true, + }, }, } @@ -71,62 +88,110 @@ func TestTableHeader(t *testing.T) { expected string }{ { - name: "when color is disabled, text is not stylized", - cs: NewColorScheme(false, false, false, true, false, NoTheme), + name: "when color is disabled, text is not stylized", + cs: &ColorScheme{ + Accessible: true, + Theme: NoTheme, + }, input: "this should not be stylized", expected: "this should not be stylized", }, { - name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, false, false, true, false, NoTheme), + name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: &ColorScheme{ + Enabled: true, + Accessible: true, + Theme: NoTheme, + }, input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { - name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, false, false, true, false, LightTheme), + name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: &ColorScheme{ + Enabled: true, + Accessible: true, + Theme: LightTheme, + }, input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { - name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, false, false, true, false, DarkTheme), + name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used", + cs: &ColorScheme{ + Enabled: true, + Accessible: true, + Theme: DarkTheme, + }, input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, { - name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, true, false, true, false, NoTheme), + name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + Accessible: true, + Theme: NoTheme, + }, input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { - name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, true, false, true, false, LightTheme), + name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + Accessible: true, + Theme: LightTheme, + }, input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { - name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, true, false, true, false, DarkTheme), + name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + Accessible: true, + Theme: DarkTheme, + }, input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, { - name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, true, true, true, false, NoTheme), + name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: NoTheme, + }, input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { - name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, true, true, true, false, LightTheme), + name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: LightTheme, + }, input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { - name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, true, true, true, false, DarkTheme), + name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: DarkTheme, + }, input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, @@ -154,43 +219,70 @@ func TestMuted(t *testing.T) { }{ { name: "when color is disabled but accessible colors are disabled, text is not stylized", - cs: NewColorScheme(false, false, false, false, false, NoTheme), + cs: &ColorScheme{}, input: "this should not be stylized", expected: "this should not be stylized", }, { - name: "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used", - cs: NewColorScheme(true, false, false, false, false, NoTheme), + name: "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used", + cs: &ColorScheme{ + Enabled: true, + }, input: "this should be 4-bit gray", expected: fmt.Sprintf("%sthis should be 4-bit gray%s", gray4bit, reset), }, { - name: "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", - cs: NewColorScheme(true, true, false, false, false, NoTheme), + name: "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + }, input: "this should be 8-bit gray", expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset), }, { - name: "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", - cs: NewColorScheme(true, true, true, false, false, NoTheme), + name: "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + }, input: "this should be 8-bit gray", expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset), }, { - name: "when 4-bit color is enabled and theme is dark, 4-bit light color is used", - cs: NewColorScheme(true, true, true, true, false, DarkTheme), + name: "when 4-bit color is enabled and theme is dark, 4-bit light color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: DarkTheme, + }, input: "this should be 4-bit dim black", expected: fmt.Sprintf("%sthis should be 4-bit dim black%s", dimBlack4bit, reset), }, { - name: "when 4-bit color is enabled and theme is light, 4-bit dark color is used", - cs: NewColorScheme(true, true, true, true, false, LightTheme), + name: "when 4-bit color is enabled and theme is light, 4-bit dark color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: LightTheme, + }, input: "this should be 4-bit bright black", expected: fmt.Sprintf("%sthis should be 4-bit bright black%s", brightBlack4bit, reset), }, { - name: "when 4-bit color is enabled but no theme, 4-bit default color is used", - cs: NewColorScheme(true, true, true, true, false, NoTheme), + name: "when 4-bit color is enabled but no theme, 4-bit default color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: NoTheme, + }, input: "this should have no explicit color", expected: "this should have no explicit color", }, diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 28e81abbaa5..f5e3c2aee39 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -376,7 +376,14 @@ func (s *IOStreams) TerminalWidth() int { } func (s *IOStreams) ColorScheme() *ColorScheme { - return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.AccessibleColorsEnabled(), s.ColorLabels(), s.TerminalTheme()) + return &ColorScheme{ + Enabled: s.ColorEnabled(), + EightBitColor: s.ColorSupport256(), + TrueColor: s.HasTrueColor(), + Accessible: s.AccessibleColorsEnabled(), + ColorLabels: s.ColorLabels(), + Theme: s.TerminalTheme(), + } } func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) { From 5b0d49c6ecf9ef704a3f36d6701e81412b0e54f7 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:06:40 -0600 Subject: [PATCH 056/116] test(prompter): more tests for bad input --- internal/prompter/prompter.go | 3 +- ...eech_synthesizer_friendly_prompter_test.go | 58 ++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 86408c64520..f9146d8b74b 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -174,6 +174,7 @@ func (p *speechSynthesizerFriendlyPrompter) AuthToken() (string, error) { huh.NewGroup( huh.NewInput(). Title("Paste your authentication token:"). + // Note: if this validation fails, the prompt loops. Validate(func(input string) error { if input == "" { return fmt.Errorf("token is required") @@ -229,7 +230,7 @@ func (p *speechSynthesizerFriendlyPrompter) InputHostname() (string, error) { func (p *speechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { var result string options := []huh.Option[string]{ - huh.NewOption("Open Editor", "open"), + huh.NewOption(fmt.Sprintf("Open Editor: %s", p.editorCmd), "open"), } if blankAllowed { options = append(options, huh.NewOption("Skip", "skip")) diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 21d126f75f3..119106f6455 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -152,19 +152,45 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { // TODO: Need one that enters invalid input // TODO: write tests for control-c t.Run("AuthToken", func(t *testing.T) { + dummyAuthToken := "12345abcdefg" go func() { // Wait for prompt to appear _, err := console.ExpectString("Paste your authentication token:") require.NoError(t, err) - // Enter a number - _, err = console.SendLine("12345abcdefg") + // Enter some dummy auth token + _, err = console.SendLine(dummyAuthToken) + require.NoError(t, err) + }() + + authValue, err := p.AuthToken() + require.NoError(t, err) + require.Equal(t, dummyAuthToken, authValue) + }) + + t.Run("AuthToken - blank input returns error", func(t *testing.T) { + dummyAuthTokenForAfterFailure := "12345abcdefg" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Paste your authentication token:") + require.NoError(t, err) + + // Enter nothing + _, err = console.SendLine("") + require.NoError(t, err) + + // Expect an error message + _, err = console.ExpectString("token is required") + require.NoError(t, err) + + // Now enter some dummy auth token to return control back to the test + _, err = console.SendLine(dummyAuthTokenForAfterFailure) require.NoError(t, err) }() authValue, err := p.AuthToken() require.NoError(t, err) - require.Equal(t, "12345abcdefg", authValue) + require.Equal(t, dummyAuthTokenForAfterFailure, authValue) }) t.Run("ConfirmDeletion", func(t *testing.T) { @@ -184,6 +210,32 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { require.NoError(t, err) }) + t.Run("ConfirmDeletion - bad input", func(t *testing.T) { + requiredValue := "test" + badInputValue := "garbage" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue)) + require.NoError(t, err) + + // Confirm with bad input + _, err = console.SendLine(badInputValue) + require.NoError(t, err) + + // Expect an error message and loop back to the prompt + _, err = console.ExpectString(fmt.Sprintf("You entered: %q", badInputValue)) + require.NoError(t, err) + + // Confirm with the correct input to return control back to the test + _, err = console.SendLine(requiredValue) + require.NoError(t, err) + }() + + // An err indicates that the confirmation text sent did not match + err := p.ConfirmDeletion(requiredValue) + require.NoError(t, err) + }) + t.Run("InputHostname", func(t *testing.T) { hostname := "somethingdoesnotmatter.com" go func() { From 4cf048a8d17bf87ee9717756c9877fb5d5b77169 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:33:10 -0600 Subject: [PATCH 057/116] fix(prompter): input returns default when blank --- internal/prompter/prompter.go | 5 +++++ ...eech_synthesizer_friendly_prompter_test.go | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index f9146d8b74b..23f967e13f4 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -125,6 +125,7 @@ func (p *speechSynthesizerFriendlyPrompter) MultiSelect(prompt string, defaults func (p *speechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) (string, error) { result := defaultValue + prompt = fmt.Sprintf("%s (%s)", prompt, defaultValue) form := p.newForm( huh.NewGroup( huh.NewInput(). @@ -134,6 +135,10 @@ func (p *speechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) ( ) err := form.Run() + + if result == "" { + return defaultValue, nil + } return result, err } diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 119106f6455..8f41de9738b 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -116,6 +116,28 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { assert.Equal(t, dummyText, inputValue) }) + t.Run("Input - blank input returns default value", func(t *testing.T) { + dummyDefaultValue := "12345abcdefg" + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Enter some characters") + require.NoError(t, err) + + // Enter nothing + _, err = console.SendLine("") + require.NoError(t, err) + + // Expect the default value to be returned + _, err = console.ExpectString(dummyDefaultValue) + require.NoError(t, err) + }() + + inputValue, err := p.Input("Enter some characters", dummyDefaultValue) + require.NoError(t, err) + + assert.Equal(t, dummyDefaultValue, inputValue) + }) + t.Run("Password", func(t *testing.T) { dummyPassword := "12345abcdefg" go func() { From 5c39e0bd10bc33808411e62ddda679131e9217b4 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 4 Apr 2025 14:52:25 -0600 Subject: [PATCH 058/116] fix(prompter): notes about Confirm default --- internal/prompter/prompter.go | 7 +++++-- ...eech_synthesizer_friendly_prompter_test.go | 21 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 23f967e13f4..2e6f7fe4f73 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -158,8 +158,11 @@ func (p *speechSynthesizerFriendlyPrompter) Password(prompt string) (string, err return result, err } -func (p *speechSynthesizerFriendlyPrompter) Confirm(prompt string, _ bool) (bool, error) { - var result bool +func (p *speechSynthesizerFriendlyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) { + // This is currently an inneffectual assignment because the value is + // not respected as the default in accessible mode. + // See https://github.com/charmbracelet/huh/issues/615 + result := defaultValue form := p.newForm( huh.NewGroup( huh.NewConfirm(). diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 8f41de9738b..4091db33aaa 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -171,8 +171,25 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { require.Equal(t, true, confirmValue) }) - // TODO: Need one that enters invalid input - // TODO: write tests for control-c + // This test currently fails because the value is + // not respected as the default in accessible mode. + // See https://github.com/charmbracelet/huh/issues/615 + // t.Run("Confirm - blank input returns default", func(t *testing.T) { + // go func() { + // // Wait for prompt to appear + // _, err := console.ExpectString("Are you sure") + // require.NoError(t, err) + + // // Enter nothing + // _, err = console.SendLine("") + // require.NoError(t, err) + // }() + + // confirmValue, err := p.Confirm("Are you sure", false) + // require.NoError(t, err) + // require.Equal(t, false, confirmValue) + // }) + t.Run("AuthToken", func(t *testing.T) { dummyAuthToken := "12345abcdefg" go func() { From 2e48cadf581b122860e4c9e2f83d0dbf588fe0a6 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:05:23 -0600 Subject: [PATCH 059/116] fix(prompter): remove impossible condition --- internal/prompter/prompter.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 2e6f7fe4f73..fc2b3fe3207 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -258,9 +258,6 @@ func (p *speechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue } if result == "skip" { - if !blankAllowed && defaultValue == "" { - panic("blank not allowed and no default value") - } return "", nil } From 0b49522467c9122cb7644fb007f5d22db72874e3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:07:21 -0600 Subject: [PATCH 060/116] refactor(prompter): less magic strings --- internal/prompter/prompter.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index fc2b3fe3207..67ae0fa6a77 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -237,11 +237,13 @@ func (p *speechSynthesizerFriendlyPrompter) InputHostname() (string, error) { func (p *speechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { var result string + skipOption := "skip" + openOption := "open" options := []huh.Option[string]{ - huh.NewOption(fmt.Sprintf("Open Editor: %s", p.editorCmd), "open"), + huh.NewOption(fmt.Sprintf("Open Editor: %s", p.editorCmd), openOption), } if blankAllowed { - options = append(options, huh.NewOption("Skip", "skip")) + options = append(options, huh.NewOption("Skip", skipOption)) } form := p.newForm( @@ -257,10 +259,11 @@ func (p *speechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue return "", err } - if result == "skip" { + if result == skipOption { return "", nil } + // openOption was selected text, err := surveyext.Edit(p.editorCmd, "*.md", defaultValue, p.stdin, p.stdout, p.stderr) if err != nil { return "", err From f89700160b3fcc8f7c4d2f2ef3128063fe2cd8fc Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:08:17 -0600 Subject: [PATCH 061/116] doc(prompter): clarify comments --- internal/prompter/prompter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 67ae0fa6a77..0a42a8df98a 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -160,7 +160,8 @@ func (p *speechSynthesizerFriendlyPrompter) Password(prompt string) (string, err func (p *speechSynthesizerFriendlyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) { // This is currently an inneffectual assignment because the value is - // not respected as the default in accessible mode. + // not respected as the default in accessible mode. Leaving this in here + // because it may change in the future. // See https://github.com/charmbracelet/huh/issues/615 result := defaultValue form := p.newForm( From e21243fe9b4db0f4ba837caa149a5120d7209773 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 4 Apr 2025 21:45:54 -0600 Subject: [PATCH 062/116] ci: pin third party actions to commit sha --- .github/workflows/deployment.yml | 10 +++++----- .github/workflows/homebrew-bump.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 60354a95322..a7b03f40d20 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -50,7 +50,7 @@ jobs: with: go-version-file: 'go.mod' - name: Install GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 with: version: "~1.17.1" install-only: true @@ -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@v6 + uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 with: version: "~1.17.1" install-only: true @@ -157,7 +157,7 @@ jobs: with: go-version-file: 'go.mod' - name: Install GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 with: version: "~1.17.1" install-only: true @@ -196,7 +196,7 @@ jobs: run: script/release --local "$TAG_NAME" --platform windows - name: Set up MSBuild id: setupmsbuild - uses: microsoft/setup-msbuild@v2.0.0 + uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce - name: Build MSI shell: bash env: @@ -384,7 +384,7 @@ jobs: git diff --name-status @{upstream}.. fi - name: Bump homebrew-core formula - uses: mislav/bump-homebrew-formula-action@v3 + uses: mislav/bump-homebrew-formula-action@942e550c6344cfdb9e1ab29b9bb9bf0c43efa19b if: inputs.environment == 'production' && !contains(inputs.tag_name, '-') with: formula-name: gh diff --git a/.github/workflows/homebrew-bump.yml b/.github/workflows/homebrew-bump.yml index 2bc395a1eb4..228f1a3451b 100644 --- a/.github/workflows/homebrew-bump.yml +++ b/.github/workflows/homebrew-bump.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Bump homebrew-core formula - uses: mislav/bump-homebrew-formula-action@v3 + uses: mislav/bump-homebrew-formula-action@942e550c6344cfdb9e1ab29b9bb9bf0c43efa19b if: inputs.environment == 'production' && !contains(inputs.tag_name, '-') with: formula-name: gh From 918cafc222aa328c1fc932672c51027f9b39060d Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Sun, 6 Apr 2025 10:18:48 -0400 Subject: [PATCH 063/116] Deprecate ColorScheme.Gray for ColorScheme.Muted This commit converts all of the places using ColorScheme.Gray and ColorScheme.Grayf to Muted and Mutedf. There is a little extra tidying up with local variable names or converting code to use Mutedf format. --- pkg/cmd/gist/create/create.go | 2 +- pkg/cmd/gist/shared/shared.go | 2 +- pkg/cmd/gist/view/view.go | 4 ++-- pkg/cmd/issue/shared/display.go | 4 ++-- pkg/cmd/issue/view/view.go | 4 ++-- pkg/cmd/pr/checks/output.go | 2 +- pkg/cmd/pr/create/create.go | 26 +++++++++++++------------- pkg/cmd/pr/review/review.go | 2 +- pkg/cmd/pr/shared/comments.go | 8 ++++---- pkg/cmd/pr/shared/display.go | 4 ++-- pkg/cmd/pr/status/status.go | 2 +- pkg/cmd/pr/view/view.go | 4 ++-- pkg/cmd/release/view/view.go | 16 ++++++++-------- pkg/cmd/repo/license/view/view.go | 6 +++--- pkg/cmd/repo/view/view.go | 6 +++--- pkg/cmd/run/shared/presentation.go | 2 +- pkg/cmd/run/shared/shared.go | 2 +- pkg/cmd/run/view/view.go | 6 +++--- pkg/cmd/search/commits/commits.go | 2 +- pkg/cmd/search/repos/repos.go | 4 ++-- pkg/cmd/search/shared/shared.go | 2 +- pkg/cmd/status/status.go | 2 +- pkg/cmd/workflow/view/view.go | 2 +- pkg/iostreams/color.go | 6 +++--- 24 files changed, 60 insertions(+), 60 deletions(-) diff --git a/pkg/cmd/gist/create/create.go b/pkg/cmd/gist/create/create.go index 5392b997e6a..4f51bed255d 100644 --- a/pkg/cmd/gist/create/create.go +++ b/pkg/cmd/gist/create/create.go @@ -138,7 +138,7 @@ func createRun(opts *CreateOptions) error { processMessage = fmt.Sprintf("Creating gist %s", gistName) } } - fmt.Fprintf(errOut, "%s %s\n", cs.Gray("-"), processMessage) + fmt.Fprintf(errOut, "%s %s\n", cs.Muted("-"), processMessage) httpClient, err := opts.HttpClient() if err != nil { diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go index 53b577e4cb0..fc63f56ce89 100644 --- a/pkg/cmd/gist/shared/shared.go +++ b/pkg/cmd/gist/shared/shared.go @@ -230,7 +230,7 @@ func PromptGists(prompter prompter.Prompter, client *http.Client, host string, c for i, gist := range gists { gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt) // TODO: support dynamic maxWidth - opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Gray(gistTime)) + opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Muted(gistTime)) } result, err := prompter.Select("Select a gist", "", opts) diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index 705f8f703a7..f789c5b048c 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -140,7 +140,7 @@ func viewRun(opts *ViewOptions) error { if len(gist.Files) == 1 || opts.Filename != "" { return fmt.Errorf("error: file is binary") } - _, err = fmt.Fprintln(opts.IO.Out, cs.Gray("(skipping rendering binary content)")) + _, err = fmt.Fprintln(opts.IO.Out, cs.Muted("(skipping rendering binary content)")) return nil } @@ -197,7 +197,7 @@ func viewRun(opts *ViewOptions) error { for i, fn := range filenames { if showFilenames { - fmt.Fprintf(opts.IO.Out, "%s\n\n", cs.Gray(fn)) + fmt.Fprintf(opts.IO.Out, "%s\n\n", cs.Muted(fn)) } if err := render(gist.Files[fn]); err != nil { return err diff --git a/pkg/cmd/issue/shared/display.go b/pkg/cmd/issue/shared/display.go index 0c56ffd2cae..08ec484d66c 100644 --- a/pkg/cmd/issue/shared/display.go +++ b/pkg/cmd/issue/shared/display.go @@ -38,13 +38,13 @@ func PrintIssues(io *iostreams.IOStreams, now time.Time, prefix string, totalCou } table.AddField(text.RemoveExcessiveWhitespace(issue.Title)) table.AddField(issueLabelList(&issue, cs, isTTY)) - table.AddTimeField(now, issue.UpdatedAt, cs.Gray) + table.AddTimeField(now, issue.UpdatedAt, cs.Muted) table.EndRow() } _ = table.Render() remaining := totalCount - len(issues) if remaining > 0 { - fmt.Fprintf(io.Out, cs.Gray("%sAnd %d more\n"), prefix, remaining) + fmt.Fprintf(io.Out, cs.Muted("%sAnd %d more\n"), prefix, remaining) } } diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go index b188c6a4ca3..f61c603b4a4 100644 --- a/pkg/cmd/issue/view/view.go +++ b/pkg/cmd/issue/view/view.go @@ -228,7 +228,7 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue var md string var err error if issue.Body == "" { - md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided")) + md = fmt.Sprintf("\n %s\n\n", cs.Muted("No description provided")) } else { md, err = markdown.Render(issue.Body, markdown.WithTheme(opts.IO.TerminalTheme()), @@ -250,7 +250,7 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue } // Footer - fmt.Fprintf(out, cs.Gray("View this issue on GitHub: %s\n"), issue.URL) + fmt.Fprintf(out, cs.Muted("View this issue on GitHub: %s\n"), issue.URL) return nil } diff --git a/pkg/cmd/pr/checks/output.go b/pkg/cmd/pr/checks/output.go index 5d2f080c668..24105c3e226 100644 --- a/pkg/cmd/pr/checks/output.go +++ b/pkg/cmd/pr/checks/output.go @@ -30,7 +30,7 @@ func addRow(tp *tableprinter.TablePrinter, io *iostreams.IOStreams, o check) { markColor = cs.Yellow case "skipping", "cancel": mark = "-" - markColor = cs.Gray + markColor = cs.Muted } if io.IsStdoutTTY() { diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 0066bbc6e02..5f8979c11c1 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -879,37 +879,37 @@ func renderPullRequestPlain(w io.Writer, params map[string]interface{}, state *s } func renderPullRequestTTY(io *iostreams.IOStreams, params map[string]interface{}, state *shared.IssueMetadataState) error { - iofmt := io.ColorScheme() + cs := io.ColorScheme() out := io.Out fmt.Fprint(out, "Would have created a Pull Request with:\n") - fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Title"), params["title"].(string)) - fmt.Fprintf(out, "%s: %t\n", iofmt.Bold("Draft"), params["draft"]) - fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Base"), params["baseRefName"]) - fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Head"), params["headRefName"]) + fmt.Fprintf(out, "%s: %s\n", cs.Bold("Title"), params["title"].(string)) + fmt.Fprintf(out, "%s: %t\n", cs.Bold("Draft"), params["draft"]) + fmt.Fprintf(out, "%s: %s\n", cs.Bold("Base"), params["baseRefName"]) + fmt.Fprintf(out, "%s: %s\n", cs.Bold("Head"), params["headRefName"]) if len(state.Labels) != 0 { - fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Labels"), strings.Join(state.Labels, ", ")) + fmt.Fprintf(out, "%s: %s\n", cs.Bold("Labels"), strings.Join(state.Labels, ", ")) } if len(state.Reviewers) != 0 { - fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Reviewers"), strings.Join(state.Reviewers, ", ")) + fmt.Fprintf(out, "%s: %s\n", cs.Bold("Reviewers"), strings.Join(state.Reviewers, ", ")) } if len(state.Assignees) != 0 { - fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Assignees"), strings.Join(state.Assignees, ", ")) + fmt.Fprintf(out, "%s: %s\n", cs.Bold("Assignees"), strings.Join(state.Assignees, ", ")) } if len(state.Milestones) != 0 { - fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Milestones"), strings.Join(state.Milestones, ", ")) + fmt.Fprintf(out, "%s: %s\n", cs.Bold("Milestones"), strings.Join(state.Milestones, ", ")) } if len(state.Projects) != 0 { - fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Projects"), strings.Join(state.Projects, ", ")) + fmt.Fprintf(out, "%s: %s\n", cs.Bold("Projects"), strings.Join(state.Projects, ", ")) } - fmt.Fprintf(out, "%s: %t\n", iofmt.Bold("MaintainerCanModify"), params["maintainerCanModify"]) + fmt.Fprintf(out, "%s: %t\n", cs.Bold("MaintainerCanModify"), params["maintainerCanModify"]) - fmt.Fprintf(out, "%s\n", iofmt.Bold("Body:")) + fmt.Fprintf(out, "%s\n", cs.Bold("Body:")) // Body var md string var err error if len(params["body"].(string)) == 0 { - md = fmt.Sprintf("%s\n", iofmt.Gray("No description provided")) + md = fmt.Sprintf("%s\n", cs.Muted("No description provided")) } else { md, err = markdown.Render(params["body"].(string), markdown.WithTheme(io.TerminalTheme()), diff --git a/pkg/cmd/pr/review/review.go b/pkg/cmd/pr/review/review.go index 25f81d97387..cafa6ce8fef 100644 --- a/pkg/cmd/pr/review/review.go +++ b/pkg/cmd/pr/review/review.go @@ -191,7 +191,7 @@ func reviewRun(opts *ReviewOptions) error { switch reviewData.State { case api.ReviewComment: - fmt.Fprintf(opts.IO.ErrOut, "%s Reviewed pull request %s#%d\n", cs.Gray("-"), ghrepo.FullName(baseRepo), pr.Number) + fmt.Fprintf(opts.IO.ErrOut, "%s Reviewed pull request %s#%d\n", cs.Muted("-"), ghrepo.FullName(baseRepo), pr.Number) case api.ReviewApprove: fmt.Fprintf(opts.IO.ErrOut, "%s Approved pull request %s#%d\n", cs.SuccessIcon(), ghrepo.FullName(baseRepo), pr.Number) case api.ReviewRequestChanges: diff --git a/pkg/cmd/pr/shared/comments.go b/pkg/cmd/pr/shared/comments.go index a05108d7ba1..7c6e9154c80 100644 --- a/pkg/cmd/pr/shared/comments.go +++ b/pkg/cmd/pr/shared/comments.go @@ -62,7 +62,7 @@ func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.Pul hiddenCount := totalCount - retrievedCount if preview && hiddenCount > 0 { - fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Not showing %s ————————", text.Pluralize(hiddenCount, "comment")))) + fmt.Fprint(&b, cs.Muted(fmt.Sprintf("———————— Not showing %s ————————", text.Pluralize(hiddenCount, "comment")))) fmt.Fprintf(&b, "\n\n\n") } @@ -79,7 +79,7 @@ func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.Pul } if preview && hiddenCount > 0 { - fmt.Fprint(&b, cs.Gray("Use --comments to view the full conversation")) + fmt.Fprint(&b, cs.Muted("Use --comments to view the full conversation")) fmt.Fprintln(&b) } @@ -122,7 +122,7 @@ func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (strin var md string var err error if comment.Content() == "" { - md = fmt.Sprintf("\n %s\n\n", cs.Gray("No body provided")) + md = fmt.Sprintf("\n %s\n\n", cs.Muted("No body provided")) } else { md, err = markdown.Render(comment.Content(), markdown.WithTheme(io.TerminalTheme()), @@ -135,7 +135,7 @@ func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (strin // Footer if comment.Link() != "" { - fmt.Fprintf(&b, cs.Gray("View the full review: %s\n\n"), comment.Link()) + fmt.Fprintf(&b, cs.Muted("View the full review: %s\n\n"), comment.Link()) } return b.String(), nil diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go index 02482951c5c..b4d83c7197e 100644 --- a/pkg/cmd/pr/shared/display.go +++ b/pkg/cmd/pr/shared/display.go @@ -60,7 +60,7 @@ func PrintHeader(io *iostreams.IOStreams, s string) { } func PrintMessage(io *iostreams.IOStreams, s string) { - fmt.Fprintln(io.Out, io.ColorScheme().Gray(s)) + fmt.Fprintln(io.Out, io.ColorScheme().Muted(s)) } func ListNoResults(repoName string, itemName string, hasFilters bool) error { @@ -83,7 +83,7 @@ func ListHeader(repoName string, itemName string, matchCount int, totalMatchCoun } func PrCheckStatusSummaryWithColor(cs *iostreams.ColorScheme, checks api.PullRequestChecksStatus) string { - var summary = cs.Gray("No checks") + var summary = cs.Muted("No checks") if checks.Total > 0 { if checks.Failing > 0 { if checks.Failing == checks.Total { diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index b7b390bf27f..d20522d04af 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -316,6 +316,6 @@ func printPrs(io *iostreams.IOStreams, totalCount int, prs ...api.PullRequest) { } remaining := totalCount - len(prs) if remaining > 0 { - fmt.Fprintf(w, cs.Gray(" And %d more\n"), remaining) + fmt.Fprintf(w, cs.Muted(" And %d more\n"), remaining) } } diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go index ccad4fa77cf..ed5984b4656 100644 --- a/pkg/cmd/pr/view/view.go +++ b/pkg/cmd/pr/view/view.go @@ -260,7 +260,7 @@ func printHumanPrPreview(opts *ViewOptions, baseRepo ghrepo.Interface, pr *api.P var md string var err error if pr.Body == "" { - md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided")) + md = fmt.Sprintf("\n %s\n\n", cs.Muted("No description provided")) } else { md, err = markdown.Render(pr.Body, markdown.WithTheme(opts.IO.TerminalTheme()), @@ -282,7 +282,7 @@ func printHumanPrPreview(opts *ViewOptions, baseRepo ghrepo.Interface, pr *api.P } // Footer - fmt.Fprintf(out, cs.Gray("View this pull request on GitHub: %s\n"), pr.URL) + fmt.Fprintf(out, cs.Muted("View this pull request on GitHub: %s\n"), pr.URL) return nil } diff --git a/pkg/cmd/release/view/view.go b/pkg/cmd/release/view/view.go index a32482e65ab..db0e6ae1d46 100644 --- a/pkg/cmd/release/view/view.go +++ b/pkg/cmd/release/view/view.go @@ -129,19 +129,19 @@ func viewRun(opts *ViewOptions) error { } func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error { - iofmt := io.ColorScheme() + cs := io.ColorScheme() w := io.Out - fmt.Fprintf(w, "%s\n", iofmt.Bold(release.TagName)) + fmt.Fprintf(w, "%s\n", cs.Bold(release.TagName)) if release.IsDraft { - fmt.Fprintf(w, "%s • ", iofmt.Red("Draft")) + fmt.Fprintf(w, "%s • ", cs.Red("Draft")) } else if release.IsPrerelease { - fmt.Fprintf(w, "%s • ", iofmt.Yellow("Pre-release")) + fmt.Fprintf(w, "%s • ", cs.Yellow("Pre-release")) } if release.IsDraft { - fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt)))) + fmt.Fprintf(w, "%s\n", cs.Mutedf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt))) } else { - fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt)))) + fmt.Fprintf(w, "%s\n", cs.Mutedf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt))) } renderedDescription, err := markdown.Render(release.Body, @@ -153,7 +153,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error { fmt.Fprintln(w, renderedDescription) if len(release.Assets) > 0 { - fmt.Fprintf(w, "%s\n", iofmt.Bold("Assets")) + fmt.Fprintf(w, "%s\n", cs.Bold("Assets")) //nolint:staticcheck // SA1019: Showing NAME|SIZE headers adds nothing to table. table := tableprinter.New(io, tableprinter.NoHeader) for _, a := range release.Assets { @@ -168,7 +168,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error { fmt.Fprint(w, "\n") } - fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("View on GitHub: %s", release.URL))) + fmt.Fprintln(w, cs.Mutedf("View on GitHub: %s", release.URL)) return nil } diff --git a/pkg/cmd/repo/license/view/view.go b/pkg/cmd/repo/license/view/view.go index d3e63c241ca..14228ba3600 100644 --- a/pkg/cmd/repo/license/view/view.go +++ b/pkg/cmd/repo/license/view/view.go @@ -119,9 +119,9 @@ func renderLicense(license *api.License, opts *ViewOptions) error { cs := opts.IO.ColorScheme() var out strings.Builder if opts.IO.IsStdoutTTY() { - out.WriteString(fmt.Sprintf("\n%s\n", cs.Gray(license.Description))) - out.WriteString(fmt.Sprintf("\n%s\n", cs.Grayf("To implement: %s", license.Implementation))) - out.WriteString(fmt.Sprintf("\n%s\n\n", cs.Grayf("For more information, see: %s", license.HTMLURL))) + out.WriteString(fmt.Sprintf("\n%s\n", cs.Muted(license.Description))) + out.WriteString(fmt.Sprintf("\n%s\n", cs.Mutedf("To implement: %s", license.Implementation))) + out.WriteString(fmt.Sprintf("\n%s\n\n", cs.Mutedf("For more information, see: %s", license.HTMLURL))) } out.WriteString(license.Body) _, err := opts.IO.Out.Write([]byte(out.String())) diff --git a/pkg/cmd/repo/view/view.go b/pkg/cmd/repo/view/view.go index 06f85d04893..b13276c80ae 100644 --- a/pkg/cmd/repo/view/view.go +++ b/pkg/cmd/repo/view/view.go @@ -181,7 +181,7 @@ func viewRun(opts *ViewOptions) error { var readmeContent string if readme == nil { - readmeContent = cs.Gray("This repository does not have a README") + readmeContent = cs.Muted("This repository does not have a README") } else if isMarkdownFile(readme.Filename) { var err error readmeContent, err = markdown.Render(readme.Content, @@ -197,7 +197,7 @@ func viewRun(opts *ViewOptions) error { description := repo.Description if description == "" { - description = cs.Gray("No description provided") + description = cs.Muted("No description provided") } repoData := struct { @@ -209,7 +209,7 @@ func viewRun(opts *ViewOptions) error { FullName: cs.Bold(fullName), Description: description, Readme: readmeContent, - View: cs.Gray(fmt.Sprintf("View this repository on GitHub: %s", openURL)), + View: cs.Mutedf("View this repository on GitHub: %s", openURL), } return tmpl.Execute(stdout, repoData) diff --git a/pkg/cmd/run/shared/presentation.go b/pkg/cmd/run/shared/presentation.go index 2ec1497292c..699ea120f54 100644 --- a/pkg/cmd/run/shared/presentation.go +++ b/pkg/cmd/run/shared/presentation.go @@ -52,7 +52,7 @@ func RenderAnnotations(cs *iostreams.ColorScheme, annotations []Annotation) stri for _, a := range annotations { lines = append(lines, fmt.Sprintf("%s %s", AnnotationSymbol(cs, a), a.Message)) - lines = append(lines, cs.Grayf("%s: %s#%d\n", a.JobName, a.Path, a.StartLine)) + lines = append(lines, cs.Mutedf("%s: %s#%d\n", a.JobName, a.Path, a.StartLine)) } return strings.Join(lines, "\n") diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index ce909fd772e..8dbf59c414c 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -575,7 +575,7 @@ func Symbol(cs *iostreams.ColorScheme, status Status, conclusion Conclusion) (st case Success: return cs.SuccessIconWithColor(noColor), cs.Green case Skipped, Neutral: - return "-", cs.Gray + return "-", cs.Muted default: return cs.FailureIconWithColor(noColor), cs.Red } diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index c794cff9af2..d308962ed06 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -397,7 +397,7 @@ func runView(opts *ViewOptions) error { for _, a := range artifacts { expiredBadge := "" if a.Expired { - expiredBadge = cs.Gray(" (expired)") + expiredBadge = cs.Muted(" (expired)") } fmt.Fprintf(out, "%s%s\n", a.Name, expiredBadge) } @@ -411,7 +411,7 @@ func runView(opts *ViewOptions) error { } else { fmt.Fprintf(out, "For more information about a job, try: gh run view --job=\n") } - fmt.Fprintf(out, cs.Gray("View this run on GitHub: %s\n"), run.URL) + fmt.Fprintln(out, cs.Mutedf("View this run on GitHub: %s", run.URL)) if opts.ExitStatus && shared.IsFailureState(run.Conclusion) { return cmdutil.SilentError @@ -423,7 +423,7 @@ func runView(opts *ViewOptions) error { } else { fmt.Fprintf(out, "To see the full job log, try: gh run view --log --job=%d\n", selectedJob.ID) } - fmt.Fprintf(out, cs.Gray("View this run on GitHub: %s\n"), run.URL) + fmt.Fprintln(out, cs.Mutedf("View this run on GitHub: %s", run.URL)) if opts.ExitStatus && shared.IsFailureState(selectedJob.Conclusion) { return cmdutil.SilentError diff --git a/pkg/cmd/search/commits/commits.go b/pkg/cmd/search/commits/commits.go index bc37684a2d1..fb1742dc9da 100644 --- a/pkg/cmd/search/commits/commits.go +++ b/pkg/cmd/search/commits/commits.go @@ -161,7 +161,7 @@ func displayResults(io *iostreams.IOStreams, now time.Time, results search.Commi tp.AddField(commit.Sha) tp.AddField(text.RemoveExcessiveWhitespace(commit.Info.Message)) tp.AddField(commit.Author.Login) - tp.AddTimeField(now, commit.Info.Author.Date, cs.Gray) + tp.AddTimeField(now, commit.Info.Author.Date, cs.Muted) tp.EndRow() } if io.IsStdoutTTY() { diff --git a/pkg/cmd/search/repos/repos.go b/pkg/cmd/search/repos/repos.go index 0bad650d3b1..2815ee6dc2b 100644 --- a/pkg/cmd/search/repos/repos.go +++ b/pkg/cmd/search/repos/repos.go @@ -171,14 +171,14 @@ func displayResults(io *iostreams.IOStreams, now time.Time, results search.Repos tags = append(tags, "archived") } info := strings.Join(tags, ", ") - infoColor := cs.Gray + infoColor := cs.Muted if repo.IsPrivate { infoColor = cs.Yellow } tp.AddField(repo.FullName, tableprinter.WithColor(cs.Bold)) tp.AddField(text.RemoveExcessiveWhitespace(repo.Description)) tp.AddField(info, tableprinter.WithColor(infoColor)) - tp.AddTimeField(now, repo.UpdatedAt, cs.Gray) + tp.AddTimeField(now, repo.UpdatedAt, cs.Muted) tp.EndRow() } if io.IsStdoutTTY() { diff --git a/pkg/cmd/search/shared/shared.go b/pkg/cmd/search/shared/shared.go index f0a346fc878..aefd0c0546c 100644 --- a/pkg/cmd/search/shared/shared.go +++ b/pkg/cmd/search/shared/shared.go @@ -132,7 +132,7 @@ func displayIssueResults(io *iostreams.IOStreams, now time.Time, et EntityType, } tp.AddField(text.RemoveExcessiveWhitespace(issue.Title)) tp.AddField(listIssueLabels(&issue, cs, tp.IsTTY())) - tp.AddTimeField(now, issue.UpdatedAt, cs.Gray) + tp.AddTimeField(now, issue.UpdatedAt, cs.Muted) tp.EndRow() } diff --git a/pkg/cmd/status/status.go b/pkg/cmd/status/status.go index 0ed7dd3a7fc..c9acce8bd69 100644 --- a/pkg/cmd/status/status.go +++ b/pkg/cmd/status/status.go @@ -740,7 +740,7 @@ func statusRun(opts *StatusOptions) error { errs := sg.authErrors.ToSlice() sort.Strings(errs) for _, msg := range errs { - fmt.Fprintln(out, cs.Gray(fmt.Sprintf("warning: %s", msg))) + fmt.Fprintln(out, cs.Mutedf("warning: %s", msg)) } } diff --git a/pkg/cmd/workflow/view/view.go b/pkg/cmd/workflow/view/view.go index 188d79e2222..2e550f4963f 100644 --- a/pkg/cmd/workflow/view/view.go +++ b/pkg/cmd/workflow/view/view.go @@ -175,7 +175,7 @@ func viewWorkflowContent(opts *ViewOptions, client *api.Client, repo ghrepo.Inte out := opts.IO.Out fileName := workflow.Base() - fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Gray(fileName)) + fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Muted(fileName)) fmt.Fprintf(out, "ID: %s", cs.Cyanf("%d", workflow.ID)) codeBlock := fmt.Sprintf("```yaml\n%s\n```", yaml) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 58d13e6ef55..a92fdcab365 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -142,7 +142,7 @@ func (c *ColorScheme) GreenBold(t string) string { return greenBold(t) } -// Use Muted instead for thematically contrasting color. +// Deprecated: Use Muted instead for thematically contrasting color. func (c *ColorScheme) Gray(t string) string { if !c.enabled { return t @@ -153,7 +153,7 @@ func (c *ColorScheme) Gray(t string) string { return gray(t) } -// Use Mutedf instead for thematically contrasting color. +// Deprecated: Use Mutedf instead for thematically contrasting color. func (c *ColorScheme) Grayf(t string, args ...interface{}) string { return c.Gray(fmt.Sprintf(t, args...)) } @@ -255,7 +255,7 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string { case "green": fn = c.Green case "gray": - fn = c.Gray + fn = c.Muted case "magenta": fn = c.Magenta case "cyan": From 2582948d5ffc282b475f88686eed9a77cb8fa7b2 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Sun, 6 Apr 2025 22:36:09 +0100 Subject: [PATCH 064/116] Extract job name sanitization as a separate function Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index c794cff9af2..39c9de3c1f0 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -533,7 +533,7 @@ func promptForJob(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs []sh const JOB_NAME_MAX_LENGTH = 90 -func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp { +func getJobNameForLogFilename(name string) string { // As described in https://github.com/cli/cli/issues/5011#issuecomment-1570713070, there are a number of steps // the server can take when producing the downloaded zip file that can result in a mismatch between the job name // and the filename in the zip including: @@ -545,9 +545,14 @@ func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp { // * Strip `/` which occur when composite action job names are constructed of the form ` / ` // * Truncate long job names // - sanitizedJobName := strings.ReplaceAll(job.Name, "/", "") + sanitizedJobName := strings.ReplaceAll(name, "/", "") sanitizedJobName = strings.ReplaceAll(sanitizedJobName, ":", "") sanitizedJobName = truncateAsUTF16(sanitizedJobName, JOB_NAME_MAX_LENGTH) + return sanitizedJobName +} + +func stepLogFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp { + sanitizedJobName := getJobNameForLogFilename(job.Name) re := fmt.Sprintf(`^%s\/%d_.*\.txt`, regexp.QuoteMeta(sanitizedJobName), step.Number) return regexp.MustCompile(re) } @@ -637,7 +642,7 @@ func truncateAsUTF16(str string, max int) string { func attachRunLog(rlz *zip.Reader, jobs []shared.Job) { for i, job := range jobs { for j, step := range job.Steps { - re := logFilenameRegexp(job, step) + re := stepLogFilenameRegexp(job, step) for _, file := range rlz.File { if re.MatchString(file.Name) { jobs[i].Steps[j].Log = file From 4dee1c3c98342b406b2e7940044921ccf2c80aa4 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 7 Apr 2025 12:59:49 +0100 Subject: [PATCH 065/116] Add `jobLogFilenameRegexp` function Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 39c9de3c1f0..2cfe26db68d 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -551,6 +551,12 @@ func getJobNameForLogFilename(name string) string { return sanitizedJobName } +func jobLogFilenameRegexp(job shared.Job) *regexp.Regexp { + sanitizedJobName := getJobNameForLogFilename(job.Name) + re := fmt.Sprintf(`^-?\d+_%s\.txt`, regexp.QuoteMeta(sanitizedJobName)) + return regexp.MustCompile(re) +} + func stepLogFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp { sanitizedJobName := getJobNameForLogFilename(job.Name) re := fmt.Sprintf(`^%s\/%d_.*\.txt`, regexp.QuoteMeta(sanitizedJobName), step.Number) From f7efdde5ef3606f6b604bbbc333cbbb420066771 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 7 Apr 2025 13:51:56 +0100 Subject: [PATCH 066/116] Add `Log` to `Job` data structure Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/shared/shared.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index ce909fd772e..33882349e8f 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -230,6 +230,8 @@ type Job struct { CompletedAt time.Time `json:"completed_at"` URL string `json:"html_url"` RunID int64 `json:"run_id"` + + Log *zip.File } type Step struct { @@ -239,7 +241,8 @@ type Step struct { Number int StartedAt time.Time `json:"started_at"` CompletedAt time.Time `json:"completed_at"` - Log *zip.File + + Log *zip.File } type Steps []Step From 5e78832a7ea2535ecf844f0db9ca6f9f1393ef7f Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 7 Apr 2025 13:01:48 +0100 Subject: [PATCH 067/116] Fallback to print entire job run log if step logs are missing Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 64 +++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 2cfe26db68d..718d5b902f0 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -638,15 +638,31 @@ func truncateAsUTF16(str string, max int) string { // │ ├── 2_anotherstepname.txt // │ ├── 3_stepstepname.txt // │ └── 4_laststepname.txt -// └── jobname2/ -// ├── 1_stepname.txt -// └── 2_somestepname.txt +// ├── jobname2/ +// | ├── 1_stepname.txt +// | └── 2_somestepname.txt +// ├── 0_jobname1.txt +// ├── 1_jobname2.txt +// └── -9999999999_jobname3.txt // // It iterates through the list of jobs and tries to find the matching // log in the zip file. If the matching log is found it is attached // to the job. +// +// The top-level .txt files include the logs for an entire job run. Note that +// the prefixed number is either: +// - An ordinal and cannot be mapped to the corresponding job's ID. +// - A negative integer which is the ID of the job in the old Actions service. func attachRunLog(rlz *zip.Reader, jobs []shared.Job) { for i, job := range jobs { + re := jobLogFilenameRegexp(job) + for _, file := range rlz.File { + if re.MatchString(file.Name) { + jobs[i].Log = file + break + } + } + for j, step := range job.Steps { re := stepLogFilenameRegexp(job, step) for _, file := range rlz.File { @@ -661,6 +677,8 @@ func attachRunLog(rlz *zip.Reader, jobs []shared.Job) { func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error { for _, job := range jobs { + var hasStepLogs bool + steps := job.Steps sort.Sort(steps) for _, step := range steps { @@ -670,18 +688,44 @@ func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error { if step.Log == nil { continue } + hasStepLogs = true prefix := fmt.Sprintf("%s\t%s\t", job.Name, step.Name) - f, err := step.Log.Open() - if err != nil { + if err := printZIPFile(w, step.Log, prefix); err != nil { return err } - scanner := bufio.NewScanner(f) - for scanner.Scan() { - fmt.Fprintf(w, "%s%s\n", prefix, scanner.Text()) - } - f.Close() + } + + if hasStepLogs { + continue + } + + if failed && !shared.IsFailureState(job.Conclusion) { + continue + } + + if job.Log == nil { + continue + } + + prefix := fmt.Sprintf("%s\tUNKNOWN\t", job.Name) + if err := printZIPFile(w, job.Log, prefix); err != nil { + return err } } return nil } + +func printZIPFile(w io.Writer, file *zip.File, prefix string) error { + f, err := file.Open() + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + fmt.Fprintf(w, "%s%s\n", prefix, scanner.Text()) + } + return nil +} From df8c9a317dfef8ed80bd68b9bd45e63fd19d4f97 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 7 Apr 2025 13:47:01 +0100 Subject: [PATCH 068/116] Update `run_log.zip` fixture Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/fixtures/run_log.zip | Bin 6880 -> 8148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pkg/cmd/run/view/fixtures/run_log.zip b/pkg/cmd/run/view/fixtures/run_log.zip index 757a6398307d835f12293e64b602d58ebc965308..60701d9254cbcacdda1d9aff426de744b6d574d7 100644 GIT binary patch delta 1175 zcmaE0dc}Ujdp;Lt77+#p1`dY98SPP8PIt@2fjlQ5=3|gyFo;ji&(BfF%1_cOsVE5z z;bdT5xBqiG2$xoHGcdBeU}j(d5|b~oig>XIPiJBX@P?Up4$VAXpm~Pz#fd2>#vKM4 z1;QA{#iAK^Yi(PUz@w6EkYg2rSOI995!`tS<(VZJ3VHbo#U-f)3OV`d#c&7Py!e#f zJ{{8r1z(nRs7A^IjWmY25RZxNxJ-=qi*848q#@8mT}wEK&q+;BOs-Ub1u7mpf>(S_ zH{OHA)p1fOsCF1&w*wl`c&vE5{c}19qX)gj=J$Mex$7C32 zr4{u^$s5%g%w&UXjf4OL;Y0+q1(b;JgpXf6Mp8nyr4?uk83_w$FDPN*u@|1Y%7Kv! q%4Y@)$o8@bVzrk*Vgp)>k Date: Mon, 7 Apr 2025 13:50:27 +0100 Subject: [PATCH 069/116] Verify job run logs attached in `attachRunLog` test Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view_test.go | 141 ++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 32 deletions(-) diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index 3a04fb1867a..ea0b92f4047 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -1419,14 +1419,23 @@ func TestViewRun(t *testing.T) { // ├── sad job/ // │ ├── 1_barf the quux.txt // │ └── 2_quux the barf.txt -// └── ad job/ -// └── 1_barf the quux.txt +// ├── ad job/ +// | └── 1_barf the quux.txt +// ├── 0_cool job.txt +// ├── 1_sad job.txt +// ├── 2_cool job with no step logs.txt +// ├── 3_sad job with no step logs.txt +// ├── -9999999999_legacy cool job with no step logs.txt +// └── -9999999999_legacy sad job with no step logs.txt + func Test_attachRunLog(t *testing.T) { tests := []struct { - name string - job shared.Job - wantMatch bool - wantFilename string + name string + job shared.Job + wantJobMatch bool + wantJobFilename string + wantStepMatch bool + wantStepFilename string }{ { name: "matching job name and step number 1", @@ -1437,8 +1446,10 @@ func Test_attachRunLog(t *testing.T) { Number: 1, }}, }, - wantMatch: true, - wantFilename: "cool job/1_fob the barz.txt", + wantJobMatch: true, + wantJobFilename: "0_cool job.txt", + wantStepMatch: true, + wantStepFilename: "cool job/1_fob the barz.txt", }, { name: "matching job name and step number 2", @@ -1449,8 +1460,10 @@ func Test_attachRunLog(t *testing.T) { Number: 2, }}, }, - wantMatch: true, - wantFilename: "cool job/2_barz the fob.txt", + wantJobMatch: true, + wantJobFilename: "0_cool job.txt", + wantStepMatch: true, + wantStepFilename: "cool job/2_barz the fob.txt", }, { name: "matching job name and step number and mismatch step name", @@ -1461,8 +1474,10 @@ func Test_attachRunLog(t *testing.T) { Number: 1, }}, }, - wantMatch: true, - wantFilename: "cool job/1_fob the barz.txt", + wantJobMatch: true, + wantJobFilename: "0_cool job.txt", + wantStepMatch: true, + wantStepFilename: "cool job/1_fob the barz.txt", }, { name: "matching job name and mismatch step number", @@ -1473,7 +1488,53 @@ func Test_attachRunLog(t *testing.T) { Number: 3, }}, }, - wantMatch: false, + wantJobMatch: true, + wantJobFilename: "0_cool job.txt", + wantStepMatch: false, + }, + { + name: "matching job name with no step logs", + job: shared.Job{ + Name: "cool job with no step logs", + Steps: []shared.Step{{ + Name: "fob the barz", + Number: 1, + }}, + }, + wantJobMatch: true, + wantJobFilename: "2_cool job with no step logs.txt", + wantStepMatch: false, + }, + { + name: "matching job name with no step data", + job: shared.Job{ + Name: "cool job with no step logs", + }, + wantJobMatch: true, + wantJobFilename: "2_cool job with no step logs.txt", + wantStepMatch: false, + }, + { + name: "matching job name with legacy filename and no step logs", + job: shared.Job{ + Name: "legacy cool job with no step logs", + Steps: []shared.Step{{ + Name: "fob the barz", + Number: 1, + }}, + }, + wantJobMatch: true, + wantJobFilename: "-9999999999_legacy cool job with no step logs.txt", + wantStepMatch: false, + }, + { + name: "matching job name with legacy filename and no step data", + job: shared.Job{ + Name: "legacy cool job with no step logs", + }, + wantJobMatch: true, + wantJobFilename: "-9999999999_legacy cool job with no step logs.txt", + wantStepMatch: false, }, { name: "one job name is a suffix of another", @@ -1484,8 +1545,8 @@ func Test_attachRunLog(t *testing.T) { Number: 1, }}, }, - wantMatch: true, - wantFilename: "ad job/1_barf the quux.txt", + wantStepMatch: true, + wantStepFilename: "ad job/1_barf the quux.txt", }, { name: "escape metacharacters in job name", @@ -1496,7 +1557,8 @@ func Test_attachRunLog(t *testing.T) { Number: 0, }}, }, - wantMatch: false, + wantJobMatch: false, + wantStepMatch: false, }, { name: "mismatching job name", @@ -1507,7 +1569,8 @@ func Test_attachRunLog(t *testing.T) { Number: 1, }}, }, - wantMatch: false, + wantJobMatch: false, + wantStepMatch: false, }, { name: "job name with forward slash matches dir with slash removed", @@ -1518,9 +1581,10 @@ func Test_attachRunLog(t *testing.T) { Number: 1, }}, }, - wantMatch: true, + wantJobMatch: false, + wantStepMatch: true, // not the double space in the dir name, as the slash has been removed - wantFilename: "cool job with slash/1_fob the barz.txt", + wantStepFilename: "cool job with slash/1_fob the barz.txt", }, { name: "job name with colon matches dir with colon removed", @@ -1531,8 +1595,9 @@ func Test_attachRunLog(t *testing.T) { Number: 1, }}, }, - wantMatch: true, - wantFilename: "cool job with colon/1_fob the barz.txt", + wantJobMatch: false, + wantStepMatch: true, + wantStepFilename: "cool job with colon/1_fob the barz.txt", }, { name: "Job name with really long name (over the ZIP limit)", @@ -1543,8 +1608,9 @@ func Test_attachRunLog(t *testing.T) { Number: 1, }}, }, - wantMatch: true, - wantFilename: "thisisnineteenchars_thisisnineteenchars_thisisnineteenchars_thisisnineteenchars_thisisnine/1_Long Name Job.txt", + wantJobMatch: false, + wantStepMatch: true, + wantStepFilename: "thisisnineteenchars_thisisnineteenchars_thisisnineteenchars_thisisnineteenchars_thisisnine/1_Long Name Job.txt", }, { name: "Job name that would be truncated by the C# server to split a grapheme", @@ -1555,8 +1621,9 @@ func Test_attachRunLog(t *testing.T) { Number: 1, }}, }, - wantMatch: true, - wantFilename: "Emoji Test 😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅�/1_Emoji Job.txt", + wantJobMatch: false, + wantStepMatch: true, + wantStepFilename: "Emoji Test 😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅😅�/1_Emoji Job.txt", }, } @@ -1566,17 +1633,27 @@ func Test_attachRunLog(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + jobs := []shared.Job{tt.job} - attachRunLog(&run_log_zip_reader.Reader, []shared.Job{tt.job}) + attachRunLog(&run_log_zip_reader.Reader, jobs) t.Logf("Job details: ") - for _, step := range tt.job.Steps { - log := step.Log - logPresent := log != nil - require.Equal(t, tt.wantMatch, logPresent, "log not present") - if logPresent { - require.Equal(t, tt.wantFilename, log.Name, "Filename mismatch") + job := jobs[0] + + jobLog := job.Log + jobLogPresent := jobLog != nil + require.Equal(t, tt.wantJobMatch, jobLogPresent, "job log not present") + if jobLogPresent { + require.Equal(t, tt.wantJobFilename, jobLog.Name, "job log filename mismatch") + } + + for _, step := range job.Steps { + stepLog := step.Log + stepLogPresent := stepLog != nil + require.Equal(t, tt.wantStepMatch, stepLogPresent, "step log not present") + if stepLogPresent { + require.Equal(t, tt.wantStepFilename, stepLog.Name, "step log filename mismatch") } } }) From 021537418e577720241cc0ff61473a045125edee Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 7 Apr 2025 13:53:35 +0100 Subject: [PATCH 070/116] Verify fallback to job run logs when step logs are missing Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/shared/test.go | 108 ++++++ pkg/cmd/run/view/view_test.go | 639 ++++++++++++++++++++++++++++++++++ 2 files changed, 747 insertions(+) diff --git a/pkg/cmd/run/shared/test.go b/pkg/cmd/run/shared/test.go index 0619541a452..5a8a4584e3d 100644 --- a/pkg/cmd/run/shared/test.go +++ b/pkg/cmd/run/shared/test.go @@ -104,6 +104,60 @@ var SuccessfulJob Job = Job{ }, } +// Note that this run *has* steps, but in the ZIP archive the step logs are not +// included. +var SuccessfulJobWithoutStepLogs Job = Job{ + ID: 11, + Status: Completed, + Conclusion: Success, + Name: "cool job with no step logs", + StartedAt: TestRunStartTime, + CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34), + URL: "https://github.com/jobs/11", + RunID: 3, + Steps: []Step{ + { + Name: "fob the barz", + Status: Completed, + Conclusion: Success, + Number: 1, + }, + { + Name: "barz the fob", + Status: Completed, + Conclusion: Success, + Number: 2, + }, + }, +} + +// Note that this run *has* steps, but in the ZIP archive the step logs are not +// included. +var LegacySuccessfulJobWithoutStepLogs Job = Job{ + ID: 12, + Status: Completed, + Conclusion: Success, + Name: "legacy cool job with no step logs", + StartedAt: TestRunStartTime, + CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34), + URL: "https://github.com/jobs/12", + RunID: 3, + Steps: []Step{ + { + Name: "fob the barz", + Status: Completed, + Conclusion: Success, + Number: 1, + }, + { + Name: "barz the fob", + Status: Completed, + Conclusion: Success, + Number: 2, + }, + }, +} + var FailedJob Job = Job{ ID: 20, Status: Completed, @@ -129,6 +183,60 @@ var FailedJob Job = Job{ }, } +// Note that this run *has* steps, but in the ZIP archive the step logs are not +// included. +var FailedJobWithoutStepLogs Job = Job{ + ID: 21, + Status: Completed, + Conclusion: Failure, + Name: "sad job with no step logs", + StartedAt: TestRunStartTime, + CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34), + URL: "https://github.com/jobs/21", + RunID: 1234, + Steps: []Step{ + { + Name: "barf the quux", + Status: Completed, + Conclusion: Success, + Number: 1, + }, + { + Name: "quux the barf", + Status: Completed, + Conclusion: Failure, + Number: 2, + }, + }, +} + +// Note that this run *has* steps, but in the ZIP archive the step logs are not +// included. +var LegacyFailedJobWithoutStepLogs Job = Job{ + ID: 22, + Status: Completed, + Conclusion: Failure, + Name: "legacy sad job with no step logs", + StartedAt: TestRunStartTime, + CompletedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34), + URL: "https://github.com/jobs/22", + RunID: 1234, + Steps: []Step{ + { + Name: "barf the quux", + Status: Completed, + Conclusion: Success, + Number: 1, + }, + { + Name: "quux the barf", + Status: Completed, + Conclusion: Failure, + Number: 2, + }, + }, +} + var SuccessfulJobAnnotations []Annotation = []Annotation{ { JobName: "cool job", diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index ea0b92f4047..b5d619cd6c7 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -990,6 +990,619 @@ func TestViewRun(t *testing.T) { }, wantOut: quuxTheBarfLogOutput, }, + { + name: "interactive with log, with no step logs available (#10551)", + tty: true, + opts: &ViewOptions{ + Prompt: true, + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJobWithoutStepLogs, + shared.FailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow run", + []string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "✓ cool commit, CI [trunk] Feb 23, 2021") + }) + pm.RegisterSelect("View a specific job in this run?", + []string{"View all jobs in this run", "✓ cool job with no step logs", "X sad job with no step logs"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "✓ cool job with no step logs") + }) + }, + wantOut: coolJobRunWithNoStepLogsLogOutput, + }, + { + name: "noninteractive with log, with no step logs available (#10551)", + opts: &ViewOptions{ + JobID: "11", + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/11"), + httpmock.JSONResponse(shared.SuccessfulJobWithoutStepLogs)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: coolJobRunWithNoStepLogsLogOutput, + }, + { + name: "interactive with log-failed, with no step logs available (#10551)", + tty: true, + opts: &ViewOptions{ + Prompt: true, + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJobWithoutStepLogs, + shared.FailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow run", + []string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, + func(_, _ string, opts []string) (int, error) { + return 4, nil + }) + pm.RegisterSelect("View a specific job in this run?", + []string{"View all jobs in this run", "✓ cool job with no step logs", "X sad job with no step logs"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "X sad job with no step logs") + }) + }, + wantOut: sadJobRunWithNoStepLogsLogOutput, + }, + { + name: "noninteractive with log-failed, with no step logs available (#10551)", + opts: &ViewOptions{ + JobID: "21", + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/21"), + httpmock.JSONResponse(shared.FailedJobWithoutStepLogs)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: sadJobRunWithNoStepLogsLogOutput, + }, + { + name: "interactive with run log, with no step logs available (#10551)", + tty: true, + opts: &ViewOptions{ + Prompt: true, + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJobWithoutStepLogs, + shared.FailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow run", + []string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "✓ cool commit, CI [trunk] Feb 23, 2021") + }) + pm.RegisterSelect("View a specific job in this run?", + []string{"View all jobs in this run", "✓ cool job with no step logs", "X sad job with no step logs"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "View all jobs in this run") + }) + }, + wantOut: expectedRunLogOutputWithNoSteps, + }, + { + name: "noninteractive with run log, with no step logs available (#10551)", + opts: &ViewOptions{ + RunID: "3", + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJobWithoutStepLogs, + shared.FailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: expectedRunLogOutputWithNoSteps, + }, + { + name: "interactive with run log-failed, with no step logs available (#10551)", + tty: true, + opts: &ViewOptions{ + Prompt: true, + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJobWithoutStepLogs, + shared.FailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow run", + []string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, + func(_, _ string, opts []string) (int, error) { + return 4, nil + }) + pm.RegisterSelect("View a specific job in this run?", + []string{"View all jobs in this run", "✓ cool job with no step logs", "X sad job with no step logs"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "View all jobs in this run") + }) + }, + wantOut: sadJobRunWithNoStepLogsLogOutput, + }, + { + name: "noninteractive with run log-failed, with no step logs available (#10551)", + opts: &ViewOptions{ + RunID: "1234", + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJobWithoutStepLogs, + shared.FailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: sadJobRunWithNoStepLogsLogOutput, + }, + { + name: "interactive with log, legacy service data, with no step logs available", + tty: true, + opts: &ViewOptions{ + Prompt: true, + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.LegacySuccessfulJobWithoutStepLogs, + shared.LegacyFailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow run", + []string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "✓ cool commit, CI [trunk] Feb 23, 2021") + }) + pm.RegisterSelect("View a specific job in this run?", + []string{"View all jobs in this run", "✓ legacy cool job with no step logs", "X legacy sad job with no step logs"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "✓ legacy cool job with no step logs") + }) + }, + wantOut: legacyCoolJobRunWithNoStepLogsLogOutput, + }, + { + name: "noninteractive with log, legacy service data, with no step logs available", + opts: &ViewOptions{ + JobID: "12", + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/12"), + httpmock.JSONResponse(shared.LegacySuccessfulJobWithoutStepLogs)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: legacyCoolJobRunWithNoStepLogsLogOutput, + }, + + { + name: "interactive with log-failed, legacy service data, with no step logs available (#10551)", + tty: true, + opts: &ViewOptions{ + Prompt: true, + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.LegacySuccessfulJobWithoutStepLogs, + shared.LegacyFailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow run", + []string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, + func(_, _ string, opts []string) (int, error) { + return 4, nil + }) + pm.RegisterSelect("View a specific job in this run?", + []string{"View all jobs in this run", "✓ legacy cool job with no step logs", "X legacy sad job with no step logs"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "X legacy sad job with no step logs") + }) + }, + wantOut: legacySadJobRunWithNoStepLogsLogOutput, + }, + { + name: "noninteractive with log-failed, legacy service data, with no step logs available (#10551)", + opts: &ViewOptions{ + JobID: "22", + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/22"), + httpmock.JSONResponse(shared.LegacyFailedJobWithoutStepLogs)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: legacySadJobRunWithNoStepLogsLogOutput, + }, + { + name: "interactive with run log, legacy service data, with no step logs available (#10551)", + tty: true, + opts: &ViewOptions{ + Prompt: true, + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.LegacySuccessfulJobWithoutStepLogs, + shared.LegacyFailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow run", + []string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "✓ cool commit, CI [trunk] Feb 23, 2021") + }) + pm.RegisterSelect("View a specific job in this run?", + []string{"View all jobs in this run", "✓ legacy cool job with no step logs", "X legacy sad job with no step logs"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "View all jobs in this run") + }) + }, + wantOut: expectedLegacyRunLogOutputWithNoSteps, + }, + { + name: "noninteractive with run log, legacy service data, with no step logs available (#10551)", + opts: &ViewOptions{ + RunID: "3", + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.LegacySuccessfulJobWithoutStepLogs, + shared.LegacyFailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: expectedLegacyRunLogOutputWithNoSteps, + }, + { + name: "interactive with run log-failed, legacy service data, with no step logs available (#10551)", + tty: true, + opts: &ViewOptions{ + Prompt: true, + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.LegacySuccessfulJobWithoutStepLogs, + shared.LegacyFailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow run", + []string{"X cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "✓ cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "* cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, + func(_, _ string, opts []string) (int, error) { + return 4, nil + }) + pm.RegisterSelect("View a specific job in this run?", + []string{"View all jobs in this run", "✓ legacy cool job with no step logs", "X legacy sad job with no step logs"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "View all jobs in this run") + }) + }, + wantOut: legacySadJobRunWithNoStepLogsLogOutput, + }, + { + name: "noninteractive with run log-failed, legacy service data, with no step logs available (#10551)", + opts: &ViewOptions{ + RunID: "1234", + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.LegacySuccessfulJobWithoutStepLogs, + shared.LegacyFailedJobWithoutStepLogs, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: legacySadJobRunWithNoStepLogsLogOutput, + }, { name: "run log but run is not done", tty: true, @@ -1684,9 +2297,35 @@ sad job quux the barf log line 2 sad job quux the barf log line 3 `) +var coolJobRunWithNoStepLogsLogOutput = heredoc.Doc(` +cool job with no step logs UNKNOWN log line 1 +cool job with no step logs UNKNOWN log line 2 +cool job with no step logs UNKNOWN log line 3 +`) + +var legacyCoolJobRunWithNoStepLogsLogOutput = heredoc.Doc(` +legacy cool job with no step logs UNKNOWN log line 1 +legacy cool job with no step logs UNKNOWN log line 2 +legacy cool job with no step logs UNKNOWN log line 3 +`) + +var sadJobRunWithNoStepLogsLogOutput = heredoc.Doc(` +sad job with no step logs UNKNOWN log line 1 +sad job with no step logs UNKNOWN log line 2 +sad job with no step logs UNKNOWN log line 3 +`) + +var legacySadJobRunWithNoStepLogsLogOutput = heredoc.Doc(` +legacy sad job with no step logs UNKNOWN log line 1 +legacy sad job with no step logs UNKNOWN log line 2 +legacy sad job with no step logs UNKNOWN log line 3 +`) + var coolJobRunLogOutput = fmt.Sprintf("%s%s", fobTheBarzLogOutput, barfTheFobLogOutput) var sadJobRunLogOutput = fmt.Sprintf("%s%s", barfTheQuuxLogOutput, quuxTheBarfLogOutput) var expectedRunLogOutput = fmt.Sprintf("%s%s", coolJobRunLogOutput, sadJobRunLogOutput) +var expectedRunLogOutputWithNoSteps = fmt.Sprintf("%s%s", coolJobRunWithNoStepLogsLogOutput, sadJobRunWithNoStepLogsLogOutput) +var expectedLegacyRunLogOutputWithNoSteps = fmt.Sprintf("%s%s", legacyCoolJobRunWithNoStepLogsLogOutput, legacySadJobRunWithNoStepLogsLogOutput) func TestRunLog(t *testing.T) { t.Run("when the cache dir doesn't exist, exists return false", func(t *testing.T) { From fc84b7a3cbf0d17d2d16e666aba5899d88340156 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 7 Apr 2025 15:21:48 +0100 Subject: [PATCH 071/116] Escape dots in regexp pattern in example command Signed-off-by: Babak K. Shandiz --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fbd99a5a807..ad012588c36 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ There are two common ways to verify a downloaded release, depending if `gh` is a $ cosign verify-blob-attestation --bundle cli-cli-attestation-3120304.sigstore.json \ --new-bundle-format \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ - --certificate-identity-regexp="^https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk$" \ + --certificate-identity-regexp='^https://github\.com/cli/cli/\.github/workflows/deployment\.yml@refs/heads/trunk$' \ gh_2.62.0_macOS_arm64.zip Verified OK ``` From 5996f882fc9561b40d99d27cb5184f05ae72ced0 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 7 Apr 2025 10:40:29 -0600 Subject: [PATCH 072/116] doc(envs): speech synthesis prompter --- pkg/cmd/root/help_topic.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index db0ef098d18..e0a1a853511 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -108,6 +108,9 @@ var HelpTopics = []helpTopic{ %[1]sGH_MDWIDTH%[1]s: default maximum width for markdown render wrapping. The max width of lines wrapped on the terminal will be taken as the lesser of the terminal width, this value, or 120 if not specified. This value is used, for example, with %[1]spr view%[1]s subcommand. + + %[1]sGH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER%[1]s: set to a truthy value to enable prompts that are + more compatible with speech synthesis based screen readers. `, "`"), }, { From 2a851e33e89509e37c6fba810944b11dd30113ff Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:59:05 -0600 Subject: [PATCH 073/116] test(prompter): fix race conditions --- ...eech_synthesizer_friendly_prompter_test.go | 140 +++++++++++++----- 1 file changed, 107 insertions(+), 33 deletions(-) diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 4091db33aaa..e845febad7a 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -7,6 +7,7 @@ import ( "io" "os" "strings" + "sync" "testing" "time" @@ -56,10 +57,17 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { os.Stderr = console.Tty() t.Setenv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER", "true") - p := prompter.New("", nil, nil, nil) + // Using echo as the editor command here because it will immediately exit + // and return no input. + p := prompter.New("echo", nil, nil, nil) + + var wg sync.WaitGroup t.Run("Select", func(t *testing.T) { + wg.Add(1) + go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Choose:") require.NoError(t, err) @@ -71,12 +79,16 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"}) require.NoError(t, err) - assert.Equal(t, 0, selectValue) + + wg.Wait() }) t.Run("MultiSelect", func(t *testing.T) { + wg.Add(1) + go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Select a number") require.NoError(t, err) @@ -94,13 +106,17 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"}) require.NoError(t, err) - assert.Equal(t, []int{0, 1}, multiSelectValue) + + wg.Wait() }) t.Run("Input", func(t *testing.T) { + wg.Add(1) + dummyText := "12345abcdefg" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Enter some characters") require.NoError(t, err) @@ -112,13 +128,17 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { inputValue, err := p.Input("Enter some characters", "") require.NoError(t, err) - assert.Equal(t, dummyText, inputValue) + + wg.Wait() }) t.Run("Input - blank input returns default value", func(t *testing.T) { + wg.Add(1) + dummyDefaultValue := "12345abcdefg" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Enter some characters") require.NoError(t, err) @@ -134,13 +154,17 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { inputValue, err := p.Input("Enter some characters", dummyDefaultValue) require.NoError(t, err) - assert.Equal(t, dummyDefaultValue, inputValue) + + wg.Wait() }) t.Run("Password", func(t *testing.T) { + wg.Add(1) + dummyPassword := "12345abcdefg" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Enter password") require.NoError(t, err) @@ -153,10 +177,15 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { passwordValue, err := p.Password("Enter password") require.NoError(t, err) require.Equal(t, dummyPassword, passwordValue) + + wg.Wait() }) t.Run("Confirm", func(t *testing.T) { + wg.Add(1) + go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Are you sure") require.NoError(t, err) @@ -169,30 +198,16 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { confirmValue, err := p.Confirm("Are you sure", false) require.NoError(t, err) require.Equal(t, true, confirmValue) - }) - // This test currently fails because the value is - // not respected as the default in accessible mode. - // See https://github.com/charmbracelet/huh/issues/615 - // t.Run("Confirm - blank input returns default", func(t *testing.T) { - // go func() { - // // Wait for prompt to appear - // _, err := console.ExpectString("Are you sure") - // require.NoError(t, err) - - // // Enter nothing - // _, err = console.SendLine("") - // require.NoError(t, err) - // }() - - // confirmValue, err := p.Confirm("Are you sure", false) - // require.NoError(t, err) - // require.Equal(t, false, confirmValue) - // }) + wg.Wait() + }) t.Run("AuthToken", func(t *testing.T) { + wg.Add(1) + dummyAuthToken := "12345abcdefg" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Paste your authentication token:") require.NoError(t, err) @@ -205,11 +220,16 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { authValue, err := p.AuthToken() require.NoError(t, err) require.Equal(t, dummyAuthToken, authValue) + + wg.Wait() }) t.Run("AuthToken - blank input returns error", func(t *testing.T) { + wg.Add(1) + dummyAuthTokenForAfterFailure := "12345abcdefg" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Paste your authentication token:") require.NoError(t, err) @@ -230,11 +250,16 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { authValue, err := p.AuthToken() require.NoError(t, err) require.Equal(t, dummyAuthTokenForAfterFailure, authValue) + + wg.Wait() }) t.Run("ConfirmDeletion", func(t *testing.T) { + wg.Add(1) + requiredValue := "test" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue)) require.NoError(t, err) @@ -247,12 +272,17 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { // An err indicates that the confirmation text sent did not match err := p.ConfirmDeletion(requiredValue) require.NoError(t, err) + + wg.Wait() }) t.Run("ConfirmDeletion - bad input", func(t *testing.T) { + wg.Add(1) + requiredValue := "test" badInputValue := "garbage" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue)) require.NoError(t, err) @@ -273,11 +303,16 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { // An err indicates that the confirmation text sent did not match err := p.ConfirmDeletion(requiredValue) require.NoError(t, err) + + wg.Wait() }) t.Run("InputHostname", func(t *testing.T) { + wg.Add(1) + hostname := "somethingdoesnotmatter.com" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Hostname:") require.NoError(t, err) @@ -290,10 +325,15 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { inputValue, err := p.InputHostname() require.NoError(t, err) require.Equal(t, hostname, inputValue) + + wg.Wait() }) - t.Run("MarkdownEditor - blank allowed", func(t *testing.T) { + t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) { + wg.Add(1) + go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("How to edit?") require.NoError(t, err) @@ -306,15 +346,21 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { inputValue, err := p.MarkdownEditor("How to edit?", "", true) require.NoError(t, err) require.Equal(t, "", inputValue) + + wg.Wait() }) - t.Run("MarkdownEditor - blank disallowed", func(t *testing.T) { + t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) { + wg.Add(1) + + defaultValue := "12345abcdefg" go func() { + defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("How to edit?") require.NoError(t, err) - // Enter number 2 to select "skip". This shoudln't be allowed. + // Enter number 2 to select "skip". This shouldn't be allowed. _, err = console.SendLine("2") require.NoError(t, err) @@ -322,18 +368,46 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { _, err = console.ExpectString("invalid input. please try again") require.NoError(t, err) - // Send a 1 to select to open the editor. - // Sending the input won't fail, so we expect no error here. - // See below though, since we expect the editor to fail to open. + // Send a 1 to select to open the editor. This will immediately exit + _, err = console.SendLine("1") + require.NoError(t, err) + }() + + inputValue, err := p.MarkdownEditor("How to edit?", defaultValue, false) + require.NoError(t, err) + require.Equal(t, defaultValue, inputValue) + + wg.Wait() + }) + + t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) { + wg.Add(1) + + go func() { + defer wg.Done() + // Wait for prompt to appear + _, err := console.ExpectString("How to edit?") + require.NoError(t, err) + + // Enter number 2 to select "skip". This shouldn't be allowed. + _, err = console.SendLine("2") + require.NoError(t, err) + + // Expect a notice to enter something valid since blank is disallowed. + _, err = console.ExpectString("invalid input. please try again") + require.NoError(t, err) + + // Send a 1 to select to open the editor since skip is invalid and + // we need to return control back to the test. _, err = console.SendLine("1") require.NoError(t, err) }() - // However, here we do expect an error because the editor program - // is intentionally empty and will fail. inputValue, err := p.MarkdownEditor("How to edit?", "", false) - require.Error(t, err) + require.NoError(t, err) require.Equal(t, "", inputValue) + + wg.Wait() }) } From 0543aac53c564fb7ead6c44f741954594dd85494 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:54:03 -0600 Subject: [PATCH 074/116] test(prompter): add basic survey prompter test --- ...eech_synthesizer_friendly_prompter_test.go | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index e845febad7a..6970e3d337c 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -411,6 +411,63 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { }) } +func TestSurveyPrompter(t *testing.T) { + // Create a PTY and hook up a virtual terminal emulator + ptm, pts, err := pty.Open() + require.NoError(t, err) + + term := vt10x.New(vt10x.WithWriter(pts)) + + // Create a console via Expect that allows scripting against the terminal + consoleOpts := []expect.ConsoleOpt{ + expect.WithStdin(ptm), + expect.WithStdout(term), + expect.WithCloser(ptm, pts), + failOnExpectError(t), + failOnSendError(t), + expect.WithDefaultTimeout(time.Second * 600), + } + + console, err := expect.NewConsole(consoleOpts...) + require.NoError(t, err) + t.Cleanup(func() { testCloser(t, console) }) + + t.Setenv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER", "") + t.Setenv("NO_COLOR", "1") + // Using echo as the editor command here because it will immediately exit + // and return no input. + p := prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) + + var wg sync.WaitGroup + + // This not a comprehensive test of the survey prompter, but it does + // demonstrate that the survey prompter is used when the speech + // synthesizer friendly prompter is disabled. + t.Run("Select uses survey prompter when speech synthesizer friendly prompter is disabled", func(t *testing.T) { + wg.Add(1) + + go func() { + defer wg.Done() + // Wait for prompt to appear + _, err := console.ExpectString("Select a number") + require.NoError(t, err) + + // Send a newline to select the first option + // Note: This would not work with the speech synthesizer friendly prompter + // because it would requires sending a 1 to select the first option. + // So it proves we are seeing a survey prompter. + _, err = console.SendLine("") + require.NoError(t, err) + }() + + selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"}) + require.NoError(t, err) + assert.Equal(t, 0, selectValue) + + wg.Wait() + }) +} + // failOnExpectError adds an observer that will fail the test in a standardised way // if any expectation on the command output fails, without requiring an explicit // assertion. From 66407402c0fa7df7f8789642e0b76d1772d96c89 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:53:31 -0600 Subject: [PATCH 075/116] doc: comment typos and formatting Co-authored-by: Andy Feller --- internal/prompter/prompter.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 0a42a8df98a..913b1a5e7ea 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -144,13 +144,12 @@ func (p *speechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) ( func (p *speechSynthesizerFriendlyPrompter) Password(prompt string) (string, error) { var result string + // EchoMode(huh.EchoModePassword) doesn't have any effect in accessible mode. form := p.newForm( huh.NewGroup( huh.NewInput(). Title(prompt). Value(&result), - // This doesn't have any effect in accessible mode. - // EchoMode(huh.EchoModePassword), ), ) @@ -159,7 +158,7 @@ func (p *speechSynthesizerFriendlyPrompter) Password(prompt string) (string, err } func (p *speechSynthesizerFriendlyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) { - // This is currently an inneffectual assignment because the value is + // This is currently an ineffectual assignment because the value is // not respected as the default in accessible mode. Leaving this in here // because it may change in the future. // See https://github.com/charmbracelet/huh/issues/615 @@ -202,6 +201,7 @@ func (p *speechSynthesizerFriendlyPrompter) AuthToken() (string, error) { func (p *speechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string) error { var result string + // EchoMode(huh.EchoModePassword) doesn't have any effect in accessible mode. form := p.newForm( huh.NewGroup( huh.NewInput(). @@ -213,8 +213,6 @@ func (p *speechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string return nil }). Value(&result), - // This doesn't have any effect in accessible mode. - // EchoMode(huh.EchoModePassword), ), ) From c5ffb3cbfeec54c3882f589457ac2af561c551af Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:54:41 -0600 Subject: [PATCH 076/116] test: use example.com in tests --- internal/prompter/speech_synthesizer_friendly_prompter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 6970e3d337c..df8d2f44578 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -310,7 +310,7 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { t.Run("InputHostname", func(t *testing.T) { wg.Add(1) - hostname := "somethingdoesnotmatter.com" + hostname := "example.com" go func() { defer wg.Done() // Wait for prompt to appear From fb80b5bd86cde6a009071b0b2e6d173e89abe3a9 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:56:33 -0600 Subject: [PATCH 077/116] test(prompter): remove needless NO_COLOR set --- internal/prompter/speech_synthesizer_friendly_prompter_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index df8d2f44578..74b053072cb 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -433,7 +433,6 @@ func TestSurveyPrompter(t *testing.T) { t.Cleanup(func() { testCloser(t, console) }) t.Setenv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER", "") - t.Setenv("NO_COLOR", "1") // Using echo as the editor command here because it will immediately exit // and return no input. p := prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) From d8d3874778038fb5de581b7ed93b4e9bbaa4f185 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:38:54 -0600 Subject: [PATCH 078/116] fix(prompter): use os.lookupenv for accessible prompter --- internal/prompter/prompter.go | 24 ++++++++++--------- ...eech_synthesizer_friendly_prompter_test.go | 1 - 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 913b1a5e7ea..2c027c184d9 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -3,6 +3,7 @@ package prompter import ( "fmt" "os" + "slices" "strings" "github.com/AlecAivazis/survey/v2" @@ -43,17 +44,10 @@ type Prompter interface { } func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { - accessiblePrompterValue := os.Getenv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER") - switch accessiblePrompterValue { - case "", "false", "0", "no": - return &surveyPrompter{ - prompter: ghPrompter.New(stdin, stdout, stderr), - stdin: stdin, - stdout: stdout, - stderr: stderr, - editorCmd: editorCmd, - } - default: + accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER") + falseyValues := []string{"false", "0", "no", ""} + + if accessiblePrompterIsSet && !slices.Contains(falseyValues, accessiblePrompterValue) { return &speechSynthesizerFriendlyPrompter{ stdin: stdin, stdout: stdout, @@ -61,6 +55,14 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr editorCmd: editorCmd, } } + + return &surveyPrompter{ + prompter: ghPrompter.New(stdin, stdout, stderr), + stdin: stdin, + stdout: stdout, + stderr: stderr, + editorCmd: editorCmd, + } } type speechSynthesizerFriendlyPrompter struct { diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 74b053072cb..64e9ac2fe99 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -432,7 +432,6 @@ func TestSurveyPrompter(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { testCloser(t, console) }) - t.Setenv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER", "") // Using echo as the editor command here because it will immediately exit // and return no input. p := prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) From ef58e627f9d2cd3698bba857d6eca1450278dbb5 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:44:14 -0600 Subject: [PATCH 079/116] test(prompter): timeout for tests is 1s --- internal/prompter/speech_synthesizer_friendly_prompter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 64e9ac2fe99..d461bf1e49b 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -33,7 +33,7 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { expect.WithCloser(ptm, pts), failOnExpectError(t), failOnSendError(t), - expect.WithDefaultTimeout(time.Second * 600), + expect.WithDefaultTimeout(time.Second), } console, err := expect.NewConsole(consoleOpts...) From c4be95afd962945d123a760101e6d8b26aecd7c3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:58:50 -0600 Subject: [PATCH 080/116] refactor(prompter): remove unused variable --- internal/prompter/prompter.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 2c027c184d9..2595463e092 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -202,7 +202,6 @@ func (p *speechSynthesizerFriendlyPrompter) AuthToken() (string, error) { } func (p *speechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string) error { - var result string // EchoMode(huh.EchoModePassword) doesn't have any effect in accessible mode. form := p.newForm( huh.NewGroup( @@ -213,8 +212,7 @@ func (p *speechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string return fmt.Errorf("You entered: %q", input) } return nil - }). - Value(&result), + }), ), ) From 8821f77fbbe97bf3f16ad70a7ef6e1471a948da0 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:00:02 -0600 Subject: [PATCH 081/116] doc(prompter): remove senseless comment --- internal/prompter/prompter.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 2595463e092..f1900c751aa 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -202,7 +202,6 @@ func (p *speechSynthesizerFriendlyPrompter) AuthToken() (string, error) { } func (p *speechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string) error { - // EchoMode(huh.EchoModePassword) doesn't have any effect in accessible mode. form := p.newForm( huh.NewGroup( huh.NewInput(). From 9cf341302eac404813eef1de4e45d8605a8985e0 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:03:55 -0600 Subject: [PATCH 082/116] refactor(prompter): explicit return values --- internal/prompter/prompter.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index f1900c751aa..556fa6f4c82 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -156,7 +156,11 @@ func (p *speechSynthesizerFriendlyPrompter) Password(prompt string) (string, err ) err := form.Run() - return result, err + if err != nil { + return "", err + } + + return result, nil } func (p *speechSynthesizerFriendlyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) { @@ -230,7 +234,10 @@ func (p *speechSynthesizerFriendlyPrompter) InputHostname() (string, error) { ) err := form.Run() - return result, err + if err != nil { + return "", err + } + return result, nil } func (p *speechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { From 19387b84187bef5edf437f4a46399843979e955e Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:52:39 -0600 Subject: [PATCH 083/116] fix(prompter): rename GH_ACCESSIBLE_PROMPTER --- internal/prompter/prompter.go | 2 +- internal/prompter/speech_synthesizer_friendly_prompter_test.go | 2 +- pkg/cmd/root/help_topic.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 556fa6f4c82..bf5655846b1 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -44,7 +44,7 @@ type Prompter interface { } func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { - accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER") + accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER") falseyValues := []string{"false", "0", "no", ""} if accessiblePrompterIsSet && !slices.Contains(falseyValues, accessiblePrompterValue) { diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index d461bf1e49b..20935a5669c 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -56,7 +56,7 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { os.Stdout = console.Tty() os.Stderr = console.Tty() - t.Setenv("GH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER", "true") + t.Setenv("GH_ACCESSIBLE_PROMPTER", "true") // Using echo as the editor command here because it will immediately exit // and return no input. p := prompter.New("echo", nil, nil, nil) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index e0a1a853511..f7a827dcd51 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -109,7 +109,7 @@ var HelpTopics = []helpTopic{ wrapped on the terminal will be taken as the lesser of the terminal width, this value, or 120 if not specified. This value is used, for example, with %[1]spr view%[1]s subcommand. - %[1]sGH_SPEECH_SYNTHESIZER_FRIENDLY_PROMPTER%[1]s: set to a truthy value to enable prompts that are + %[1]sGH_ACCESSIBLE_PROMPTER%[1]s: set to a truthy value to enable prompts that are more compatible with speech synthesis based screen readers. `, "`"), }, From fa03157bebce3d0578d934a81e67d9b9acf7188d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:04:04 -0600 Subject: [PATCH 084/116] doc(help): label GH_ACCESSIBLE_PROMPTER as preview --- pkg/cmd/root/help_topic.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index f7a827dcd51..1a0934bb5ac 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -109,7 +109,7 @@ var HelpTopics = []helpTopic{ wrapped on the terminal will be taken as the lesser of the terminal width, this value, or 120 if not specified. This value is used, for example, with %[1]spr view%[1]s subcommand. - %[1]sGH_ACCESSIBLE_PROMPTER%[1]s: set to a truthy value to enable prompts that are + %[1]sGH_ACCESSIBLE_PROMPTER%[1]s (preview): set to a truthy value to enable prompts that are more compatible with speech synthesis based screen readers. `, "`"), }, From d230b08c4367266c60a3c3618b208d234d0a9ffb Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:09:15 -0600 Subject: [PATCH 085/116] test(prompter): re-add skipped test for accessible confirm default --- ...eech_synthesizer_friendly_prompter_test.go | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/speech_synthesizer_friendly_prompter_test.go index 20935a5669c..5488257070b 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/speech_synthesizer_friendly_prompter_test.go @@ -202,6 +202,26 @@ func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { wg.Wait() }) + // This test currently fails because the value is + // not respected as the default in accessible mode. + // See https://github.com/charmbracelet/huh/issues/615 + t.Run("Confirm - blank input returns default", func(t *testing.T) { + t.Skip("Skipped due to https://github.com/charmbracelet/huh/issues/615") + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Are you sure") + require.NoError(t, err) + + // Enter nothing + _, err = console.SendLine("") + require.NoError(t, err) + }() + + confirmValue, err := p.Confirm("Are you sure", false) + require.NoError(t, err) + require.Equal(t, false, confirmValue) + }) + t.Run("AuthToken", func(t *testing.T) { wg.Add(1) From c5206109ca9db2d2215b1de68c5ccc0cb9bfa548 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Tue, 8 Apr 2025 15:24:18 -0400 Subject: [PATCH 086/116] Use truthy value for `GH_COLOR_LABELS` --- pkg/cmd/factory/default.go | 9 +++++++-- pkg/cmd/factory/default_test.go | 32 +++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index 68df7a3169d..6286c999d0a 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -293,8 +293,13 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { io.SetPager(pager.Value) } - if _, ghColorLabels := os.LookupEnv("GH_COLOR_LABELS"); ghColorLabels { - io.SetColorLabels(true) // TODO: should this be a truthy value? + if ghColorLabels, ghColorLabelsExists := os.LookupEnv("GH_COLOR_LABELS"); ghColorLabelsExists { + switch ghColorLabels { + case "", "0", "false", "no": + io.SetColorLabels(false) + default: + io.SetColorLabels(true) + } } else if prompt := cfg.ColorLabels(""); prompt.Value == "enabled" { io.SetColorLabels(true) } diff --git a/pkg/cmd/factory/default_test.go b/pkg/cmd/factory/default_test.go index b1730d6e615..407c2fcdb15 100644 --- a/pkg/cmd/factory/default_test.go +++ b/pkg/cmd/factory/default_test.go @@ -449,10 +449,40 @@ func Test_ioStreams_colorLabels(t *testing.T) { colorLabelsEnabled: true, }, { - name: "colorLabels enabled via GH_COLOR_LABELS env var", + name: "colorLabels enabled via `1` in GH_COLOR_LABELS env var", env: map[string]string{"GH_COLOR_LABELS": "1"}, colorLabelsEnabled: true, }, + { + name: "colorLabels enabled via `true` in GH_COLOR_LABELS env var", + env: map[string]string{"GH_COLOR_LABELS": "true"}, + colorLabelsEnabled: true, + }, + { + name: "colorLabels enabled via `yes` in GH_COLOR_LABELS env var", + env: map[string]string{"GH_COLOR_LABELS": "yes"}, + colorLabelsEnabled: true, + }, + { + name: "colorLabels disable via empty string in GH_COLOR_LABELS env var", + env: map[string]string{"GH_COLOR_LABELS": ""}, + colorLabelsEnabled: false, + }, + { + name: "colorLabels disabled via `0` in GH_COLOR_LABELS env var", + env: map[string]string{"GH_COLOR_LABELS": "0"}, + colorLabelsEnabled: false, + }, + { + name: "colorLabels disabled via `false` in GH_COLOR_LABELS env var", + env: map[string]string{"GH_COLOR_LABELS": "false"}, + colorLabelsEnabled: false, + }, + { + name: "colorLabels disabled via `no` in GH_COLOR_LABELS env var", + env: map[string]string{"GH_COLOR_LABELS": "no"}, + colorLabelsEnabled: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 5c12f5633e3beee887be089a82b0ec52469ad087 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Tue, 8 Apr 2025 15:27:36 -0400 Subject: [PATCH 087/116] Test for explicit config disable of label colors --- pkg/cmd/factory/default_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/cmd/factory/default_test.go b/pkg/cmd/factory/default_test.go index 407c2fcdb15..c0275d1dec4 100644 --- a/pkg/cmd/factory/default_test.go +++ b/pkg/cmd/factory/default_test.go @@ -448,6 +448,11 @@ func Test_ioStreams_colorLabels(t *testing.T) { config: enableColorLabelsConfig(), colorLabelsEnabled: true, }, + { + name: "config with colorLabels disabled", + config: disableColorLabelsConfig(), + colorLabelsEnabled: false, + }, { name: "colorLabels enabled via `1` in GH_COLOR_LABELS env var", env: map[string]string{"GH_COLOR_LABELS": "1"}, @@ -611,6 +616,10 @@ func disablePromptConfig() gh.Config { return config.NewFromString("prompt: disabled") } +func disableColorLabelsConfig() gh.Config { + return config.NewFromString("color_labels: disabled") +} + func enableColorLabelsConfig() gh.Config { return config.NewFromString("color_labels: enabled") } From 644dbe6275861cec50b744330576f97b78f978b5 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Tue, 8 Apr 2025 15:56:23 -0400 Subject: [PATCH 088/116] Apply suggestions from code review Co-authored-by: Babak K. Shandiz --- pkg/cmd/pr/status/status.go | 2 +- pkg/cmd/release/view/view.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index d20522d04af..eb120e5a7df 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -316,6 +316,6 @@ func printPrs(io *iostreams.IOStreams, totalCount int, prs ...api.PullRequest) { } remaining := totalCount - len(prs) if remaining > 0 { - fmt.Fprintf(w, cs.Muted(" And %d more\n"), remaining) + fmt.Fprintln(w, cs.Mutedf(" And %d more", remaining)) } } diff --git a/pkg/cmd/release/view/view.go b/pkg/cmd/release/view/view.go index db0e6ae1d46..c9030f299fe 100644 --- a/pkg/cmd/release/view/view.go +++ b/pkg/cmd/release/view/view.go @@ -139,9 +139,9 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error { fmt.Fprintf(w, "%s • ", cs.Yellow("Pre-release")) } if release.IsDraft { - fmt.Fprintf(w, "%s\n", cs.Mutedf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt))) + fmt.Fprintln(w, cs.Mutedf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt))) } else { - fmt.Fprintf(w, "%s\n", cs.Mutedf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt))) + fmt.Fprintln(w, cs.Mutedf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt))) } renderedDescription, err := markdown.Render(release.Body, @@ -153,7 +153,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error { fmt.Fprintln(w, renderedDescription) if len(release.Assets) > 0 { - fmt.Fprintf(w, "%s\n", cs.Bold("Assets")) + fmt.Fprintln(w, cs.Bold("Assets")) //nolint:staticcheck // SA1019: Showing NAME|SIZE headers adds nothing to table. table := tableprinter.New(io, tableprinter.NoHeader) for _, a := range release.Assets { From 93e51c583b7f1cfa85c096f83d3abc7589836076 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Tue, 8 Apr 2025 16:40:39 -0400 Subject: [PATCH 089/116] Update pkg/cmd/run/shared/presentation.go --- pkg/cmd/run/shared/presentation.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/run/shared/presentation.go b/pkg/cmd/run/shared/presentation.go index 699ea120f54..a3556d74390 100644 --- a/pkg/cmd/run/shared/presentation.go +++ b/pkg/cmd/run/shared/presentation.go @@ -52,6 +52,7 @@ func RenderAnnotations(cs *iostreams.ColorScheme, annotations []Annotation) stri for _, a := range annotations { lines = append(lines, fmt.Sprintf("%s %s", AnnotationSymbol(cs, a), a.Message)) + // Following newline is essential for spacing between annotations lines = append(lines, cs.Mutedf("%s: %s#%d\n", a.JobName, a.Path, a.StartLine)) } From 7b20ee5549454db7007a6314e002d2e5ab683eca Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 9 Apr 2025 03:58:20 -0600 Subject: [PATCH 090/116] Merge pull request #10749 from malancas/update-to-sigstore-go-v0.7.1 Update github.com/sigstore/sigstore-go to 0.7.1 and fix breaking function change --- go.mod | 65 +++-- go.sum | 326 +++++++++++++------------- pkg/cmd/attestation/test/data/data.go | 10 +- 3 files changed, 196 insertions(+), 205 deletions(-) diff --git a/go.mod b/go.mod index bea712a2d54..c24bd57cc06 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.8 github.com/gdamore/tcell/v2 v2.5.4 github.com/golang/snappy v0.0.4 - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.3 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/websocket v1.5.3 @@ -40,18 +40,18 @@ 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.3.3 - github.com/sigstore/sigstore-go v0.7.0 - github.com/spf13/cobra v1.8.1 + github.com/sigstore/protobuf-specs v0.4.1 + github.com/sigstore/sigstore-go v0.7.1 + github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 github.com/zalando/go-keyring v0.2.5 - golang.org/x/crypto v0.35.0 - golang.org/x/sync v0.12.0 - golang.org/x/term v0.30.0 - golang.org/x/text v0.23.0 - google.golang.org/grpc v1.69.4 - google.golang.org/protobuf v1.36.5 + golang.org/x/crypto v0.37.0 + golang.org/x/sync v0.13.0 + golang.org/x/term v0.31.0 + golang.org/x/text v0.24.0 + google.golang.org/grpc v1.71.0 + google.golang.org/protobuf v1.36.6 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -83,29 +83,29 @@ require ( github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/fatih/color v1.16.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.0 // indirect + github.com/go-openapi/errors v0.22.1 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/loads v0.22.0 // indirect 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.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/certificate-transparency-go v1.3.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/hcl v1.0.0 // 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 @@ -116,8 +116,7 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -129,27 +128,26 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rodaine/table v1.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect 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.3.8 // indirect - github.com/sigstore/sigstore v1.8.12 // indirect - github.com/sigstore/timestamp-authority v1.2.4 // indirect + github.com/sigstore/rekor v1.3.9 // indirect + github.com/sigstore/sigstore v1.9.1 // indirect + github.com/sigstore/timestamp-authority v1.2.5 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/viper v1.19.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/viper v1.20.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect @@ -163,18 +161,17 @@ require ( github.com/yuin/goldmark-emoji v1.0.5 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.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-20240325151524-a685a6edb6d8 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.36.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.32.0 // indirect golang.org/x/tools v0.29.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect k8s.io/klog/v2 v2.130.1 // indirect ) diff --git a/go.sum b/go.sum index 2b5a3121206..a0f64b3343e 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,17 @@ -cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= -cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= -cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= -cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= -cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= +cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= -cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= -cloud.google.com/go/kms v1.20.4 h1:CJ0hMpOg1ANN9tx/a/GPJ+Uxudy8k6f3fvGFuTHiE5A= -cloud.google.com/go/kms v1.20.4/go.mod h1:gPLsp1r4FblUgBYPOcvI/bUPpdMg2Jm1ZVKU4tQUfcc= -cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= -cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM= +cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM= +cloud.google.com/go/kms v1.21.1 h1:r1Auo+jlfJSf8B7mUnVw5K0fI7jWyoUy65bV53VjKyk= +cloud.google.com/go/kms v1.21.1/go.mod h1:s0wCyByc9LjTdCjG88toVs70U9W+cc6RKFc8zAqX7nE= +cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= +cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -21,18 +20,18 @@ github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjq github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 h1:7rKG7UmnrxX4N53TFhkYqjc+kVUZuw0fL8I3Fh+Ld9E= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0/go.mod h1:Wjo+24QJVhhl/L7jy6w9yzFF2yDOf3cKECAa8ecf9vE= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8heteyfdmv+pcfHvhR9llnh7c7GMwZ8RVG04= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -53,36 +52,36 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo= -github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= -github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg= -github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk= -github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE= -github.com/aws/aws-sdk-go-v2/service/kms v1.37.8 h1:KbLZjYqhQ9hyB4HwXiheiflTlYQa0+Fz0Ms/rh5f3mk= -github.com/aws/aws-sdk-go-v2/service/kms v1.37.8/go.mod h1:ANs9kBhK4Ghj9z1W+bsr3WsNaPF71qkgd6eE6Ekol/Y= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc= -github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= -github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/config v1.29.10 h1:yNjgjiGBp4GgaJrGythyBXg2wAs+Im9fSWIUwvi1CAc= +github.com/aws/aws-sdk-go-v2/config v1.29.10/go.mod h1:A0mbLXSdtob/2t59n1X0iMkPQ5d+YzYZB4rwu7SZ7aA= +github.com/aws/aws-sdk-go-v2/credentials v1.17.63 h1:rv1V3kIJ14pdmTu01hwcMJ0WAERensSiD9rEWEBb1Tk= +github.com/aws/aws-sdk-go-v2/credentials v1.17.63/go.mod h1:EJj+yDf0txT26Ulo0VWTavBl31hOsaeuMxIHu2m0suY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +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.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 h1:wK8O+j2dOolmpNVY1EWIbLgxrGCHJKVPm08Hv/u80M8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= @@ -131,7 +130,6 @@ 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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -167,8 +165,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= @@ -177,8 +175,6 @@ github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/ github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -188,8 +184,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= +github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= @@ -202,32 +198,34 @@ 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.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= @@ -236,8 +234,8 @@ github.com/google/trillian v1.7.1 h1:+zX8jLM3524bAMPS+VxaDIDgsMv3/ty6DuLWerHXcek github.com/google/trillian v1.7.1/go.mod h1:E1UMAHqpZCA8AQdrKdWmHmtUfSeiD0sDWD1cv00Xa+c= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -265,13 +263,12 @@ github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0S github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= -github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= +github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -330,10 +327,8 @@ github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2T github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -378,8 +373,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -387,12 +382,12 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0= @@ -409,10 +404,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= 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= @@ -429,51 +422,44 @@ 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.3.3 h1:RMZQgXTD/pF7KW6b5NaRLYxFYZ/wzx44PQFXN2PEo5g= -github.com/sigstore/protobuf-specs v0.3.3/go.mod h1:vIhZ6Uor1a38+wvRrKcqL2PtYNlgoIW9lhzYzkyy4EU= -github.com/sigstore/rekor v1.3.8 h1:B8kJI8mpSIXova4Jxa6vXdJyysRxFGsEsLKBDl0rRjA= -github.com/sigstore/rekor v1.3.8/go.mod h1:/dHFYKSuxEygfDRnEwyJ+ZD6qoVYNXQdi1mJrKvKWsI= -github.com/sigstore/sigstore v1.8.12 h1:S8xMVZbE2z9ZBuQUEG737pxdLjnbOIcFi5v9UFfkJFc= -github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo= -github.com/sigstore/sigstore-go v0.7.0 h1:bIGPc2IbnbxnzlqQcKlh1o96bxVJ4yRElpP1gHrOH48= -github.com/sigstore/sigstore-go v0.7.0/go.mod h1:4RrCK+i+jhx7lyOG2Vgef0/kFLbKlDI1hrioUYvkxxA= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12 h1:EC3UmIaa7nV9sCgSpVevmvgvTYTkMqyrRbj5ojPp7tE= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12/go.mod h1:aw60vs3crnQdM/DYH+yF2P0MVKtItwAX34nuaMrY7Lk= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12 h1:FPpliDTywSy0woLHMAdmTSZ5IS/lVBZ0dY0I+2HmnSY= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12/go.mod h1:NkPiz4XA0JcBSXzJUrjMj7Xi7oSTew1Ip3Zmt56mHlw= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12 h1:kweBChR6M9FEvmxN3BMEcl7SNnwxTwKF7THYFKLOE5U= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12/go.mod h1:6+d+A6oYt1W5OgtzgEVb21V7tAZ/C2Ihtzc5MNJbayY= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12 h1:jvY1B9bjP+tKzdKDyuq5K7O19CG2IKzGJNTy5tuL2Gs= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12/go.mod h1:2uEeOb8xE2RC6OvzxKux1wkS39Zv8gA27z92m49xUTc= -github.com/sigstore/timestamp-authority v1.2.4 h1:RjXZxOWorEiem/uSr0pFHVtQpyzpcFxgugo5jVqm3mw= -github.com/sigstore/timestamp-authority v1.2.4/go.mod h1:ExrbobKdEuwuBptZIiKp1IaVBRiUeKbiuSyZTO8Okik= +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/rekor v1.3.9 h1:sUjRpKVh/hhgqGMs0t+TubgYsksArZ6poLEC3MsGAzU= +github.com/sigstore/rekor v1.3.9/go.mod h1:xThNUhm6eNEmkJ/SiU/FVU7pLY2f380fSDZFsdDWlcM= +github.com/sigstore/sigstore v1.9.1 h1:bNMsfFATsMPaagcf+uppLk4C9rQZ2dh5ysmCxQBYWaw= +github.com/sigstore/sigstore v1.9.1/go.mod h1:zUoATYzR1J3rLNp3jmp4fzIJtWdhC3ZM6MnpcBtnsE4= +github.com/sigstore/sigstore-go v0.7.1 h1:lyzi3AjO6+BHc5zCf9fniycqPYOt3RaC08M/FRmQhVY= +github.com/sigstore/sigstore-go v0.7.1/go.mod h1:AIRj4I3LC82qd07VFm3T2zXYiddxeBV1k/eoS8nTz0E= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.1 h1:/YcNq687WnXpIRXl04nLfJX741G4iW+w+7Nem2Zy0f4= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.1/go.mod h1:ApL9RpKsi7gkSYN0bMNdm/3jZ9EefxMmfYHfUmq2ZYM= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.1 h1:FnusXyTIInnwfIOzzl5PFilRm1I97dxMSOcCkZBu9Kc= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.1/go.mod h1:d5m5LOa/69a+t2YC9pDPwS1n2i/PhqB4cUKbpVDlKKE= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.1 h1:LFiYK1DEWQ6Hf/nroFzBMM+s5rVSjVL45Alpb5Ctl5A= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.1/go.mod h1:GFyFmDsE2wDuIHZD+4+JErGpA0S4zJsKNz5l2JVJd8s= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.1 h1:sIW6xe4yU5eIMH8fve2C78d+r29KmHnIb+7po+80bsY= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.1/go.mod h1:3pNf99GnK9eu3XUa5ebHzgEQSVYf9hqAoPFwbwD6O6M= +github.com/sigstore/timestamp-authority v1.2.5 h1:W22JmwRv1Salr/NFFuP7iJuhytcZszQjldoB8GiEdnw= +github.com/sigstore/timestamp-authority v1.2.5/go.mod h1:gWPKWq4HMWgPCETre0AakgBzcr9DRqHrsgbrRqsigOs= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +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 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -484,6 +470,12 @@ github.com/theupdateframework/go-tuf/v2 v2.0.2 h1:PyNnjV9BJNzN1ZE6BcWK+5JbF+if37 github.com/theupdateframework/go-tuf/v2 v2.0.2/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= +github.com/tink-crypto/tink-go/v2 v2.3.0 h1:4/TA0lw0lA/iVKBL9f8R5eP7397bfc4antAMXF5JRhs= +github.com/tink-crypto/tink-go/v2 v2.3.0/go.mod h1:kfPOtXIadHlekBTeBtJrHWqoGL+Fm3JQg0wtltPuxLU= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= @@ -504,22 +496,22 @@ go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= 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/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.step.sm/crypto v0.57.0 h1:YjoRQDaJYAxHLVwjst0Bl0xcnoKzVwuHCJtEo2VSHYU= -go.step.sm/crypto v0.57.0/go.mod h1:+Lwp5gOVPaTa3H/Ul/TzGbxQPXZZcKIUGMS0lG6n9Go= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.step.sm/crypto v0.60.0 h1:UgSw8DFG5xUOGB3GUID17UA32G4j1iNQ4qoMhBmsVFw= +go.step.sm/crypto v0.60.0/go.mod h1:Ep83Lv818L4gV0vhFTdPWRKnL6/5fRMpi8SaoP5ArSw= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -528,24 +520,24 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -555,46 +547,44 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY= -google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc= +google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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= diff --git a/pkg/cmd/attestation/test/data/data.go b/pkg/cmd/attestation/test/data/data.go index b33efaa28d9..ef3c35c2034 100644 --- a/pkg/cmd/attestation/test/data/data.go +++ b/pkg/cmd/attestation/test/data/data.go @@ -5,13 +5,17 @@ import ( "testing" "github.com/sigstore/sigstore-go/pkg/bundle" - sgData "github.com/sigstore/sigstore-go/pkg/testing/data" ) //go:embed sigstore-js-2.1.0-bundle.json var SigstoreBundleRaw []byte -// SigstoreBundle returns a test *sigstore.Bundle +// SigstoreBundle returns a test sigstore-go bundle.Bundle func SigstoreBundle(t *testing.T) *bundle.Bundle { - return sgData.TestBundle(t, SigstoreBundleRaw) + b := &bundle.Bundle{} + err := b.UnmarshalJSON(SigstoreBundleRaw) + if err != nil { + t.Fatalf("failed to unmarshal sigstore bundle: %v", err) + } + return b } From 3b2e7f7f712b473dc4378b56d907f98c80255931 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 9 Apr 2025 08:13:21 -0600 Subject: [PATCH 091/116] chore: go mod tidy --- go.mod | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 3a800505db9..c44b83a200f 100644 --- a/go.mod +++ b/go.mod @@ -126,7 +126,8 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect From a34c9ea79937f4cb7b2d01ee4e60d495d86d7abe Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 9 Apr 2025 08:20:07 -0600 Subject: [PATCH 092/116] doc(prompter env): accessible prompter includes braille reader Co-authored-by: Andy Feller --- pkg/cmd/root/help_topic.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index dc39fd8d2ee..b85d64ca37f 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -113,7 +113,7 @@ var HelpTopics = []helpTopic{ not specified. This value is used, for example, with %[1]spr view%[1]s subcommand. %[1]sGH_ACCESSIBLE_PROMPTER%[1]s (preview): set to a truthy value to enable prompts that are - more compatible with speech synthesis based screen readers. + more compatible with speech synthesis and braille screen readers. `, "`"), }, { From 8fc8486af5ff47730c0f5021d16a6009377849d6 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 9 Apr 2025 08:24:54 -0600 Subject: [PATCH 093/116] refactor(prompter): rename speechSynthesizerFriendlyPrompter to accessiblePrompter --- ...er_test.go => accessible_prompter_test.go} | 10 ++++---- internal/prompter/prompter.go | 24 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) rename internal/prompter/{speech_synthesizer_friendly_prompter_test.go => accessible_prompter_test.go} (97%) diff --git a/internal/prompter/speech_synthesizer_friendly_prompter_test.go b/internal/prompter/accessible_prompter_test.go similarity index 97% rename from internal/prompter/speech_synthesizer_friendly_prompter_test.go rename to internal/prompter/accessible_prompter_test.go index 5488257070b..fcb806641cb 100644 --- a/internal/prompter/speech_synthesizer_friendly_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -19,7 +19,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSpeechSynthesizerFriendlyPrompter(t *testing.T) { +func TestAccessiblePrompter(t *testing.T) { // Create a PTY and hook up a virtual terminal emulator ptm, pts, err := pty.Open() require.NoError(t, err) @@ -459,9 +459,9 @@ func TestSurveyPrompter(t *testing.T) { var wg sync.WaitGroup // This not a comprehensive test of the survey prompter, but it does - // demonstrate that the survey prompter is used when the speech - // synthesizer friendly prompter is disabled. - t.Run("Select uses survey prompter when speech synthesizer friendly prompter is disabled", func(t *testing.T) { + // demonstrate that the survey prompter is used when the + // accessible prompter is disabled. + t.Run("Select uses survey prompter when accessible prompter is disabled", func(t *testing.T) { wg.Add(1) go func() { @@ -471,7 +471,7 @@ func TestSurveyPrompter(t *testing.T) { require.NoError(t, err) // Send a newline to select the first option - // Note: This would not work with the speech synthesizer friendly prompter + // Note: This would not work with the accessible prompter // because it would requires sending a 1 to select the first option. // So it proves we are seeing a survey prompter. _, err = console.SendLine("") diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index bf5655846b1..f3a87c4758c 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -48,7 +48,7 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr falseyValues := []string{"false", "0", "no", ""} if accessiblePrompterIsSet && !slices.Contains(falseyValues, accessiblePrompterValue) { - return &speechSynthesizerFriendlyPrompter{ + return &accessiblePrompter{ stdin: stdin, stdout: stdout, stderr: stderr, @@ -65,14 +65,14 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr } } -type speechSynthesizerFriendlyPrompter struct { +type accessiblePrompter struct { stdin ghPrompter.FileReader stdout ghPrompter.FileWriter stderr ghPrompter.FileWriter editorCmd string } -func (p *speechSynthesizerFriendlyPrompter) newForm(groups ...*huh.Group) *huh.Form { +func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form { return huh.NewForm(groups...). WithTheme(huh.ThemeBase16()). WithAccessible(true) @@ -80,7 +80,7 @@ func (p *speechSynthesizerFriendlyPrompter) newForm(groups ...*huh.Group) *huh.F // WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin)) } -func (p *speechSynthesizerFriendlyPrompter) Select(prompt, _ string, options []string) (int, error) { +func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, error) { var result int formOptions := []huh.Option[int]{} for i, o := range options { @@ -100,7 +100,7 @@ func (p *speechSynthesizerFriendlyPrompter) Select(prompt, _ string, options []s return result, err } -func (p *speechSynthesizerFriendlyPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { +func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { var result []int formOptions := make([]huh.Option[int], len(options)) for i, o := range options { @@ -125,7 +125,7 @@ func (p *speechSynthesizerFriendlyPrompter) MultiSelect(prompt string, defaults return result[:mid], nil } -func (p *speechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) (string, error) { +func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) { result := defaultValue prompt = fmt.Sprintf("%s (%s)", prompt, defaultValue) form := p.newForm( @@ -144,7 +144,7 @@ func (p *speechSynthesizerFriendlyPrompter) Input(prompt, defaultValue string) ( return result, err } -func (p *speechSynthesizerFriendlyPrompter) Password(prompt string) (string, error) { +func (p *accessiblePrompter) Password(prompt string) (string, error) { var result string // EchoMode(huh.EchoModePassword) doesn't have any effect in accessible mode. form := p.newForm( @@ -163,7 +163,7 @@ func (p *speechSynthesizerFriendlyPrompter) Password(prompt string) (string, err return result, nil } -func (p *speechSynthesizerFriendlyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) { +func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) { // This is currently an ineffectual assignment because the value is // not respected as the default in accessible mode. Leaving this in here // because it may change in the future. @@ -182,7 +182,7 @@ func (p *speechSynthesizerFriendlyPrompter) Confirm(prompt string, defaultValue return result, nil } -func (p *speechSynthesizerFriendlyPrompter) AuthToken() (string, error) { +func (p *accessiblePrompter) AuthToken() (string, error) { var result string form := p.newForm( huh.NewGroup( @@ -205,7 +205,7 @@ func (p *speechSynthesizerFriendlyPrompter) AuthToken() (string, error) { return result, err } -func (p *speechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string) error { +func (p *accessiblePrompter) ConfirmDeletion(requiredValue string) error { form := p.newForm( huh.NewGroup( huh.NewInput(). @@ -222,7 +222,7 @@ func (p *speechSynthesizerFriendlyPrompter) ConfirmDeletion(requiredValue string return form.Run() } -func (p *speechSynthesizerFriendlyPrompter) InputHostname() (string, error) { +func (p *accessiblePrompter) InputHostname() (string, error) { var result string form := p.newForm( huh.NewGroup( @@ -240,7 +240,7 @@ func (p *speechSynthesizerFriendlyPrompter) InputHostname() (string, error) { return result, nil } -func (p *speechSynthesizerFriendlyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { +func (p *accessiblePrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { var result string skipOption := "skip" openOption := "open" From 2f5e8965355bd120364cccb52e6f973a6e85718c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:18:53 -0600 Subject: [PATCH 094/116] fix(prompter): update `huh` and fix tests --- go.mod | 6 +++--- go.sum | 6 ++++++ internal/prompter/accessible_prompter_test.go | 4 ---- internal/prompter/prompter.go | 7 +------ 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index c44b83a200f..e0f76dce842 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/briandowns/spinner v1.18.1 github.com/cenkalti/backoff/v4 v4.3.0 github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 - github.com/charmbracelet/huh v0.6.0 + github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5 github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc github.com/cli/go-gh/v2 v2.12.0 github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 @@ -71,8 +71,8 @@ require ( 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 - github.com/catppuccin/go v0.2.0 // indirect - github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect github.com/charmbracelet/bubbletea v1.3.4 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect diff --git a/go.sum b/go.sum index 0c23b57d1cb..bf7f39142b7 100644 --- a/go.sum +++ b/go.sum @@ -98,12 +98,16 @@ github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gL github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= @@ -112,6 +116,8 @@ github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25S github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s= github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= +github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5 h1:uOnMxWghHfEYm2DPMeIHHAEirV/TduBVC9ZRXGcX9Q8= +github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5/go.mod h1:xl27E/xNaX3WwdkqpvBwjJcGWhupkU52CWLC5hReBTw= github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys= github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index fcb806641cb..0b0f6844f75 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -202,11 +202,7 @@ func TestAccessiblePrompter(t *testing.T) { wg.Wait() }) - // This test currently fails because the value is - // not respected as the default in accessible mode. - // See https://github.com/charmbracelet/huh/issues/615 t.Run("Confirm - blank input returns default", func(t *testing.T) { - t.Skip("Skipped due to https://github.com/charmbracelet/huh/issues/615") go func() { // Wait for prompt to appear _, err := console.ExpectString("Are you sure") diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index f3a87c4758c..d50150098ec 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -121,8 +121,7 @@ func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, optio return nil, err } - mid := len(result) / 2 - return result[:mid], nil + return result, nil } func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) { @@ -164,10 +163,6 @@ func (p *accessiblePrompter) Password(prompt string) (string, error) { } func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) { - // This is currently an ineffectual assignment because the value is - // not respected as the default in accessible mode. Leaving this in here - // because it may change in the future. - // See https://github.com/charmbracelet/huh/issues/615 result := defaultValue form := p.newForm( huh.NewGroup( From fab0de5583da0bc9373372a9049159162fb009a0 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:37:48 -0600 Subject: [PATCH 095/116] fix(prompter): pass io to `huh` and refactor tests --- internal/prompter/accessible_prompter_test.go | 218 +++++++----------- internal/prompter/prompter.go | 6 +- 2 files changed, 83 insertions(+), 141 deletions(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index 0b0f6844f75..ed96cd65c1e 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -5,9 +5,7 @@ package prompter_test import ( "fmt" "io" - "os" "strings" - "sync" "testing" "time" @@ -20,54 +18,11 @@ import ( ) func TestAccessiblePrompter(t *testing.T) { - // Create a PTY and hook up a virtual terminal emulator - ptm, pts, err := pty.Open() - require.NoError(t, err) - - term := vt10x.New(vt10x.WithWriter(pts)) - - // Create a console via Expect that allows scripting against the terminal - consoleOpts := []expect.ConsoleOpt{ - expect.WithStdin(ptm), - expect.WithStdout(term), - expect.WithCloser(ptm, pts), - failOnExpectError(t), - failOnSendError(t), - expect.WithDefaultTimeout(time.Second), - } - - console, err := expect.NewConsole(consoleOpts...) - require.NoError(t, err) - t.Cleanup(func() { testCloser(t, console) }) - - // Using OS here because huh currently ignores configured iostreams - // See https://github.com/charmbracelet/huh/issues/612 - stdIn := os.Stdin - stdOut := os.Stdout - stdErr := os.Stderr - - t.Cleanup(func() { - os.Stdin = stdIn - os.Stdout = stdOut - os.Stderr = stdErr - }) - - os.Stdin = console.Tty() - os.Stdout = console.Tty() - os.Stderr = console.Tty() - - t.Setenv("GH_ACCESSIBLE_PROMPTER", "true") - // Using echo as the editor command here because it will immediately exit - // and return no input. - p := prompter.New("echo", nil, nil, nil) - - var wg sync.WaitGroup - t.Run("Select", func(t *testing.T) { - wg.Add(1) + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Choose:") require.NoError(t, err) @@ -80,15 +35,13 @@ func TestAccessiblePrompter(t *testing.T) { selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"}) require.NoError(t, err) assert.Equal(t, 0, selectValue) - - wg.Wait() }) t.Run("MultiSelect", func(t *testing.T) { - wg.Add(1) + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Select a number") require.NoError(t, err) @@ -107,16 +60,14 @@ func TestAccessiblePrompter(t *testing.T) { multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"}) require.NoError(t, err) assert.Equal(t, []int{0, 1}, multiSelectValue) - - wg.Wait() }) t.Run("Input", func(t *testing.T) { - wg.Add(1) - + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) dummyText := "12345abcdefg" + go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Enter some characters") require.NoError(t, err) @@ -129,16 +80,14 @@ func TestAccessiblePrompter(t *testing.T) { inputValue, err := p.Input("Enter some characters", "") require.NoError(t, err) assert.Equal(t, dummyText, inputValue) - - wg.Wait() }) t.Run("Input - blank input returns default value", func(t *testing.T) { - wg.Add(1) - + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) dummyDefaultValue := "12345abcdefg" + go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Enter some characters") require.NoError(t, err) @@ -155,16 +104,14 @@ func TestAccessiblePrompter(t *testing.T) { inputValue, err := p.Input("Enter some characters", dummyDefaultValue) require.NoError(t, err) assert.Equal(t, dummyDefaultValue, inputValue) - - wg.Wait() }) t.Run("Password", func(t *testing.T) { - wg.Add(1) - + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) dummyPassword := "12345abcdefg" + go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Enter password") require.NoError(t, err) @@ -177,15 +124,13 @@ func TestAccessiblePrompter(t *testing.T) { passwordValue, err := p.Password("Enter password") require.NoError(t, err) require.Equal(t, dummyPassword, passwordValue) - - wg.Wait() }) t.Run("Confirm", func(t *testing.T) { - wg.Add(1) + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Are you sure") require.NoError(t, err) @@ -198,11 +143,12 @@ func TestAccessiblePrompter(t *testing.T) { confirmValue, err := p.Confirm("Are you sure", false) require.NoError(t, err) require.Equal(t, true, confirmValue) - - wg.Wait() }) t.Run("Confirm - blank input returns default", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) + go func() { // Wait for prompt to appear _, err := console.ExpectString("Are you sure") @@ -219,11 +165,11 @@ func TestAccessiblePrompter(t *testing.T) { }) t.Run("AuthToken", func(t *testing.T) { - wg.Add(1) - + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) dummyAuthToken := "12345abcdefg" + go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Paste your authentication token:") require.NoError(t, err) @@ -236,16 +182,14 @@ func TestAccessiblePrompter(t *testing.T) { authValue, err := p.AuthToken() require.NoError(t, err) require.Equal(t, dummyAuthToken, authValue) - - wg.Wait() }) t.Run("AuthToken - blank input returns error", func(t *testing.T) { - wg.Add(1) - + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) dummyAuthTokenForAfterFailure := "12345abcdefg" + go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Paste your authentication token:") require.NoError(t, err) @@ -266,16 +210,14 @@ func TestAccessiblePrompter(t *testing.T) { authValue, err := p.AuthToken() require.NoError(t, err) require.Equal(t, dummyAuthTokenForAfterFailure, authValue) - - wg.Wait() }) t.Run("ConfirmDeletion", func(t *testing.T) { - wg.Add(1) + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) requiredValue := "test" go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue)) require.NoError(t, err) @@ -288,17 +230,15 @@ func TestAccessiblePrompter(t *testing.T) { // An err indicates that the confirmation text sent did not match err := p.ConfirmDeletion(requiredValue) require.NoError(t, err) - - wg.Wait() }) t.Run("ConfirmDeletion - bad input", func(t *testing.T) { - wg.Add(1) - + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) requiredValue := "test" badInputValue := "garbage" + go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue)) require.NoError(t, err) @@ -319,16 +259,14 @@ func TestAccessiblePrompter(t *testing.T) { // An err indicates that the confirmation text sent did not match err := p.ConfirmDeletion(requiredValue) require.NoError(t, err) - - wg.Wait() }) t.Run("InputHostname", func(t *testing.T) { - wg.Add(1) - + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) hostname := "example.com" + go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("Hostname:") require.NoError(t, err) @@ -341,15 +279,13 @@ func TestAccessiblePrompter(t *testing.T) { inputValue, err := p.InputHostname() require.NoError(t, err) require.Equal(t, hostname, inputValue) - - wg.Wait() }) t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) { - wg.Add(1) + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("How to edit?") require.NoError(t, err) @@ -362,16 +298,14 @@ func TestAccessiblePrompter(t *testing.T) { inputValue, err := p.MarkdownEditor("How to edit?", "", true) require.NoError(t, err) require.Equal(t, "", inputValue) - - wg.Wait() }) t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) { - wg.Add(1) - + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) defaultValue := "12345abcdefg" + go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("How to edit?") require.NoError(t, err) @@ -392,15 +326,13 @@ func TestAccessiblePrompter(t *testing.T) { inputValue, err := p.MarkdownEditor("How to edit?", defaultValue, false) require.NoError(t, err) require.Equal(t, defaultValue, inputValue) - - wg.Wait() }) t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) { - wg.Add(1) + console := newTestVirtualTerminal(t) + p := newTestAcessiblePrompter(t, console) go func() { - defer wg.Done() // Wait for prompt to appear _, err := console.ExpectString("How to edit?") require.NoError(t, err) @@ -422,12 +354,39 @@ func TestAccessiblePrompter(t *testing.T) { inputValue, err := p.MarkdownEditor("How to edit?", "", false) require.NoError(t, err) require.Equal(t, "", inputValue) - - wg.Wait() }) } func TestSurveyPrompter(t *testing.T) { + // This not a comprehensive test of the survey prompter, but it does + // demonstrate that the survey prompter is used when the + // accessible prompter is disabled. + t.Run("Select uses survey prompter when accessible prompter is disabled", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestSurveyPrompter(t, console) + + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Select a number") + require.NoError(t, err) + + // Send a newline to select the first option + // Note: This would not work with the accessible prompter + // because it would requires sending a 1 to select the first option. + // So it proves we are seeing a survey prompter. + _, err = console.SendLine("") + require.NoError(t, err) + }() + + selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"}) + require.NoError(t, err) + assert.Equal(t, 0, selectValue) + }) +} + +func newTestVirtualTerminal(t testing.TB) *expect.Console { + t.Helper() + // Create a PTY and hook up a virtual terminal emulator ptm, pts, err := pty.Open() require.NoError(t, err) @@ -441,45 +400,28 @@ func TestSurveyPrompter(t *testing.T) { expect.WithCloser(ptm, pts), failOnExpectError(t), failOnSendError(t), - expect.WithDefaultTimeout(time.Second * 600), + expect.WithDefaultTimeout(time.Second), } console, err := expect.NewConsole(consoleOpts...) require.NoError(t, err) t.Cleanup(func() { testCloser(t, console) }) - // Using echo as the editor command here because it will immediately exit - // and return no input. - p := prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) - - var wg sync.WaitGroup - - // This not a comprehensive test of the survey prompter, but it does - // demonstrate that the survey prompter is used when the - // accessible prompter is disabled. - t.Run("Select uses survey prompter when accessible prompter is disabled", func(t *testing.T) { - wg.Add(1) + return console +} - go func() { - defer wg.Done() - // Wait for prompt to appear - _, err := console.ExpectString("Select a number") - require.NoError(t, err) +func newTestAcessiblePrompter(t testing.TB, console *expect.Console) prompter.Prompter { + t.Helper() - // Send a newline to select the first option - // Note: This would not work with the accessible prompter - // because it would requires sending a 1 to select the first option. - // So it proves we are seeing a survey prompter. - _, err = console.SendLine("") - require.NoError(t, err) - }() + t.Setenv("GH_ACCESSIBLE_PROMPTER", "true") + return prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) +} - selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"}) - require.NoError(t, err) - assert.Equal(t, 0, selectValue) +func newTestSurveyPrompter(t testing.TB, console *expect.Console) prompter.Prompter { + t.Helper() - wg.Wait() - }) + t.Setenv("GH_ACCESSIBLE_PROMPTER", "false") + return prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) } // failOnExpectError adds an observer that will fail the test in a standardised way diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index d50150098ec..6cdbc9a8726 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -75,9 +75,9 @@ type accessiblePrompter struct { func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form { return huh.NewForm(groups...). WithTheme(huh.ThemeBase16()). - WithAccessible(true) - // Commented out because https://github.com/charmbracelet/huh/issues/612 - // WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin)) + WithAccessible(true). + WithInput(p.stdin). + WithOutput(p.stdout) } func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, error) { From 46150697735ba6965e851a68456533b2af8e5035 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:43:18 -0600 Subject: [PATCH 096/116] chore: go mod tidy --- go.sum | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/go.sum b/go.sum index bf7f39142b7..5441c96c553 100644 --- a/go.sum +++ b/go.sum @@ -96,16 +96,12 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= -github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= -github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= -github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= @@ -114,8 +110,6 @@ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4p github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25SvI2WiZdt/gxINcYBnHD7PE2Vr9auqwg5B05g= github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s= -github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= -github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5 h1:uOnMxWghHfEYm2DPMeIHHAEirV/TduBVC9ZRXGcX9Q8= github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5/go.mod h1:xl27E/xNaX3WwdkqpvBwjJcGWhupkU52CWLC5hReBTw= github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys= @@ -124,8 +118,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= From 512fac2dba4a302561bef26b3aa8808ed0d5a927 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Thu, 10 Apr 2025 08:39:12 +0200 Subject: [PATCH 097/116] Simplify cosign verification example by not using a regex. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad012588c36..cefe1abb074 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ There are two common ways to verify a downloaded release, depending if `gh` is a $ cosign verify-blob-attestation --bundle cli-cli-attestation-3120304.sigstore.json \ --new-bundle-format \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ - --certificate-identity-regexp='^https://github\.com/cli/cli/\.github/workflows/deployment\.yml@refs/heads/trunk$' \ + --certificate-identity="https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk" \ gh_2.62.0_macOS_arm64.zip Verified OK ``` From f0d4acd501ef19a3e24f0e9436c83460683bb0b1 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 10 Apr 2025 14:00:24 +0100 Subject: [PATCH 098/116] Add tests for `IsPinned` method Signed-off-by: Babak K. Shandiz --- pkg/cmd/extension/extension_test.go | 79 +++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/pkg/cmd/extension/extension_test.go b/pkg/cmd/extension/extension_test.go index 6f0afc3a6cc..a0635c9a277 100644 --- a/pkg/cmd/extension/extension_test.go +++ b/pkg/cmd/extension/extension_test.go @@ -102,3 +102,82 @@ func TestOwnerCached(t *testing.T) { assert.Equal(t, "cli", e.Owner()) } + +func TestIsPinnedBinaryExtensionUnpinned(t *testing.T) { + tempDir := t.TempDir() + extName := "gh-bin-ext" + extDir := filepath.Join(tempDir, "extensions", extName) + extPath := filepath.Join(extDir, extName) + bm := binManifest{ + Name: "gh-bin-ext", + } + assert.NoError(t, stubBinaryExtension(extDir, bm)) + e := &Extension{ + kind: BinaryKind, + path: extPath, + } + + assert.False(t, e.IsPinned()) +} + +func TestIsPinnedBinaryExtensionPinned(t *testing.T) { + tempDir := t.TempDir() + extName := "gh-bin-ext" + extDir := filepath.Join(tempDir, "extensions", extName) + extPath := filepath.Join(extDir, extName) + bm := binManifest{ + Name: "gh-bin-ext", + IsPinned: true, + } + assert.NoError(t, stubBinaryExtension(extDir, bm)) + e := &Extension{ + kind: BinaryKind, + path: extPath, + } + + assert.True(t, e.IsPinned()) +} + +func TestIsPinnedGitExtensionUnpinned(t *testing.T) { + tempDir := t.TempDir() + extPath := filepath.Join(tempDir, "extensions", "gh-local", "gh-local") + assert.NoError(t, stubExtension(extPath)) + + gc := &mockGitClient{} + gc.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("abcd1234", nil) + e := &Extension{ + kind: GitKind, + gitClient: gc, + path: extPath, + } + + assert.False(t, e.IsPinned()) +} + +func TestIsPinnedGitExtensionPinned(t *testing.T) { + tempDir := t.TempDir() + extPath := filepath.Join(tempDir, "extensions", "gh-local", "gh-local") + assert.NoError(t, stubPinnedExtension(extPath, "abcd1234")) + + gc := &mockGitClient{} + gc.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("abcd1234", nil) + e := &Extension{ + kind: GitKind, + gitClient: gc, + path: extPath, + } + + assert.True(t, e.IsPinned()) +} + +func TestIsPinnedLocalExtension(t *testing.T) { + tempDir := t.TempDir() + extPath := filepath.Join(tempDir, "extensions", "gh-local", "gh-local") + assert.NoError(t, stubLocalExtension(tempDir, extPath)) + e := &Extension{ + kind: LocalKind, + path: extPath, + } + + assert.False(t, e.IsPinned()) +} From 791e1af8286a2e97b50ce2da800bf8168a07ec7d Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 10 Apr 2025 14:09:49 +0100 Subject: [PATCH 099/116] Add missing `gc.AssertExpectations` calls Signed-off-by: Babak K. Shandiz --- pkg/cmd/extension/extension_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/extension/extension_test.go b/pkg/cmd/extension/extension_test.go index a0635c9a277..6928c6ed93d 100644 --- a/pkg/cmd/extension/extension_test.go +++ b/pkg/cmd/extension/extension_test.go @@ -152,6 +152,7 @@ func TestIsPinnedGitExtensionUnpinned(t *testing.T) { } assert.False(t, e.IsPinned()) + gc.AssertExpectations(t) } func TestIsPinnedGitExtensionPinned(t *testing.T) { @@ -168,6 +169,7 @@ func TestIsPinnedGitExtensionPinned(t *testing.T) { } assert.True(t, e.IsPinned()) + gc.AssertExpectations(t) } func TestIsPinnedLocalExtension(t *testing.T) { From 47d603221d4cafebfc79619eb2296b8ea2328037 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:37:36 -0600 Subject: [PATCH 100/116] test(prompter): use *testing.T instead --- internal/prompter/accessible_prompter_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index ed96cd65c1e..e80c3085ee2 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -384,7 +384,7 @@ func TestSurveyPrompter(t *testing.T) { }) } -func newTestVirtualTerminal(t testing.TB) *expect.Console { +func newTestVirtualTerminal(t *testing.T) *expect.Console { t.Helper() // Create a PTY and hook up a virtual terminal emulator @@ -410,14 +410,14 @@ func newTestVirtualTerminal(t testing.TB) *expect.Console { return console } -func newTestAcessiblePrompter(t testing.TB, console *expect.Console) prompter.Prompter { +func newTestAcessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter { t.Helper() t.Setenv("GH_ACCESSIBLE_PROMPTER", "true") return prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) } -func newTestSurveyPrompter(t testing.TB, console *expect.Console) prompter.Prompter { +func newTestSurveyPrompter(t *testing.T, console *expect.Console) prompter.Prompter { t.Helper() t.Setenv("GH_ACCESSIBLE_PROMPTER", "false") @@ -429,7 +429,7 @@ func newTestSurveyPrompter(t testing.TB, console *expect.Console) prompter.Promp // assertion. // // Use WithRelaxedIO to disable this behaviour. -func failOnExpectError(t testing.TB) expect.ConsoleOpt { +func failOnExpectError(t *testing.T) expect.ConsoleOpt { t.Helper() return expect.WithExpectObserver( func(matchers []expect.Matcher, buf string, err error) { @@ -456,7 +456,7 @@ func failOnExpectError(t testing.TB) expect.ConsoleOpt { // if any sending of input fails, without requiring an explicit assertion. // // Use WithRelaxedIO to disable this behaviour. -func failOnSendError(t testing.TB) expect.ConsoleOpt { +func failOnSendError(t *testing.T) expect.ConsoleOpt { t.Helper() return expect.WithSendObserver( func(msg string, n int, err error) { @@ -473,7 +473,7 @@ func failOnSendError(t testing.TB) expect.ConsoleOpt { } // testCloser is a helper to fail the test if a Closer fails to close. -func testCloser(t testing.TB, closer io.Closer) { +func testCloser(t *testing.T, closer io.Closer) { t.Helper() if err := closer.Close(); err != nil { t.Errorf("Close failed: %s", err) From 8b70870f4f553ad364795e7523c1af00714b8ccf Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:39:59 -0600 Subject: [PATCH 101/116] test(prompter): describe why echo is editorcmd --- internal/prompter/accessible_prompter_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index e80c3085ee2..6d2bee92c20 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -414,6 +414,9 @@ func newTestAcessiblePrompter(t *testing.T, console *expect.Console) prompter.Pr t.Helper() t.Setenv("GH_ACCESSIBLE_PROMPTER", "true") + // `echo`` is chose as the editor command because it immediately returns + // a success exit code, returns an empty string, doesn't require any user input, + // and since this file is only built on Linux, it is near guaranteed to be available. return prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) } From 9eee77a2bf290558d65c6111b5c9ed53bd5648fe Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:55:24 -0600 Subject: [PATCH 102/116] test(prompter): doc how accessible prompter tests work --- internal/prompter/accessible_prompter_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index 6d2bee92c20..60ae5162b0d 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -17,6 +17,19 @@ import ( "github.com/stretchr/testify/require" ) +// The following tests are broadly testing the accessible prompter, and NOT asserting +// on the prompter's complete and exact output strings. +// +// These tests generally operate with this logic: +// - Wait for a particular substring (a portion of the prompt) to appear +// - Send input +// - Wait for another substring to appear or for control to return to the test +// - Assert that the input value was returned from the prompter function + +// In the future, expanding these tests to assert on the exact prompt strings +// would help build confidence in `huh` upgrades, but for now these tests +// are sufficient to ensure that the accessible prompter behaves roughly as expected +// but doesn't mandate that prompts always look exactly the same. func TestAccessiblePrompter(t *testing.T) { t.Run("Select", func(t *testing.T) { console := newTestVirtualTerminal(t) From 20ff409bfcf0d7316fd05ab3d06a970a434cdbd0 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:56:12 -0600 Subject: [PATCH 103/116] fix(prompter): remove needless default value assignment --- internal/prompter/prompter.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 6cdbc9a8726..7dd45bf5c2d 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -136,10 +136,6 @@ func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) ) err := form.Run() - - if result == "" { - return defaultValue, nil - } return result, err } From b8cd094ca8f7a3cf3ab77a1e69f94eaae6403626 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Thu, 10 Apr 2025 18:11:38 -0400 Subject: [PATCH 104/116] Ensure markdown confirm prompt shows editor name Apparently, `gh` might not actually have an editor at the time we're prompting the user if they want to use it for markdown editing. In the survey package, there is a function that will handle fallback to the default editor based on environment variables and parse it in the case the editor contains flags and arguments for cases like Visual Studio Code. Additionally, there are no tests for the EditorName function and the fact it is loaded via `init` makes this difficult to test. Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- internal/prompter/prompter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 7dd45bf5c2d..cd53d719973 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -234,9 +234,9 @@ func (p *accessiblePrompter) InputHostname() (string, error) { func (p *accessiblePrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { var result string skipOption := "skip" - openOption := "open" + launchOption := "launch" options := []huh.Option[string]{ - huh.NewOption(fmt.Sprintf("Open Editor: %s", p.editorCmd), openOption), + huh.NewOption(fmt.Sprintf("Launch %s", surveyext.EditorName(p.editorCmd)), launchOption), } if blankAllowed { options = append(options, huh.NewOption("Skip", skipOption)) From 8cd39923fe273a059858b6e38f45cb4ba7d6a125 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:09:22 -0600 Subject: [PATCH 105/116] test(prompter): fix race condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This test was trying to block on `expect`’ing a string at the same time the prompt was completed. This doesn't need to happen for this test. It should just check for the output from the Input prompt invocation. --- internal/prompter/accessible_prompter_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index 60ae5162b0d..56096972d12 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -108,10 +108,6 @@ func TestAccessiblePrompter(t *testing.T) { // Enter nothing _, err = console.SendLine("") require.NoError(t, err) - - // Expect the default value to be returned - _, err = console.ExpectString(dummyDefaultValue) - require.NoError(t, err) }() inputValue, err := p.Input("Enter some characters", dummyDefaultValue) From 70537de13260d714e5a0ef51c9525daa8fe1a217 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:18:56 -0600 Subject: [PATCH 106/116] test(prompter): fix invalid comment --- internal/prompter/prompter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index cd53d719973..6ef61cf1583 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -259,7 +259,7 @@ func (p *accessiblePrompter) MarkdownEditor(prompt, defaultValue string, blankAl return "", nil } - // openOption was selected + // launchOption was selected text, err := surveyext.Edit(p.editorCmd, "*.md", defaultValue, p.stdin, p.stdout, p.stderr) if err != nil { return "", err From 0251a8dd6df25729c91eee8e6efc5a836f7fca79 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 11 Apr 2025 09:56:17 +0100 Subject: [PATCH 107/116] Explain why step logs are preferred Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 718d5b902f0..60e1670248f 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -677,6 +677,10 @@ func attachRunLog(rlz *zip.Reader, jobs []shared.Job) { func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error { for _, job := range jobs { + // To display a run log, we first try to compile it from individual step + // logs, because this way we can prepend lines with the corresponding + // step name. However, it's possible that we don't have the step logs, + // in which case we fall back to print the entire job run log. var hasStepLogs bool steps := job.Steps From f673b409f748957c345f655e4eb499d859d8adfe Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 11 Apr 2025 09:57:13 +0100 Subject: [PATCH 108/116] Replace `UNKNOWN` with `UNKNOWN STEP` in job run log Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 2 +- pkg/cmd/run/view/view_test.go | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 60e1670248f..1e60f535e15 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -711,7 +711,7 @@ func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error { continue } - prefix := fmt.Sprintf("%s\tUNKNOWN\t", job.Name) + prefix := fmt.Sprintf("%s\tUNKNOWN STEP\t", job.Name) if err := printZIPFile(w, job.Log, prefix); err != nil { return err } diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index b5d619cd6c7..2205a9031a8 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -2298,27 +2298,27 @@ sad job quux the barf log line 3 `) var coolJobRunWithNoStepLogsLogOutput = heredoc.Doc(` -cool job with no step logs UNKNOWN log line 1 -cool job with no step logs UNKNOWN log line 2 -cool job with no step logs UNKNOWN log line 3 +cool job with no step logs UNKNOWN STEP log line 1 +cool job with no step logs UNKNOWN STEP log line 2 +cool job with no step logs UNKNOWN STEP log line 3 `) var legacyCoolJobRunWithNoStepLogsLogOutput = heredoc.Doc(` -legacy cool job with no step logs UNKNOWN log line 1 -legacy cool job with no step logs UNKNOWN log line 2 -legacy cool job with no step logs UNKNOWN log line 3 +legacy cool job with no step logs UNKNOWN STEP log line 1 +legacy cool job with no step logs UNKNOWN STEP log line 2 +legacy cool job with no step logs UNKNOWN STEP log line 3 `) var sadJobRunWithNoStepLogsLogOutput = heredoc.Doc(` -sad job with no step logs UNKNOWN log line 1 -sad job with no step logs UNKNOWN log line 2 -sad job with no step logs UNKNOWN log line 3 +sad job with no step logs UNKNOWN STEP log line 1 +sad job with no step logs UNKNOWN STEP log line 2 +sad job with no step logs UNKNOWN STEP log line 3 `) var legacySadJobRunWithNoStepLogsLogOutput = heredoc.Doc(` -legacy sad job with no step logs UNKNOWN log line 1 -legacy sad job with no step logs UNKNOWN log line 2 -legacy sad job with no step logs UNKNOWN log line 3 +legacy sad job with no step logs UNKNOWN STEP log line 1 +legacy sad job with no step logs UNKNOWN STEP log line 2 +legacy sad job with no step logs UNKNOWN STEP log line 3 `) var coolJobRunLogOutput = fmt.Sprintf("%s%s", fobTheBarzLogOutput, barfTheFobLogOutput) From 1bf1153c548fb0662be0835192b57676825c9c63 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 11 Apr 2025 10:07:31 +0100 Subject: [PATCH 109/116] Explain the `UNKNWON STEP` placeholder Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 1e60f535e15..cec1d0376f4 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -711,6 +711,11 @@ func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error { continue } + // Here, we fall back to the job run log, which means we do not know + // the step name of lines. However, we want to keep the same line + // formatting to avoid breaking any code or script that rely on the + // tab-delimited formatting. So, an unknown-step placeholder is used + // instead of the actual step name. prefix := fmt.Sprintf("%s\tUNKNOWN STEP\t", job.Name) if err := printZIPFile(w, job.Log, prefix); err != nil { return err From d35236948cfd3a868e9ee0c1a5016b4cc979ac84 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 11 Apr 2025 10:17:52 +0100 Subject: [PATCH 110/116] Improve explanation for missing step logs Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index cec1d0376f4..ee8330a5614 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -679,7 +679,8 @@ func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error { for _, job := range jobs { // To display a run log, we first try to compile it from individual step // logs, because this way we can prepend lines with the corresponding - // step name. However, it's possible that we don't have the step logs, + // step name. However, at the time of writing, logs are sometimes being + // served by a service that doesn’t include the step logs (none of them), // in which case we fall back to print the entire job run log. var hasStepLogs bool From 5adf3285ec24415046d970b518de1250cfb28f1a Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 11 Apr 2025 10:26:14 +0100 Subject: [PATCH 111/116] Explain when a negative number prefix appears Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index ee8330a5614..dfde7efe168 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -653,6 +653,9 @@ func truncateAsUTF16(str string, max int) string { // the prefixed number is either: // - An ordinal and cannot be mapped to the corresponding job's ID. // - A negative integer which is the ID of the job in the old Actions service. +// The service right now tries to get logs and use an ordinal in a loop. +// However, if it doesn't get the logs, it falls back to an old service +// where the ID can apparently be negative. func attachRunLog(rlz *zip.Reader, jobs []shared.Job) { for i, job := range jobs { re := jobLogFilenameRegexp(job) From 940bd10a1d9cd3d2ba25d7491f81355d5b0cdb70 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 11 Apr 2025 12:39:09 +0100 Subject: [PATCH 112/116] Prefer normal job run log file over legacy one Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index d46c2c9a9e1..2922fb60230 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -551,9 +551,19 @@ func getJobNameForLogFilename(name string) string { return sanitizedJobName } +// A job run log file is a top-level .txt file whose name starts with an ordinal +// number; e.g., "0_jobname.txt". func jobLogFilenameRegexp(job shared.Job) *regexp.Regexp { sanitizedJobName := getJobNameForLogFilename(job.Name) - re := fmt.Sprintf(`^-?\d+_%s\.txt`, regexp.QuoteMeta(sanitizedJobName)) + re := fmt.Sprintf(`^\d+_%s\.txt`, regexp.QuoteMeta(sanitizedJobName)) + return regexp.MustCompile(re) +} + +// A legacy job run log file is a top-level .txt file whose name starts with a +// negative number which is the ID of the run; e.g., "-2147483648_jobname.txt". +func legacyJobLogFilenameRegexp(job shared.Job) *regexp.Regexp { + sanitizedJobName := getJobNameForLogFilename(job.Name) + re := fmt.Sprintf(`^-\d+_%s\.txt`, regexp.QuoteMeta(sanitizedJobName)) return regexp.MustCompile(re) } @@ -658,24 +668,28 @@ func truncateAsUTF16(str string, max int) string { // where the ID can apparently be negative. func attachRunLog(rlz *zip.Reader, jobs []shared.Job) { for i, job := range jobs { - re := jobLogFilenameRegexp(job) - for _, file := range rlz.File { - if re.MatchString(file.Name) { - jobs[i].Log = file - break - } + // the normal job run log file is preferred over the legacy one. So, we + // try to find the normal log file, and if we couldn't find it then we + // look for the legacy one, if any. + jobLog := matchFileInZIPArchive(rlz, jobLogFilenameRegexp(job)) + if jobLog == nil { + jobLog = matchFileInZIPArchive(rlz, legacyJobLogFilenameRegexp(job)) } + jobs[i].Log = jobLog for j, step := range job.Steps { - re := stepLogFilenameRegexp(job, step) - for _, file := range rlz.File { - if re.MatchString(file.Name) { - jobs[i].Steps[j].Log = file - break - } - } + jobs[i].Steps[j].Log = matchFileInZIPArchive(rlz, stepLogFilenameRegexp(job, step)) + } + } +} + +func matchFileInZIPArchive(zr *zip.Reader, re *regexp.Regexp) *zip.File { + for _, file := range zr.File { + if re.MatchString(file.Name) { + return file } } + return nil } func displayRunLog(w io.Writer, jobs []shared.Job, failed bool) error { From 2d21b4624ca71087aa393de91c5c51652b711645 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 11 Apr 2025 12:40:26 +0100 Subject: [PATCH 113/116] Test normal job run log is preferred over legacy one Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/fixtures/run_log.zip | Bin 8148 -> 8646 bytes pkg/cmd/run/view/view_test.go | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/run/view/fixtures/run_log.zip b/pkg/cmd/run/view/fixtures/run_log.zip index 60701d9254cbcacdda1d9aff426de744b6d574d7..425ba09ddce377dc26bffb4eaf7cdc2c99bc92ab 100644 GIT binary patch delta 408 zcmca&f6RG9i{RuvEF3 Date: Fri, 11 Apr 2025 12:41:31 +0100 Subject: [PATCH 114/116] Add `$` anchor to log file regexps Signed-off-by: Babak K. Shandiz This is to make sure we do not pick up the wrong file if there is a `.txt` sequence in the middle of a job name. --- pkg/cmd/run/view/view.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 2922fb60230..b769028bc37 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -555,7 +555,7 @@ func getJobNameForLogFilename(name string) string { // number; e.g., "0_jobname.txt". func jobLogFilenameRegexp(job shared.Job) *regexp.Regexp { sanitizedJobName := getJobNameForLogFilename(job.Name) - re := fmt.Sprintf(`^\d+_%s\.txt`, regexp.QuoteMeta(sanitizedJobName)) + re := fmt.Sprintf(`^\d+_%s\.txt$`, regexp.QuoteMeta(sanitizedJobName)) return regexp.MustCompile(re) } @@ -563,13 +563,13 @@ func jobLogFilenameRegexp(job shared.Job) *regexp.Regexp { // negative number which is the ID of the run; e.g., "-2147483648_jobname.txt". func legacyJobLogFilenameRegexp(job shared.Job) *regexp.Regexp { sanitizedJobName := getJobNameForLogFilename(job.Name) - re := fmt.Sprintf(`^-\d+_%s\.txt`, regexp.QuoteMeta(sanitizedJobName)) + re := fmt.Sprintf(`^-\d+_%s\.txt$`, regexp.QuoteMeta(sanitizedJobName)) return regexp.MustCompile(re) } func stepLogFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp { sanitizedJobName := getJobNameForLogFilename(job.Name) - re := fmt.Sprintf(`^%s\/%d_.*\.txt`, regexp.QuoteMeta(sanitizedJobName), step.Number) + re := fmt.Sprintf(`^%s\/%d_.*\.txt$`, regexp.QuoteMeta(sanitizedJobName), step.Number) return regexp.MustCompile(re) } From 48274f3118f2357e536dfabf3ee5ca42535fdcc0 Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 11 Apr 2025 15:23:18 +0200 Subject: [PATCH 115/116] Document UNKNOWN STEP in run view --- pkg/cmd/run/view/view.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index d46c2c9a9e1..5b8a1da8798 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -118,6 +118,10 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman This command does not support authenticating via fine grained PATs as it is not currently possible to create a PAT with the %[1]schecks:read%[1]s permission. + + Due to platform limitations, %[1]sgh%[1]s may not always be able to associate log lines with a + particular step in a job. In this case, the step name in the log output will be replaced with + %[1]sUNKNOWN STEP%[1]s. `, "`"), Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` From f337ce90a07284634ab1db67cf823fcbc8f25799 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 11 Apr 2025 14:51:39 +0100 Subject: [PATCH 116/116] Explain job log resolution reason Signed-off-by: Babak K. Shandiz --- pkg/cmd/run/view/view.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index b769028bc37..55be7f5f4d9 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -668,9 +668,10 @@ func truncateAsUTF16(str string, max int) string { // where the ID can apparently be negative. func attachRunLog(rlz *zip.Reader, jobs []shared.Job) { for i, job := range jobs { - // the normal job run log file is preferred over the legacy one. So, we - // try to find the normal log file, and if we couldn't find it then we - // look for the legacy one, if any. + // As a highest priority, we try to use the step logs first. We have seen zips that surprisingly contain + // step logs, normal job logs and legacy job logs. In this case, both job logs would be ignored. We have + // never seen a zip containing both job logs and no step logs, however, it may be possible. In that case + // let's prioritise the normal log over the legacy one. jobLog := matchFileInZIPArchive(rlz, jobLogFilenameRegexp(job)) if jobLog == nil { jobLog = matchFileInZIPArchive(rlz, legacyJobLogFilenameRegexp(job))