From abd573ac66e8e4abd18cdc7abaef7deb60502fc6 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:40:03 -0600 Subject: [PATCH 01/80] feat(preview): add `preview prompter` command --- pkg/cmd/preview/preview.go | 20 +++ pkg/cmd/preview/prompter/prompter.go | 225 +++++++++++++++++++++++++++ pkg/cmd/root/root.go | 2 + 3 files changed, 247 insertions(+) create mode 100644 pkg/cmd/preview/preview.go create mode 100644 pkg/cmd/preview/prompter/prompter.go diff --git a/pkg/cmd/preview/preview.go b/pkg/cmd/preview/preview.go new file mode 100644 index 000000000..5c3209722 --- /dev/null +++ b/pkg/cmd/preview/preview.go @@ -0,0 +1,20 @@ +package preview + +import ( + cmdPrompter "github.com/cli/cli/v2/pkg/cmd/preview/prompter" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/spf13/cobra" +) + +func NewCmdPreview(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "preview ", + Short: "Execute previews for gh features", + } + + cmdutil.DisableAuthCheck(cmd) + + cmd.AddCommand(cmdPrompter.NewCmdPrompter(f, nil)) + + return cmd +} diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go new file mode 100644 index 000000000..97fffb23a --- /dev/null +++ b/pkg/cmd/preview/prompter/prompter.go @@ -0,0 +1,225 @@ +package prompter + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/prompter" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type prompterOptions struct { + IO *iostreams.IOStreams + Config func() (gh.Config, error) + + PromptsToRun []func(prompter.Prompter) error +} + +func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobra.Command { + opts := &prompterOptions{ + IO: f.IOStreams, + Config: f.Config, + } + + const ( + selectPrompt = "select" + multiSelectPrompt = "multi-select" + inputPrompt = "input" + passwordPrompt = "password" + confirmPrompt = "confirm" + authTokenPrompt = "auth-token" + confirmDeletionPrompt = "confirm-deletion" + inputHostnamePrompt = "input-hostname" + markdownEditorPrompt = "markdown-editor" + ) + + prompterTypeFuncMap := map[string]func(prompter.Prompter) error{ + selectPrompt: runSelect, + multiSelectPrompt: runMultiSelect, + inputPrompt: runInput, + passwordPrompt: runPassword, + confirmPrompt: runConfirm, + authTokenPrompt: runAuthToken, + confirmDeletionPrompt: runConfirmDeletion, + inputHostnamePrompt: runInputHostname, + markdownEditorPrompt: runMarkdownEditor, + } + + cmd := &cobra.Command{ + Use: "prompter [prompt type]", + Short: "Execute a test program to preview the prompter", + Long: heredoc.Doc(` + Execute a test program to preview the prompter. + Without an argument, all prompts will be run. + + Available prompt types: + - select + - multi-select + - input + - password + - confirm + - auth-token + - confirm-deletion + - input-hostname + - markdown-editor + `), + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if runF != nil { + return runF(opts) + } + + if len(args) == 0 { + // All prompts + for _, f := range prompterTypeFuncMap { + opts.PromptsToRun = append(opts.PromptsToRun, f) + } + } else { + // Only the one specified + for _, arg := range args { + f, ok := prompterTypeFuncMap[arg] + if !ok { + return fmt.Errorf("unknown prompter type: %q", arg) + } + opts.PromptsToRun = append(opts.PromptsToRun, f) + } + } + + return prompterRun(opts) + }, + } + + return cmd +} + +func prompterRun(opts *prompterOptions) error { + editor, err := cmdutil.DetermineEditor(opts.Config) + if err != nil { + return err + } + + p := prompter.New(editor, opts.IO.In, opts.IO.Out, opts.IO.ErrOut) + + for _, f := range opts.PromptsToRun { + if err := f(p); err != nil { + return err + } + // Newline for readability + fmt.Println() + } + + return nil +} + +func runSelect(p prompter.Prompter) error { + fmt.Println("Demonstrating Single Select") + cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} + favorite, err := p.Select("Favorite cuisine?", "Italian", cuisines) + if err != nil { + return err + } + fmt.Printf("Favorite cuisine: %s\n", cuisines[favorite]) + return nil +} + +func runMultiSelect(p prompter.Prompter) error { + fmt.Println("Demonstrating Multi Select") + cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} + favorites, err := p.MultiSelect("Favorite cuisines?", []string{}, cuisines) + if err != nil { + return err + } + for _, f := range favorites { + fmt.Printf("Favorite cuisine: %s\n", cuisines[f]) + } + return nil +} + +func runInput(p prompter.Prompter) error { + fmt.Println("Demonstrating Text Input") + text, err := p.Input("Favorite meal?", "Breakfast") + if err != nil { + return err + } + fmt.Printf("You typed: %s\n", text) + return nil +} + +func runPassword(p prompter.Prompter) error { + fmt.Println("Demonstrating Password Input") + safeword, err := p.Password("Safe word?") + if err != nil { + return err + } + fmt.Printf("Safe word: %s\n", safeword) + return nil +} + +func runConfirm(p prompter.Prompter) error { + fmt.Println("Demonstrating Confirmation") + confirmation, err := p.Confirm("Are you sure?", true) + if err != nil { + return err + } + fmt.Printf("Confirmation: %t\n", confirmation) + return nil +} + +func runAuthToken(p prompter.Prompter) error { + fmt.Println("Demonstrating Auth Token (can't be blank)") + token, err := p.AuthToken() + if err != nil { + return err + } + fmt.Printf("Auth token: %s\n", token) + return nil +} + +func runConfirmDeletion(p prompter.Prompter) error { + fmt.Println("Demonstrating Deletion Confirmation") + err := p.ConfirmDeletion("delete-me") + if err != nil { + return err + } + fmt.Println("Item deleted") + return nil +} + +func runInputHostname(p prompter.Prompter) error { + fmt.Println("Demonstrating Hostname") + hostname, err := p.InputHostname() + if err != nil { + return err + } + fmt.Printf("Hostname: %s\n", hostname) + return nil +} + +func runMarkdownEditor(p prompter.Prompter) error { + defaultText := "default text value" + + fmt.Println("Demonstrating Markdown Editor with blanks allowed and default text") + editorText, err := p.MarkdownEditor("Edit your text:", defaultText, true) + if err != nil { + return err + } + fmt.Printf("Returned text: %s\n", editorText) + + fmt.Println("Demonstrating Markdown Editor with blanks disallowed and default text") + editorText2, err := p.MarkdownEditor("Edit your text:", defaultText, false) + if err != nil { + return err + } + fmt.Printf("Returned text: %s\n", editorText2) + + fmt.Println("Demonstrating Markdown Editor with blanks disallowed and no default text") + editorText3, err := p.MarkdownEditor("Edit your text:", "", false) + if err != nil { + return err + } + fmt.Printf("Returned text: %s\n", editorText3) + return nil +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index c0dad93ec..ce142b32d 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -25,6 +25,7 @@ import ( labelCmd "github.com/cli/cli/v2/pkg/cmd/label" orgCmd "github.com/cli/cli/v2/pkg/cmd/org" prCmd "github.com/cli/cli/v2/pkg/cmd/pr" + previewCmd "github.com/cli/cli/v2/pkg/cmd/preview" projectCmd "github.com/cli/cli/v2/pkg/cmd/project" releaseCmd "github.com/cli/cli/v2/pkg/cmd/release" repoCmd "github.com/cli/cli/v2/pkg/cmd/repo" @@ -139,6 +140,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, cmd.AddCommand(statusCmd.NewCmdStatus(f, nil)) cmd.AddCommand(codespaceCmd.NewCmdCodespace(f)) cmd.AddCommand(projectCmd.NewCmdProject(f)) + cmd.AddCommand(previewCmd.NewCmdPreview(f)) // below here at the commands that require the "intelligent" BaseRepo resolver repoResolvingCmdFactory := *f From 54a929dcd9684dc11dc23b68f09e4ed04821faa1 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:49:30 -0600 Subject: [PATCH 02/80] feat(preview): enforce fixed order for prompts --- pkg/cmd/preview/prompter/prompter.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go index 97fffb23a..5b9b91f23 100644 --- a/pkg/cmd/preview/prompter/prompter.go +++ b/pkg/cmd/preview/prompter/prompter.go @@ -48,6 +48,18 @@ func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobr markdownEditorPrompt: runMarkdownEditor, } + allPromptsOrder := []string{ + selectPrompt, + multiSelectPrompt, + inputPrompt, + passwordPrompt, + confirmPrompt, + authTokenPrompt, + confirmDeletionPrompt, + inputHostnamePrompt, + markdownEditorPrompt, + } + cmd := &cobra.Command{ Use: "prompter [prompt type]", Short: "Execute a test program to preview the prompter", @@ -73,8 +85,9 @@ func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobr } if len(args) == 0 { - // All prompts - for _, f := range prompterTypeFuncMap { + // All prompts, in a fixed order + for _, promptType := range allPromptsOrder { + f := prompterTypeFuncMap[promptType] opts.PromptsToRun = append(opts.PromptsToRun, f) } } else { @@ -206,14 +219,14 @@ func runMarkdownEditor(p prompter.Prompter) error { if err != nil { return err } - fmt.Printf("Returned text: %s\n", editorText) + fmt.Printf("Returned text: %s\n\n", editorText) fmt.Println("Demonstrating Markdown Editor with blanks disallowed and default text") editorText2, err := p.MarkdownEditor("Edit your text:", defaultText, false) if err != nil { return err } - fmt.Printf("Returned text: %s\n", editorText2) + fmt.Printf("Returned text: %s\n\n", editorText2) fmt.Println("Demonstrating Markdown Editor with blanks disallowed and no default text") editorText3, err := p.MarkdownEditor("Edit your text:", "", false) From b63084cab57d551a909a9a74cd103b5e865281f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:47:00 +0000 Subject: [PATCH 03/80] chore(deps): bump github.com/gabriel-vasile/mimetype from 1.4.8 to 1.4.9 Bumps [github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype) from 1.4.8 to 1.4.9. - [Release notes](https://github.com/gabriel-vasile/mimetype/releases) - [Commits](https://github.com/gabriel-vasile/mimetype/compare/v1.4.8...v1.4.9) --- updated-dependencies: - dependency-name: github.com/gabriel-vasile/mimetype dependency-version: 1.4.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 31b07f2cf..435dd049d 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/creack/pty v1.1.24 github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 github.com/distribution/reference v0.6.0 - github.com/gabriel-vasile/mimetype v1.4.8 + github.com/gabriel-vasile/mimetype v1.4.9 github.com/gdamore/tcell/v2 v2.5.4 github.com/golang/snappy v0.0.4 github.com/google/go-cmp v0.7.0 @@ -182,7 +182,7 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.39.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-20250303144028-a0af3efb3deb // indirect diff --git a/go.sum b/go.sum index b312bcf6c..d0cdef6a6 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k= @@ -554,8 +554,8 @@ 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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 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= From 9bc2c388da010ccd279844b6af3384da27337c43 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 6 May 2025 14:34:05 -0600 Subject: [PATCH 04/80] fix(a11y prompter): input prompt default value is readable --- internal/prompter/accessible_prompter_test.go | 20 +++++++++++++++++++ internal/prompter/prompter.go | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index ed4da8de8..8a7c058c7 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -140,6 +140,26 @@ func TestAccessiblePrompter(t *testing.T) { assert.Equal(t, dummyDefaultValue, inputValue) }) + t.Run("Input - default value is in prompt and in readable format", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestAccessiblePrompter(t, console) + dummyDefaultValue := "12345abcdefg" + + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Enter some characters (default: 12345abcdefg)") + require.NoError(t, err) + + // Enter nothing + _, err = console.SendLine("") + 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) { console := newTestVirtualTerminal(t) p := newTestAccessiblePrompter(t, console) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 1e4f5592a..8300059ac 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -131,7 +131,11 @@ func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, optio func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) { result := defaultValue - prompt = fmt.Sprintf("%s (%s)", prompt, defaultValue) + if defaultValue != "" { + prompt = fmt.Sprintf("%s (default: %s)", prompt, defaultValue) + } else { + prompt = fmt.Sprintf("%s:", prompt) + } form := p.newForm( huh.NewGroup( huh.NewInput(). From 2ee68411a76a62ae703fb5d03fcc8e2995406204 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 6 May 2025 14:49:09 -0600 Subject: [PATCH 05/80] fix(a11y prompter): Select prompt respects defaults --- internal/prompter/accessible_prompter_test.go | 24 +++++++++++++++++++ internal/prompter/prompter.go | 7 +++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index 8a7c058c7..d4dde6358 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -5,6 +5,7 @@ package prompter_test import ( "fmt" "io" + "slices" "strings" "testing" "time" @@ -54,6 +55,29 @@ func TestAccessiblePrompter(t *testing.T) { assert.Equal(t, 0, selectValue) }) + t.Run("Select - blank input returns default value", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestAccessiblePrompter(t, console) + dummyDefaultValue := "12345abcdefg" + options := []string{"1", "2", dummyDefaultValue} + + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Input a number between 1 and 3:") + require.NoError(t, err) + + // Just press enter to accept the default + _, err = console.SendLine("") + require.NoError(t, err) + }() + + selectValue, err := p.Select("Select a number", dummyDefaultValue, options) + require.NoError(t, err) + + expectedIndex := slices.Index(options, dummyDefaultValue) + assert.Equal(t, expectedIndex, selectValue) + }) + t.Run("MultiSelect", func(t *testing.T) { console := newTestVirtualTerminal(t) p := newTestAccessiblePrompter(t, console) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 8300059ac..5c3b1238d 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -77,10 +77,15 @@ func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form { WithOutput(p.stdout) } -func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, error) { +func (p *accessiblePrompter) Select(prompt, defaultValue string, options []string) (int, error) { var result int formOptions := []huh.Option[int]{} for i, o := range options { + // If this option is the default value, assign its index + // to the result variable. huh will treat it as a default selection. + if defaultValue == o { + result = i + } formOptions = append(formOptions, huh.NewOption(o, i)) } From ab4cfb84d220b316dff439ce513adb69429ed460 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 6 May 2025 14:56:09 -0600 Subject: [PATCH 06/80] refactor(a11yprompter): shared method for prompt defaults --- internal/prompter/accessible_prompter_test.go | 2 +- internal/prompter/prompter.go | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index d4dde6358..8ae449dc9 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -171,7 +171,7 @@ func TestAccessiblePrompter(t *testing.T) { go func() { // Wait for prompt to appear - _, err := console.ExpectString("Enter some characters (default: 12345abcdefg)") + _, err := console.ExpectString("Enter some characters (default: 12345abcdefg):") require.NoError(t, err) // Enter nothing diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 5c3b1238d..4a6a4ff6d 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -77,6 +77,19 @@ func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form { WithOutput(p.stdout) } +// addDefaultsToPrompt adds default values to the prompt string. +func (p *accessiblePrompter) addDefaultsToPrompt(prompt string, defaultValues []string) string { + if len(defaultValues) == 1 { + prompt = fmt.Sprintf("%s (default: %s):", prompt, defaultValues[0]) + } else if len(defaultValues) > 1 { + prompt = fmt.Sprintf("%s (defaults: %s):", prompt, strings.Join(defaultValues, ", ")) + } else { + prompt = fmt.Sprintf("%s:", prompt) + } + + return prompt +} + func (p *accessiblePrompter) Select(prompt, defaultValue string, options []string) (int, error) { var result int formOptions := []huh.Option[int]{} @@ -136,11 +149,7 @@ func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, optio func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) { result := defaultValue - if defaultValue != "" { - prompt = fmt.Sprintf("%s (default: %s)", prompt, defaultValue) - } else { - prompt = fmt.Sprintf("%s:", prompt) - } + prompt = p.addDefaultsToPrompt(prompt, []string{defaultValue}) form := p.newForm( huh.NewGroup( huh.NewInput(). From cab906151fb142d1b000ee8be1fb3101b8087661 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 6 May 2025 15:32:30 -0600 Subject: [PATCH 07/80] fix(a11y prompter): select prompt default value is readable --- internal/prompter/accessible_prompter_test.go | 25 ++++++++++++++++++- internal/prompter/prompter.go | 12 ++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index 8ae449dc9..c47969c04 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -78,6 +78,29 @@ func TestAccessiblePrompter(t *testing.T) { assert.Equal(t, expectedIndex, selectValue) }) + t.Run("Select - default value is in prompt and in readable format", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestAccessiblePrompter(t, console) + dummyDefaultValue := "12345abcdefg" + options := []string{"1", "2", dummyDefaultValue} + + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Select a number (default: 12345abcdefg)") + require.NoError(t, err) + + // Just press enter to accept the default + _, err = console.SendLine("") + require.NoError(t, err) + }() + + selectValue, err := p.Select("Select a number", dummyDefaultValue, options) + require.NoError(t, err) + + expectedIndex := slices.Index(options, dummyDefaultValue) + assert.Equal(t, expectedIndex, selectValue) + }) + t.Run("MultiSelect", func(t *testing.T) { console := newTestVirtualTerminal(t) p := newTestAccessiblePrompter(t, console) @@ -171,7 +194,7 @@ func TestAccessiblePrompter(t *testing.T) { go func() { // Wait for prompt to appear - _, err := console.ExpectString("Enter some characters (default: 12345abcdefg):") + _, err := console.ExpectString("Enter some characters (default: 12345abcdefg)") require.NoError(t, err) // Enter nothing diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 4a6a4ff6d..c5bf04691 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -79,12 +79,15 @@ func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form { // addDefaultsToPrompt adds default values to the prompt string. func (p *accessiblePrompter) addDefaultsToPrompt(prompt string, defaultValues []string) string { + // We don't show empty default values in the prompt. + defaultValues = slices.DeleteFunc(defaultValues, func(s string) bool { + return s == "" + }) + if len(defaultValues) == 1 { - prompt = fmt.Sprintf("%s (default: %s):", prompt, defaultValues[0]) + prompt = fmt.Sprintf("%s (default: %s)", prompt, defaultValues[0]) } else if len(defaultValues) > 1 { - prompt = fmt.Sprintf("%s (defaults: %s):", prompt, strings.Join(defaultValues, ", ")) - } else { - prompt = fmt.Sprintf("%s:", prompt) + prompt = fmt.Sprintf("%s (defaults: %s)", prompt, strings.Join(defaultValues, ", ")) } return prompt @@ -93,6 +96,7 @@ func (p *accessiblePrompter) addDefaultsToPrompt(prompt string, defaultValues [] func (p *accessiblePrompter) Select(prompt, defaultValue string, options []string) (int, error) { var result int formOptions := []huh.Option[int]{} + prompt = p.addDefaultsToPrompt(prompt, []string{defaultValue}) for i, o := range options { // If this option is the default value, assign its index // to the result variable. huh will treat it as a default selection. From 957667efe6b069786584aed6034e24d543b0879d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 6 May 2025 15:33:51 -0600 Subject: [PATCH 08/80] doc(prompter): remove TODO about default value panic --- internal/prompter/prompter.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index c5bf04691..eb8e2e58f 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -126,7 +126,6 @@ func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, optio // If this option is in the defaults slice, // let's add its index to the result slice and huh // will treat it as a default selection. - // TODO: does an invalid default value constitute a panic? if slices.Contains(defaults, o) { result = append(result, i) } From 04aaaea142b831c76ddf4880829287ff60d0b761 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 6 May 2025 15:43:26 -0600 Subject: [PATCH 09/80] fix(a11y prompter): multi select defaults are readable --- internal/prompter/accessible_prompter_test.go | 31 +++++++++++++++++++ internal/prompter/prompter.go | 1 + 2 files changed, 32 insertions(+) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index c47969c04..c112c4708 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -147,6 +147,37 @@ func TestAccessiblePrompter(t *testing.T) { assert.Equal(t, []int{1}, multiSelectValue) }) + t.Run("MultiSelect - default value is in prompt and in readable format", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestAccessiblePrompter(t, console) + dummyDefaultValues := []string{"foo", "bar"} + options := []string{"1", "2"} + options = append(options, dummyDefaultValues...) + + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Select a number (defaults: foo, bar)") + require.NoError(t, err) + + // Don't select anything because the defaults should be selected. + + // This confirms selections + _, err = console.SendLine("0") + require.NoError(t, err) + }() + + multiSelectValues, err := p.MultiSelect("Select a number", dummyDefaultValues, options) + require.NoError(t, err) + var expectedIndices []int + + // Get the indices of the default values within the options slice + // as that's what we expect the prompter to return when no selections are made. + for _, defaultValue := range dummyDefaultValues { + expectedIndices = append(expectedIndices, slices.Index(options, defaultValue)) + } + assert.Equal(t, expectedIndices, multiSelectValues) + }) + t.Run("Input", func(t *testing.T) { console := newTestVirtualTerminal(t) p := newTestAccessiblePrompter(t, console) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index eb8e2e58f..f7a98cf98 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -121,6 +121,7 @@ func (p *accessiblePrompter) Select(prompt, defaultValue string, options []strin func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { var result []int + prompt = p.addDefaultsToPrompt(prompt, defaults) formOptions := make([]huh.Option[int], len(options)) for i, o := range options { // If this option is in the defaults slice, From 3594f6357b8db93c8cadf9d3c1726fa98d7c8ff6 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 6 May 2025 15:50:57 -0600 Subject: [PATCH 10/80] fix(a11y prompter): confirm prompt default is readable --- internal/prompter/accessible_prompter_test.go | 20 +++++++++++++++++++ internal/prompter/prompter.go | 5 +++++ 2 files changed, 25 insertions(+) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index c112c4708..32a412646 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -304,6 +304,26 @@ func TestAccessiblePrompter(t *testing.T) { require.Equal(t, false, confirmValue) }) + t.Run("Confirm - default value is in prompt and in readable format", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestAccessiblePrompter(t, console) + defaultValue := true + + go func() { + // Wait for prompt to appear + _, err := console.ExpectString("Are you sure (default: yes)") + require.NoError(t, err) + + // Enter nothing + _, err = console.SendLine("") + require.NoError(t, err) + }() + + confirmValue, err := p.Confirm("Are you sure", defaultValue) + require.NoError(t, err) + require.Equal(t, defaultValue, confirmValue) + }) + t.Run("AuthToken", func(t *testing.T) { console := newTestVirtualTerminal(t) p := newTestAccessiblePrompter(t, console) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index f7a98cf98..f8bd04c35 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -189,6 +189,11 @@ func (p *accessiblePrompter) Password(prompt string) (string, error) { func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) { result := defaultValue + if defaultValue { + prompt = p.addDefaultsToPrompt(prompt, []string{"yes"}) + } else { + prompt = p.addDefaultsToPrompt(prompt, []string{"no"}) + } form := p.newForm( huh.NewGroup( huh.NewConfirm(). From 55cd3c34cddfbd2f53c59ec347d30d8f278c3708 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 7 May 2025 15:52:25 +0200 Subject: [PATCH 11/80] Feature detect v1 projects on pr edit --- pkg/cmd/pr/edit/edit.go | 5 +++ pkg/cmd/pr/edit/edit_test.go | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 3c8d73ad3..0428d13d6 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -6,6 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" @@ -19,6 +20,9 @@ import ( type EditOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams + // TODO projectsV1Deprecation + // Remove this detector since it is only used for test validation. + Detector fd.Detector Finder shared.PRFinder Surveyor Surveyor @@ -193,6 +197,7 @@ func editRun(opts *EditOptions) error { findOptions := shared.FindOptions{ Selector: opts.SelectorArg, Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "assignees", "labels", "projectCards", "projectItems", "milestone"}, + Detector: opts.Detector, } pr, repo, err := opts.Finder.Find(findOptions) if err != nil { diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 3c4882961..b09ee104e 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" @@ -696,3 +697,73 @@ func (s testSurveyor) EditFields(e *shared.Editable, _ string) error { func (t testEditorRetriever) Retrieve() (string, error) { return "vim", nil } + +// TODO projectsV1Deprecation +// Remove this test. +func TestProjectsV1Deprecation(t *testing.T) { + t.Run("when projects v1 is supported, is included in query", func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + reg := &httpmock.Registry{} + reg.Register( + httpmock.GraphQL(`projectCards`), + // Simulate a GraphQL error to early exit the test. + httpmock.StatusStringResponse(500, ""), + ) + + f := &cmdutil.Factory{ + IOStreams: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + } + + // Ignore the error because we have no way to really stub it without + // fully stubbing a GQL error structure in the request body. + _ = editRun(&EditOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + Detector: &fd.EnabledDetectorMock{}, + + Finder: shared.NewFinder(f), + + SelectorArg: "https://github.com/cli/cli/pull/123", + }) + + // Verify that our request contained projectCards + reg.Verify(t) + }) + + t.Run("when projects v1 is not supported, is not included in query", func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + reg := &httpmock.Registry{} + reg.Exclude(t, httpmock.GraphQL(`projectCards`)) + + f := &cmdutil.Factory{ + IOStreams: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + } + + // Ignore the error because we have no way to really stub it without + // fully stubbing a GQL error structure in the request body. + _ = editRun(&EditOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + Detector: &fd.DisabledDetectorMock{}, + + Finder: shared.NewFinder(f), + + SelectorArg: "https://github.com/cli/cli/pull/123", + }) + + // Verify that our request did not contain projectCards + reg.Verify(t) + }) +} From 8ebbd1d4bf0ad67c420d41bbe04fdf4ccb46fa15 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 7 May 2025 12:19:16 -0600 Subject: [PATCH 12/80] feat(fd): add ActorIsAssignable to IssueFeatures --- internal/featuredetection/feature_detection.go | 9 ++++++--- internal/featuredetection/feature_detection_test.go | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index fba317f58..5ff08a573 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -18,11 +18,13 @@ type Detector interface { } type IssueFeatures struct { - StateReason bool + StateReason bool + ActorIsAssignable bool } var allIssueFeatures = IssueFeatures{ - StateReason: true, + StateReason: true, + ActorIsAssignable: true, } type PullRequestFeatures struct { @@ -70,7 +72,8 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) { } features := IssueFeatures{ - StateReason: false, + StateReason: false, + ActorIsAssignable: false, } var featureDetection struct { diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go index f1152da2c..2c7d19071 100644 --- a/internal/featuredetection/feature_detection_test.go +++ b/internal/featuredetection/feature_detection_test.go @@ -23,7 +23,8 @@ func TestIssueFeatures(t *testing.T) { name: "github.com", hostname: "github.com", wantFeatures: IssueFeatures{ - StateReason: true, + StateReason: true, + ActorIsAssignable: true, }, wantErr: false, }, @@ -31,7 +32,8 @@ func TestIssueFeatures(t *testing.T) { name: "ghec data residency (ghe.com)", hostname: "stampname.ghe.com", wantFeatures: IssueFeatures{ - StateReason: true, + StateReason: true, + ActorIsAssignable: true, }, wantErr: false, }, @@ -42,7 +44,8 @@ func TestIssueFeatures(t *testing.T) { `query Issue_fields\b`: `{"data": {}}`, }, wantFeatures: IssueFeatures{ - StateReason: false, + StateReason: false, + ActorIsAssignable: false, }, wantErr: false, }, From d78980c668dfe98bb35cb17ec2ea92d4db580a82 Mon Sep 17 00:00:00 2001 From: Art Leo Date: Thu, 8 May 2025 20:00:43 +1000 Subject: [PATCH 13/80] Fix release download test http stubbing Co-authored-by: William Martin --- pkg/cmd/release/download/download_test.go | 192 ++++++++++++++++++---- 1 file changed, 158 insertions(+), 34 deletions(-) diff --git a/pkg/cmd/release/download/download_test.go b/pkg/cmd/release/download/download_test.go index 78709dd57..dab5c0a92 100644 --- a/pkg/cmd/release/download/download_test.go +++ b/pkg/cmd/release/download/download_test.go @@ -183,6 +183,7 @@ func Test_downloadRun(t *testing.T) { name string isTTY bool opts DownloadOptions + httpStubs func(*httpmock.Registry) wantErr string wantStdout string wantStderr string @@ -196,6 +197,24 @@ func Test_downloadRun(t *testing.T) { Destination: ".", Concurrency: 2, }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", + "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" + }`) + + reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`)) + reg.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`)) + reg.Register(httpmock.REST("GET", "assets/5678"), httpmock.StringResponse(`5678`)) + }, wantStdout: ``, wantStderr: ``, wantFiles: []string{ @@ -213,6 +232,23 @@ func Test_downloadRun(t *testing.T) { Destination: "tmp/assets", Concurrency: 2, }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", + "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" + }`) + + reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`)) + reg.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`)) + }, wantStdout: ``, wantStderr: ``, wantFiles: []string{ @@ -229,6 +265,20 @@ func Test_downloadRun(t *testing.T) { Destination: ".", Concurrency: 2, }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", + "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" + }`) + }, wantStdout: ``, wantStderr: ``, wantErr: "no assets match the file pattern", @@ -242,6 +292,30 @@ func Test_downloadRun(t *testing.T) { Destination: "tmp/packages", Concurrency: 2, }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", + "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" + }`) + + reg.Register( + httpmock.REST( + "GET", + "repos/OWNER/REPO/zipball/v1.2.3", + ), + httpmock.WithHeader( + httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=zipball.zip", + ), + ) + }, wantStdout: ``, wantStderr: ``, wantFiles: []string{ @@ -257,6 +331,30 @@ func Test_downloadRun(t *testing.T) { Destination: "tmp/packages", Concurrency: 2, }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", + "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" + }`) + + reg.Register( + httpmock.REST( + "GET", + "repos/OWNER/REPO/tarball/v1.2.3", + ), + httpmock.WithHeader( + httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz", + ), + ) + }, wantStdout: ``, wantStderr: ``, wantFiles: []string{ @@ -273,6 +371,30 @@ func Test_downloadRun(t *testing.T) { Concurrency: 2, ArchiveType: "tar.gz", }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", + "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" + }`) + + reg.Register( + httpmock.REST( + "GET", + "repos/OWNER/REPO/tarball/v1.2.3", + ), + httpmock.WithHeader( + httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz", + ), + ) + }, wantStdout: ``, wantStderr: ``, wantFiles: []string{ @@ -289,6 +411,22 @@ func Test_downloadRun(t *testing.T) { Concurrency: 2, FilePatterns: []string{"*windows-32bit.zip"}, }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", + "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" + }`) + + reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`)) + }, wantStdout: ``, wantStderr: ``, wantFiles: []string{ @@ -305,6 +443,22 @@ func Test_downloadRun(t *testing.T) { Concurrency: 2, FilePatterns: []string{"*windows-32bit.zip"}, }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", + "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" + }`) + + reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`)) + }, wantStdout: `1234`, wantStderr: ``, }, @@ -324,41 +478,11 @@ func Test_downloadRun(t *testing.T) { ios.SetStderrTTY(tt.isTTY) fakeHTTP := &httpmock.Registry{} - shared.StubFetchRelease(t, fakeHTTP, "OWNER", "REPO", tt.opts.TagName, `{ - "assets": [ - { "name": "windows-32bit.zip", "size": 12, - "url": "https://api.github.com/assets/1234" }, - { "name": "windows-64bit.zip", "size": 34, - "url": "https://api.github.com/assets/3456" }, - { "name": "linux.tgz", "size": 56, - "url": "https://api.github.com/assets/5678" } - ], - "tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3", - "zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3" - }`) - fakeHTTP.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`)) - fakeHTTP.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`)) - fakeHTTP.Register(httpmock.REST("GET", "assets/5678"), httpmock.StringResponse(`5678`)) + defer fakeHTTP.Verify(t) - fakeHTTP.Register( - httpmock.REST( - "GET", - "repos/OWNER/REPO/tarball/v1.2.3", - ), - httpmock.WithHeader( - httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz", - ), - ) - - fakeHTTP.Register( - httpmock.REST( - "GET", - "repos/OWNER/REPO/zipball/v1.2.3", - ), - httpmock.WithHeader( - httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=zipball.zip", - ), - ) + if tt.httpStubs != nil { + tt.httpStubs(fakeHTTP) + } tt.opts.IO = ios tt.opts.HttpClient = func() (*http.Client, error) { From 89bed45c55557e0952cd4aacc8c66247f84d756d Mon Sep 17 00:00:00 2001 From: Art Leo Date: Thu, 8 May 2025 20:12:26 +1000 Subject: [PATCH 14/80] Release download handles missing archive URLs Co-authored-by: William Martin --- pkg/cmd/release/download/download.go | 16 +++++- pkg/cmd/release/download/download_test.go | 60 +++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/release/download/download.go b/pkg/cmd/release/download/download.go index cdf6135b6..b90721412 100644 --- a/pkg/cmd/release/download/download.go +++ b/pkg/cmd/release/download/download.go @@ -165,10 +165,24 @@ func downloadRun(opts *DownloadOptions) error { var toDownload []shared.ReleaseAsset isArchive := false if opts.ArchiveType != "" { - var archiveURL = release.ZipballURL + var archiveURL string if opts.ArchiveType == "tar.gz" { archiveURL = release.TarballURL + } else { + archiveURL = release.ZipballURL } + + if archiveURL == "" { + errMessage := fmt.Sprintf( + "release %q with tag %q, does not have a %q archive asset.", + release.Name, release.TagName, opts.ArchiveType, + ) + if release.IsDraft { + errMessage += " Most likely, this is because it is a draft." + } + return errors.New(errMessage) + } + // create pseudo-Asset with no name and pointing to ZipBallURL or TarBallURL toDownload = append(toDownload, shared.ReleaseAsset{APIURL: archiveURL}) isArchive = true diff --git a/pkg/cmd/release/download/download_test.go b/pkg/cmd/release/download/download_test.go index dab5c0a92..9337c9b65 100644 --- a/pkg/cmd/release/download/download_test.go +++ b/pkg/cmd/release/download/download_test.go @@ -462,6 +462,66 @@ func Test_downloadRun(t *testing.T) { wantStdout: `1234`, wantStderr: ``, }, + { + name: "draft release with null tarball_url and zipball_url", + isTTY: true, + opts: DownloadOptions{ + TagName: "v1.2.3", + ArchiveType: "tar.gz", + Destination: "tmp/packages", + Concurrency: 2, + }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "tag_name": "v1.2.3", + "name": "patch-36", + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": null, + "zipball_url": null, + "draft": true + }`) + }, + wantStdout: ``, + wantStderr: ``, + wantErr: "release \"patch-36\" with tag \"v1.2.3\", does not have a \"tar.gz\" archive asset. Most likely, this is because it is a draft.", + }, + { + name: "non-draft release with null tarball_url and zipball_url", + isTTY: true, + opts: DownloadOptions{ + TagName: "v1.2.3", + ArchiveType: "tar.gz", + Destination: "tmp/packages", + Concurrency: 2, + }, + httpStubs: func(reg *httpmock.Registry) { + shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ + "tag_name": "v1.2.3", + "name": "patch-36", + "assets": [ + { "name": "windows-32bit.zip", "size": 12, + "url": "https://api.github.com/assets/1234" }, + { "name": "windows-64bit.zip", "size": 34, + "url": "https://api.github.com/assets/3456" }, + { "name": "linux.tgz", "size": 56, + "url": "https://api.github.com/assets/5678" } + ], + "tarball_url": null, + "zipball_url": null, + "draft": false + }`) + }, + wantStdout: ``, + wantStderr: ``, + wantErr: "release \"patch-36\" with tag \"v1.2.3\", does not have a \"tar.gz\" archive asset.", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 9b89a0ac0e83d2191d54ea3810b4cc940d67732a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 8 May 2025 07:41:36 -0600 Subject: [PATCH 15/80] fix(a11y prompter): remove invalid defaults --- internal/prompter/accessible_prompter_test.go | 46 +++++++++++++++++++ internal/prompter/prompter.go | 21 ++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index 32a412646..b2a66141a 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -101,6 +101,27 @@ func TestAccessiblePrompter(t *testing.T) { assert.Equal(t, expectedIndex, selectValue) }) + t.Run("Select - invalid defaults are excluded from prompt", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestAccessiblePrompter(t, console) + dummyDefaultValue := "foo" + options := []string{"1", "2"} + + go func() { + // Wait for prompt to appear without the invalid default value + _, err := console.ExpectString("Select a number \r\n") + require.NoError(t, err) + + // Select option 2 + _, err = console.SendLine("2") + require.NoError(t, err) + }() + + selectValue, err := p.Select("Select a number", dummyDefaultValue, options) + require.NoError(t, err) + assert.Equal(t, 1, selectValue) + }) + t.Run("MultiSelect", func(t *testing.T) { console := newTestVirtualTerminal(t) p := newTestAccessiblePrompter(t, console) @@ -178,6 +199,31 @@ func TestAccessiblePrompter(t *testing.T) { assert.Equal(t, expectedIndices, multiSelectValues) }) + t.Run("MultiSelect - invalid defaults are excluded from prompt", func(t *testing.T) { + console := newTestVirtualTerminal(t) + p := newTestAccessiblePrompter(t, console) + dummyDefaultValues := []string{"foo", "bar"} + options := []string{"1", "2"} + + go func() { + // Wait for prompt to appear without the invalid default values + _, err := console.ExpectString("Select a number \r\n") + require.NoError(t, err) + + // Not selecting anything will fail because there are no defaults. + _, err = console.SendLine("2") + require.NoError(t, err) + + // This confirms selections + _, err = console.SendLine("0") + require.NoError(t, err) + }() + + multiSelectValues, err := p.MultiSelect("Select a number", dummyDefaultValues, options) + require.NoError(t, err) + assert.Equal(t, []int{1}, multiSelectValues) + }) + t.Run("Input", func(t *testing.T) { console := newTestVirtualTerminal(t) p := newTestAccessiblePrompter(t, console) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index f8bd04c35..c2233fd92 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -79,24 +79,32 @@ func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form { // addDefaultsToPrompt adds default values to the prompt string. func (p *accessiblePrompter) addDefaultsToPrompt(prompt string, defaultValues []string) string { - // We don't show empty default values in the prompt. + // Removing empty defaults from the slice. defaultValues = slices.DeleteFunc(defaultValues, func(s string) bool { return s == "" }) + // Pluralizing the prompt if there are multiple default values. if len(defaultValues) == 1 { prompt = fmt.Sprintf("%s (default: %s)", prompt, defaultValues[0]) } else if len(defaultValues) > 1 { prompt = fmt.Sprintf("%s (defaults: %s)", prompt, strings.Join(defaultValues, ", ")) } + // Zero-length defaultValues means return prompt unchanged. return prompt } func (p *accessiblePrompter) Select(prompt, defaultValue string, options []string) (int, error) { var result int - formOptions := []huh.Option[int]{} + + // Remove invalid default values from the defaults slice. + if !slices.Contains(options, defaultValue) { + defaultValue = "" + } + prompt = p.addDefaultsToPrompt(prompt, []string{defaultValue}) + formOptions := []huh.Option[int]{} for i, o := range options { // If this option is the default value, assign its index // to the result variable. huh will treat it as a default selection. @@ -121,6 +129,12 @@ func (p *accessiblePrompter) Select(prompt, defaultValue string, options []strin func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { var result []int + + // Remove invalid default values from the defaults slice. + defaults = slices.DeleteFunc(defaults, func(s string) bool { + return !slices.Contains(options, s) + }) + prompt = p.addDefaultsToPrompt(prompt, defaults) formOptions := make([]huh.Option[int], len(options)) for i, o := range options { @@ -189,11 +203,13 @@ func (p *accessiblePrompter) Password(prompt string) (string, error) { func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) { result := defaultValue + if defaultValue { prompt = p.addDefaultsToPrompt(prompt, []string{"yes"}) } else { prompt = p.addDefaultsToPrompt(prompt, []string{"no"}) } + form := p.newForm( huh.NewGroup( huh.NewConfirm(). @@ -201,6 +217,7 @@ func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, er Value(&result), ), ) + if err := form.Run(); err != nil { return false, err } From 90532e8377fca81a0a74e319b143ba37b3607ad9 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 8 May 2025 21:26:02 +0100 Subject: [PATCH 16/80] Improve assertion for disabled echo mode (#10927) * Improve assertion for disabled echo Signed-off-by: Babak K. Shandiz * Use `expect.RegExpPattern` Signed-off-by: Babak K. Shandiz --------- Signed-off-by: Babak K. Shandiz --- internal/prompter/accessible_prompter_test.go | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index ed4da8de8..9ba5d59b7 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -164,7 +164,12 @@ func TestAccessiblePrompter(t *testing.T) { // Ensure the dummy password is not printed to the screen, // asserting that echo mode is disabled. - _, err = console.ExpectString(" \r\n\r\n") + // + // Note that since console.ExpectString returns successful if the + // expected string matches any part of the stream, we have to use an + // anchored regexp (i.e., with ^ and $) to make sure the password/token + // is not printed at all. + _, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$")) require.NoError(t, err) }) @@ -230,7 +235,12 @@ func TestAccessiblePrompter(t *testing.T) { // Ensure the dummy password is not printed to the screen, // asserting that echo mode is disabled. - _, err = console.ExpectString(" \r\n\r\n") + // + // Note that since console.ExpectString returns successful if the + // expected string matches any part of the stream, we have to use an + // anchored regexp (i.e., with ^ and $) to make sure the password/token + // is not printed at all. + _, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$")) require.NoError(t, err) }) @@ -252,6 +262,10 @@ func TestAccessiblePrompter(t *testing.T) { _, err = console.ExpectString("token is required") require.NoError(t, err) + // Wait for the retry prompt + _, err = console.ExpectString("Paste your authentication token:") + require.NoError(t, err) + // Wait to ensure huh has time to set the echo mode time.Sleep(beforePasswordSendTimeout) @@ -266,7 +280,12 @@ func TestAccessiblePrompter(t *testing.T) { // Ensure the dummy password is not printed to the screen, // asserting that echo mode is disabled. - _, err = console.ExpectString(" \r\n\r\n") + // + // Note that since console.ExpectString returns successful if the + // expected string matches any part of the stream, we have to use an + // anchored regexp (i.e., with ^ and $) to make sure the password/token + // is not printed at all. + _, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$")) require.NoError(t, err) }) From 38e52db3779e9a6a199b6799283a452bcbe0802b Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 8 May 2025 11:06:34 -0600 Subject: [PATCH 17/80] feat(issue edit): fetch currently assigned actors --- api/queries_issue.go | 16 +++++++ api/queries_repo.go | 7 +++ pkg/cmd/issue/edit/edit.go | 41 +++++++++++++++--- pkg/cmd/issue/edit/edit_test.go | 75 +++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 7 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index f09360152..2b15a41b9 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -38,6 +38,7 @@ type Issue struct { Comments Comments Author Author Assignees Assignees + AssignedActors ActorAssignees Labels Labels ProjectCards ProjectCards ProjectItems ProjectItems @@ -91,6 +92,21 @@ func (a Assignees) Logins() []string { return logins } +type ActorAssignees struct { + Edges []struct { + Node Actor + } + TotalCount int +} + +func (a ActorAssignees) Logins() []string { + logins := make([]string, len(a.Edges)) + for i, a := range a.Edges { + logins[i] = a.Node.Login + } + return logins +} + type Labels struct { Nodes []IssueLabel TotalCount int diff --git a/api/queries_repo.go b/api/queries_repo.go index 93a32d80c..06ae48b12 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -146,6 +146,13 @@ type GitHubUser struct { Name string `json:"name"` } +// Actor is a superset of User and Bot +// At the time of writing, it does not support Name. +type Actor struct { + ID string `json:"id"` + Login string `json:"login"` +} + // BranchRef is the branch name in a GitHub repository type BranchRef struct { Name string `json:"name"` diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 8386cbcfa..a5694e6c9 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -197,9 +197,35 @@ func editRun(opts *EditOptions) error { } } + if opts.Detector == nil { + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) + } + + issueFeatures, err := opts.Detector.IssueFeatures() + if err != nil { + return err + } + lookupFields := []string{"id", "number", "title", "body", "url"} if editable.Assignees.Edited { - lookupFields = append(lookupFields, "assignees") + if issueFeatures.ActorIsAssignable { + // At the time of writing, only 10 Actors can be assigned to an issue. + assignedActors := heredoc.Doc(` + assignedActors(first: 10) { + edges { + node { + ... on Actor { + login + } + } + } + } + `) + lookupFields = append(lookupFields, assignedActors) + } else { + lookupFields = append(lookupFields, "assignees") + } } if editable.Labels.Edited { lookupFields = append(lookupFields, "labels") @@ -207,11 +233,6 @@ func editRun(opts *EditOptions) error { if editable.Projects.Edited { // TODO projectsV1Deprecation // Remove this section as we should no longer add projectCards - if opts.Detector == nil { - cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) - opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) - } - projectsV1Support := opts.Detector.ProjectsV1() if projectsV1Support == gh.ProjectsV1Supported { lookupFields = append(lookupFields, "projectCards") @@ -254,7 +275,13 @@ func editRun(opts *EditOptions) error { editable.Title.Default = issue.Title editable.Body.Default = issue.Body - editable.Assignees.Default = issue.Assignees.Logins() + // We use Actors as the default assignees if Actors are assignable + // on this GitHub host. + if issueFeatures.ActorIsAssignable { + editable.Assignees.Default = issue.AssignedActors.Logins() + } else { + editable.Assignees.Default = issue.Assignees.Logins() + } editable.Labels.Default = issue.Labels.Names() editable.Projects.Default = append(issue.ProjectCards.ProjectNames(), issue.ProjectItems.ProjectTitles()...) projectItems := map[string]string{} diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index c9aa4c409..d0115ab4e 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -875,3 +875,78 @@ func TestProjectsV1Deprecation(t *testing.T) { reg.Verify(t) }) } + +func TestActorIsAssignable(t *testing.T) { + t.Run("when actors are assignable, query includes assignedActors", func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + reg := &httpmock.Registry{} + reg.Register( + httpmock.GraphQL(`assignedActors`), + // Simulate a GraphQL error to early exit the test. + httpmock.StatusStringResponse(500, ""), + ) + + _, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + // Ignore the error because we don't care. + _ = editRun(&EditOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, + Detector: &fd.EnabledDetectorMock{}, + IssueNumbers: []int{123}, + Editable: prShared.Editable{ + Assignees: prShared.EditableSlice{ + Add: []string{"monalisa", "octocat"}, + Edited: true, + }, + }, + }) + + reg.Verify(t) + }) + + t.Run("when actors are not assignable, query includes assignees instead", func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + reg := &httpmock.Registry{} + // This test should NOT include assignedActors in the query + reg.Exclude(t, httpmock.GraphQL(`assignedActors`)) + // It should include the regular assignees field + reg.Register( + httpmock.GraphQL(`assignees`), + // Simulate a GraphQL error to early exit the test. + httpmock.StatusStringResponse(500, ""), + ) + + _, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + // Ignore the error because we're not really interested in it. + _ = editRun(&EditOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, + Detector: &fd.DisabledDetectorMock{}, + IssueNumbers: []int{123}, + Editable: prShared.Editable{ + Assignees: prShared.EditableSlice{ + Add: []string{"monalisa", "octocat"}, + Edited: true, + }, + }, + }) + + reg.Verify(t) + }) +} From ee9d16920425caf41dc9f1f1ca4bd7fc388acbd4 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 9 May 2025 13:37:15 -0600 Subject: [PATCH 18/80] feat(issue edit): fetch assignable actors --- api/queries_repo.go | 141 +++++++++++++++++++++++++++----- pkg/cmd/issue/edit/edit.go | 4 +- pkg/cmd/issue/edit/edit_test.go | 97 ++++++++++++++-------- pkg/cmd/pr/edit/edit_test.go | 36 ++++---- pkg/cmd/pr/shared/editable.go | 39 +++++++-- 5 files changed, 240 insertions(+), 77 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 06ae48b12..99041e49f 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -681,13 +681,14 @@ func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Reposit } type RepoMetadataResult struct { - CurrentLogin string - AssignableUsers []RepoAssignee - Labels []RepoLabel - Projects []RepoProject - ProjectsV2 []ProjectV2 - Milestones []RepoMilestone - Teams []OrgTeam + CurrentLogin string + AssignableUsers []RepoAssignee + AssignableActors []RepoAssignee + Labels []RepoLabel + Projects []RepoProject + ProjectsV2 []ProjectV2 + Milestones []RepoMilestone + Teams []OrgTeam } func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { @@ -701,6 +702,16 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { break } } + + // Look for ID in assignable actors if not found in assignable users + for _, u := range m.AssignableActors { + if strings.EqualFold(assigneeLogin, u.Login) { + ids = append(ids, u.ID) + found = true + break + } + } + if !found { return nil, fmt.Errorf("'%s' not found", assigneeLogin) } @@ -892,12 +903,13 @@ func (m *RepoMetadataResult) Merge(m2 *RepoMetadataResult) { } type RepoMetadataInput struct { - Assignees bool - Reviewers bool - Labels bool - ProjectsV1 bool - ProjectsV2 bool - Milestones bool + Assignees bool + ActorAssignees bool + Reviewers bool + Labels bool + ProjectsV1 bool + ProjectsV2 bool + Milestones bool } // RepoMetadata pre-fetches the metadata for attaching to issues and pull requests @@ -906,14 +918,48 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput var g errgroup.Group if input.Assignees || input.Reviewers { - g.Go(func() error { - users, err := RepoAssignableUsers(client, repo) - if err != nil { - err = fmt.Errorf("error fetching assignees: %w", err) + + if input.ActorAssignees { + g.Go(func() error { + actors, err := RepoAssignableActors(client, repo) + if err != nil { + err = fmt.Errorf("error fetching assignees: %w", err) + } + result.AssignableActors = actors + return err + }) + + // If reviewers are requested, we still need to fetch the assignable users + // since commands use assignable users for reviewers too, + // but Actors are not supported for requesting review (need to confirm this). + // TODO KW: find out how to do this in the above query so we don't need to + // run two potentially expensive queries. When we fetch Actors, this + // should still return Users - Users are distinguishable from other Actors + // by having a name property. Maybe we can use the Name to filter out + // non-user Actors and populate the users list for reviewers based on + // that. + if input.Reviewers { + g.Go(func() error { + users, err := RepoAssignableUsers(client, repo) + if err != nil { + err = fmt.Errorf("error fetching assignees: %w", err) + } + result.AssignableUsers = users + return err + }) } - result.AssignableUsers = users - return err - }) + } else { + // Not using Actors, fetch legacy assignable users. + g.Go(func() error { + users, err := RepoAssignableUsers(client, repo) + if err != nil { + err = fmt.Errorf("error fetching assignees: %w", err) + } + result.AssignableUsers = users + return err + }) + } + } if input.Reviewers { @@ -1186,6 +1232,61 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, return users, nil } +// RepoAssignableActors fetches all the assignable actors for a repository on +// GitHub hosts that support Actor assignees. +func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) { + type repoActorAssignee struct { + ID string + Login string + } + + type responseData struct { + Repository struct { + SuggestedActors struct { + Nodes []struct { + User RepoAssignee `graphql:"... on User"` + Bot repoActorAssignee `graphql:"... on Bot"` + } + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"suggestedActors(first: 100, after: $endCursor, capabilities: CAN_BE_ASSIGNED)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "endCursor": (*githubv4.String)(nil), + } + + var actors []RepoAssignee + for { + var query responseData + err := client.Query(repo.RepoHost(), "RepositoryAssignableActors", &query, variables) + if err != nil { + return nil, err + } + + for _, node := range query.Repository.SuggestedActors.Nodes { + // Edge case if the Actor is not a Bot or a User, + // it won't be unmarshalled properly, and we'll have an + // zero value node. + if node.User.ID == "" || node.User.Login == "" { + continue + } + actors = append(actors, node.User) + } + + if !query.Repository.SuggestedActors.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.SuggestedActors.PageInfo.EndCursor) + } + return actors, nil +} + type RepoLabel struct { ID string Name string diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index a5694e6c9..9177f60ad 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -210,6 +210,8 @@ func editRun(opts *EditOptions) error { lookupFields := []string{"id", "number", "title", "body", "url"} if editable.Assignees.Edited { if issueFeatures.ActorIsAssignable { + editable.Assignees.ActorAssignees = true + // At the time of writing, only 10 Actors can be assigned to an issue. assignedActors := heredoc.Doc(` assignedActors(first: 10) { @@ -277,7 +279,7 @@ func editRun(opts *EditOptions) error { editable.Body.Default = issue.Body // We use Actors as the default assignees if Actors are assignable // on this GitHub host. - if issueFeatures.ActorIsAssignable { + if editable.Assignees.ActorAssignees { editable.Assignees.Default = issue.AssignedActors.Logins() } else { editable.Assignees.Default = issue.Assignees.Logins() diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index d0115ab4e..01cd74900 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -118,9 +118,11 @@ func TestNewCmdEdit(t *testing.T) { output: EditOptions{ IssueNumbers: []int{23}, Editable: prShared.Editable{ - Assignees: prShared.EditableSlice{ - Add: []string{"monalisa", "hubot"}, - Edited: true, + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Edited: true, + }, }, }, }, @@ -132,9 +134,11 @@ func TestNewCmdEdit(t *testing.T) { output: EditOptions{ IssueNumbers: []int{23}, Editable: prShared.Editable{ - Assignees: prShared.EditableSlice{ - Remove: []string{"monalisa", "hubot"}, - Edited: true, + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Remove: []string{"monalisa", "hubot"}, + Edited: true, + }, }, }, }, @@ -354,10 +358,12 @@ func Test_editRun(t *testing.T) { Value: "new body", Edited: true, }, - Assignees: prShared.EditableSlice{ - Add: []string{"monalisa", "hubot"}, - Remove: []string{"octocat"}, - Edited: true, + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, }, Labels: prShared.EditableSlice{ Add: []string{"feature", "TODO", "bug"}, @@ -399,10 +405,12 @@ func Test_editRun(t *testing.T) { IssueNumbers: []int{456, 123}, Interactive: false, Editable: prShared.Editable{ - Assignees: prShared.EditableSlice{ - Add: []string{"monalisa", "hubot"}, - Remove: []string{"octocat"}, - Edited: true, + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, }, Labels: prShared.EditableSlice{ Add: []string{"feature", "TODO", "bug"}, @@ -449,10 +457,12 @@ func Test_editRun(t *testing.T) { IssueNumbers: []int{123, 9999}, Interactive: false, Editable: prShared.Editable{ - Assignees: prShared.EditableSlice{ - Add: []string{"monalisa", "hubot"}, - Remove: []string{"octocat"}, - Edited: true, + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, }, Labels: prShared.EditableSlice{ Add: []string{"feature", "TODO", "bug"}, @@ -494,10 +504,12 @@ func Test_editRun(t *testing.T) { IssueNumbers: []int{123, 456}, Interactive: false, Editable: prShared.Editable{ - Assignees: prShared.EditableSlice{ - Add: []string{"monalisa", "hubot"}, - Remove: []string{"octocat"}, - Edited: true, + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, }, Milestone: prShared.EditableString{ Value: "GA", @@ -509,14 +521,14 @@ func Test_editRun(t *testing.T) { httpStubs: func(t *testing.T, reg *httpmock.Registry) { // Should only be one fetch of metadata. reg.Register( - httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.GraphQL(`query RepositoryAssignableActors\b`), httpmock.StringResponse(` - { "data": { "repository": { "assignableUsers": { + { "data": { "repository": { "suggestedActors": { "nodes": [ { "login": "hubot", "id": "HUBOTID" }, { "login": "MonaLisa", "id": "MONAID" } ], - "pageInfo": { "hasNextPage": false } + "pageInfo": { "hasNextPage": false, "endCursor": "Mg" } } } } } `)) reg.Register( @@ -669,17 +681,30 @@ func mockIssueProjectItemsGet(_ *testing.T, reg *httpmock.Registry) { } func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) { + // reg.Register( + // httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + // httpmock.StringResponse(` + // { "data": { "repository": { "assignableUsers": { + // "nodes": [ + // { "login": "hubot", "id": "HUBOTID" }, + // { "login": "MonaLisa", "id": "MONAID" } + // ], + // "pageInfo": { "hasNextPage": false } + // } } } } + // `)) + reg.Register( - httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.GraphQL(`query RepositoryAssignableActors\b`), httpmock.StringResponse(` - { "data": { "repository": { "assignableUsers": { + { "data": { "repository": { "suggestedActors": { "nodes": [ { "login": "hubot", "id": "HUBOTID" }, { "login": "MonaLisa", "id": "MONAID" } ], - "pageInfo": { "hasNextPage": false } + "pageInfo": { "hasNextPage": false, "endCursor": "Mg" } } } } } `)) + reg.Register( httpmock.GraphQL(`query RepositoryLabelList\b`), httpmock.StringResponse(` @@ -902,9 +927,11 @@ func TestActorIsAssignable(t *testing.T) { Detector: &fd.EnabledDetectorMock{}, IssueNumbers: []int{123}, Editable: prShared.Editable{ - Assignees: prShared.EditableSlice{ - Add: []string{"monalisa", "octocat"}, - Edited: true, + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "octocat"}, + Edited: true, + }, }, }, }) @@ -940,9 +967,11 @@ func TestActorIsAssignable(t *testing.T) { Detector: &fd.DisabledDetectorMock{}, IssueNumbers: []int{123}, Editable: prShared.Editable{ - Assignees: prShared.EditableSlice{ - Add: []string{"monalisa", "octocat"}, - Edited: true, + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "octocat"}, + Edited: true, + }, }, }, }) diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 3c4882961..f45984c76 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -165,9 +165,11 @@ func TestNewCmdEdit(t *testing.T) { output: EditOptions{ SelectorArg: "23", Editable: shared.Editable{ - Assignees: shared.EditableSlice{ - Add: []string{"monalisa", "hubot"}, - Edited: true, + Assignees: shared.EditableAssignees{ + EditableSlice: shared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Edited: true, + }, }, }, }, @@ -179,9 +181,11 @@ func TestNewCmdEdit(t *testing.T) { output: EditOptions{ SelectorArg: "23", Editable: shared.Editable{ - Assignees: shared.EditableSlice{ - Remove: []string{"monalisa", "hubot"}, - Edited: true, + Assignees: shared.EditableAssignees{ + EditableSlice: shared.EditableSlice{ + Remove: []string{"monalisa", "hubot"}, + Edited: true, + }, }, }, }, @@ -359,10 +363,12 @@ func Test_editRun(t *testing.T) { Remove: []string{"dependabot"}, Edited: true, }, - Assignees: shared.EditableSlice{ - Add: []string{"monalisa", "hubot"}, - Remove: []string{"octocat"}, - Edited: true, + Assignees: shared.EditableAssignees{ + EditableSlice: shared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, }, Labels: shared.EditableSlice{ Add: []string{"feature", "TODO", "bug"}, @@ -413,10 +419,12 @@ func Test_editRun(t *testing.T) { Value: "base-branch-name", Edited: true, }, - Assignees: shared.EditableSlice{ - Add: []string{"monalisa", "hubot"}, - Remove: []string{"octocat"}, - Edited: true, + Assignees: shared.EditableAssignees{ + EditableSlice: shared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, }, Labels: shared.EditableSlice{ Add: []string{"feature", "TODO", "bug"}, diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index e73b3c294..238939efe 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -14,7 +14,7 @@ type Editable struct { Body EditableString Base EditableString Reviewers EditableSlice - Assignees EditableSlice + Assignees EditableAssignees Labels EditableSlice Projects EditableProjects Milestone EditableString @@ -38,6 +38,13 @@ type EditableSlice struct { Allowed bool } +// EditableAssignees is a special case of EditableSlice. +// It contains a flag to indicate whether the assignees are actors or not. +type EditableAssignees struct { + EditableSlice + ActorAssignees bool +} + // ProjectsV2 mutations require a mapping of an item ID to a project ID. // Keep that map along with standard EditableSlice data. type EditableProjects struct { @@ -245,6 +252,13 @@ func (es *EditableSlice) clone() EditableSlice { return cpy } +func (ea *EditableAssignees) clone() EditableAssignees { + return EditableAssignees{ + EditableSlice: ea.EditableSlice.clone(), + ActorAssignees: ea.ActorAssignees, + } +} + func (ep *EditableProjects) clone() EditableProjects { return EditableProjects{ EditableSlice: ep.EditableSlice.clone(), @@ -378,12 +392,13 @@ func FieldsToEditSurvey(p EditPrompter, editable *Editable) error { func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error { input := api.RepoMetadataInput{ - Reviewers: editable.Reviewers.Edited, - Assignees: editable.Assignees.Edited, - Labels: editable.Labels.Edited, - ProjectsV1: editable.Projects.Edited, - ProjectsV2: editable.Projects.Edited, - Milestones: editable.Milestone.Edited, + Reviewers: editable.Reviewers.Edited, + Assignees: editable.Assignees.Edited, + ActorAssignees: editable.Assignees.ActorAssignees, + Labels: editable.Labels.Edited, + ProjectsV1: editable.Projects.Edited, + ProjectsV2: editable.Projects.Edited, + Milestones: editable.Milestone.Edited, } metadata, err := api.RepoMetadata(client, repo, input) if err != nil { @@ -394,6 +409,10 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) for _, u := range metadata.AssignableUsers { users = append(users, u.Login) } + var actors []string + for _, a := range metadata.AssignableActors { + actors = append(actors, a.Login) + } var teams []string for _, t := range metadata.Teams { teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug)) @@ -416,7 +435,11 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) editable.Metadata = *metadata editable.Reviewers.Options = append(users, teams...) - editable.Assignees.Options = users + if editable.Assignees.ActorAssignees { + editable.Assignees.Options = actors + } else { + editable.Assignees.Options = users + } editable.Labels.Options = labels editable.Projects.Options = projects editable.Milestone.Options = milestones From 0efdfed0680c5afc8daf32da5c44928139684ca5 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 9 May 2025 22:56:09 -0600 Subject: [PATCH 19/80] feat(issue edit): support assigning actors to issues --- pkg/cmd/issue/edit/edit_test.go | 187 +++++++++++++++++------------ pkg/cmd/pr/shared/editable_http.go | 46 +++++++ 2 files changed, 154 insertions(+), 79 deletions(-) diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index 01cd74900..d27bbdee6 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -394,6 +394,7 @@ func Test_editRun(t *testing.T) { mockIssueProjectItemsGet(t, reg) mockRepoMetadata(t, reg) mockIssueUpdate(t, reg) + mockIssueUpdateActorAssignees(t, reg) mockIssueUpdateLabels(t, reg) mockProjectV2ItemUpdate(t, reg) }, @@ -441,6 +442,8 @@ func Test_editRun(t *testing.T) { mockIssueProjectItemsGet(t, reg) mockIssueUpdate(t, reg) mockIssueUpdate(t, reg) + mockIssueUpdateActorAssignees(t, reg) + mockIssueUpdateActorAssignees(t, reg) mockIssueUpdateLabels(t, reg) mockIssueUpdateLabels(t, reg) mockProjectV2ItemUpdate(t, reg) @@ -546,6 +549,14 @@ func Test_editRun(t *testing.T) { mockIssueNumberGet(t, reg, 123) mockIssueNumberGet(t, reg, 456) // Updating 123 should succeed. + reg.Register( + httpmock.GraphQLMutationMatcher(`mutation ReplaceActorsForAssignable\b`, func(m map[string]interface{}) bool { + return m["assignableId"] == "123" + }), + httpmock.GraphQLMutation(` + { "data": { "replaceActorsForAssignable": { "__typename": "" } } }`, + func(inputs map[string]interface{}) {}), + ) reg.Register( httpmock.GraphQLMutationMatcher(`mutation IssueUpdate\b`, func(m map[string]interface{}) bool { return m["id"] == "123" @@ -555,6 +566,14 @@ func Test_editRun(t *testing.T) { func(inputs map[string]interface{}) {}), ) // Updating 456 should fail. + reg.Register( + httpmock.GraphQLMutationMatcher(`mutation ReplaceActorsForAssignable\b`, func(m map[string]interface{}) bool { + return m["assignableId"] == "456" + }), + httpmock.GraphQLMutation(` + { "errors": [ { "message": "test error" } ] }`, + func(inputs map[string]interface{}) {}), + ) reg.Register( httpmock.GraphQLMutationMatcher(`mutation IssueUpdate\b`, func(m map[string]interface{}) bool { return m["id"] == "456" @@ -603,6 +622,7 @@ func Test_editRun(t *testing.T) { mockIssueProjectItemsGet(t, reg) mockRepoMetadata(t, reg) mockIssueUpdate(t, reg) + mockIssueUpdateActorAssignees(t, reg) mockIssueUpdateLabels(t, reg) mockProjectV2ItemUpdate(t, reg) }, @@ -792,6 +812,15 @@ func mockIssueUpdate(t *testing.T, reg *httpmock.Registry) { ) } +func mockIssueUpdateActorAssignees(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`), + httpmock.GraphQLMutation(` + { "data": { "replaceActorsForAssignable": { "__typename": "" } } }`, + func(inputs map[string]interface{}) {}), + ) +} + func mockIssueUpdateLabels(t *testing.T, reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`mutation LabelAdd\b`), @@ -816,6 +845,85 @@ func mockProjectV2ItemUpdate(t *testing.T, reg *httpmock.Registry) { ) } +func TestActorIsAssignable(t *testing.T) { + t.Run("when actors are assignable, query includes assignedActors", func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + reg := &httpmock.Registry{} + reg.Register( + httpmock.GraphQL(`assignedActors`), + // Simulate a GraphQL error to early exit the test. + httpmock.StatusStringResponse(500, ""), + ) + + _, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + // Ignore the error because we don't care. + _ = editRun(&EditOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, + Detector: &fd.EnabledDetectorMock{}, + IssueNumbers: []int{123}, + Editable: prShared.Editable{ + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "octocat"}, + Edited: true, + }, + }, + }, + }) + + reg.Verify(t) + }) + + t.Run("when actors are not assignable, query includes assignees instead", func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + reg := &httpmock.Registry{} + // This test should NOT include assignedActors in the query + reg.Exclude(t, httpmock.GraphQL(`assignedActors`)) + // It should include the regular assignees field + reg.Register( + httpmock.GraphQL(`assignees`), + // Simulate a GraphQL error to early exit the test. + httpmock.StatusStringResponse(500, ""), + ) + + _, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + // Ignore the error because we're not really interested in it. + _ = editRun(&EditOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, + Detector: &fd.DisabledDetectorMock{}, + IssueNumbers: []int{123}, + Editable: prShared.Editable{ + Assignees: prShared.EditableAssignees{ + EditableSlice: prShared.EditableSlice{ + Add: []string{"monalisa", "octocat"}, + Edited: true, + }, + }, + }, + }) + + reg.Verify(t) + }) +} + // TODO projectsV1Deprecation // Remove this test. func TestProjectsV1Deprecation(t *testing.T) { @@ -900,82 +1008,3 @@ func TestProjectsV1Deprecation(t *testing.T) { reg.Verify(t) }) } - -func TestActorIsAssignable(t *testing.T) { - t.Run("when actors are assignable, query includes assignedActors", func(t *testing.T) { - ios, _, _, _ := iostreams.Test() - - reg := &httpmock.Registry{} - reg.Register( - httpmock.GraphQL(`assignedActors`), - // Simulate a GraphQL error to early exit the test. - httpmock.StatusStringResponse(500, ""), - ) - - _, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - // Ignore the error because we don't care. - _ = editRun(&EditOptions{ - IO: ios, - HttpClient: func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil - }, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Detector: &fd.EnabledDetectorMock{}, - IssueNumbers: []int{123}, - Editable: prShared.Editable{ - Assignees: prShared.EditableAssignees{ - EditableSlice: prShared.EditableSlice{ - Add: []string{"monalisa", "octocat"}, - Edited: true, - }, - }, - }, - }) - - reg.Verify(t) - }) - - t.Run("when actors are not assignable, query includes assignees instead", func(t *testing.T) { - ios, _, _, _ := iostreams.Test() - - reg := &httpmock.Registry{} - // This test should NOT include assignedActors in the query - reg.Exclude(t, httpmock.GraphQL(`assignedActors`)) - // It should include the regular assignees field - reg.Register( - httpmock.GraphQL(`assignees`), - // Simulate a GraphQL error to early exit the test. - httpmock.StatusStringResponse(500, ""), - ) - - _, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - // Ignore the error because we're not really interested in it. - _ = editRun(&EditOptions{ - IO: ios, - HttpClient: func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil - }, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Detector: &fd.DisabledDetectorMock{}, - IssueNumbers: []int{123}, - Editable: prShared.Editable{ - Assignees: prShared.EditableAssignees{ - EditableSlice: prShared.EditableSlice{ - Add: []string{"monalisa", "octocat"}, - Edited: true, - }, - }, - }, - }) - - reg.Verify(t) - }) -} diff --git a/pkg/cmd/pr/shared/editable_http.go b/pkg/cmd/pr/shared/editable_http.go index fcc30095a..2c09f2e69 100644 --- a/pkg/cmd/pr/shared/editable_http.go +++ b/pkg/cmd/pr/shared/editable_http.go @@ -58,6 +58,26 @@ func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR }) } + // updateIssue mutation does not support Actors so assignment needs to + // be in a separate request when our assignees are Actors. + if options.Assignees.Edited && options.Assignees.ActorAssignees { + apiClient := api.NewClientFromHTTP(httpClient) + assigneeIds, err := options.AssigneeIds(apiClient, repo) + if err != nil { + return err + } + + // Disable the edited flag for assignees so that it doesn't + // trigger a second update in the replaceIssueFields function. + // It's important that this is done AFTER the call to + // options.AssigneeIds() above because otherwise that just exits. + options.Assignees.Edited = false + + wg.Go(func() error { + return replaceActorAssigneesForEditable(apiClient, repo, id, assigneeIds) + }) + } + if dirtyExcludingLabels(options) { wg.Go(func() error { return replaceIssueFields(httpClient, repo, id, isPR, options) @@ -67,6 +87,32 @@ func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR return wg.Wait() } +func replaceActorAssigneesForEditable(apiClient *api.Client, repo ghrepo.Interface, id string, assigneeIds *[]string) error { + type ReplaceActorsForAssignableInput struct { + AssignableID githubv4.ID `json:"assignableId"` + ActorIDs []githubv4.ID `json:"actorIds"` + } + + params := ReplaceActorsForAssignableInput{ + AssignableID: githubv4.ID(id), + ActorIDs: *ghIds(assigneeIds), + } + + var mutation struct { + ReplaceActorsForAssignable struct { + Typename string `graphql:"__typename"` + } `graphql:"replaceActorsForAssignable(input: $input)"` + } + + variables := map[string]interface{}{"input": params} + err := apiClient.Mutate(repo.RepoHost(), "ReplaceActorsForAssignable", &mutation, variables) + if err != nil { + return err + } + + return nil +} + func replaceIssueFields(httpClient *http.Client, repo ghrepo.Interface, id string, isPR bool, options Editable) error { apiClient := api.NewClientFromHTTP(httpClient) assigneeIds, err := options.AssigneeIds(apiClient, repo) From e0afc91fef1c3fbfe81d34c3054dbbd147e4b231 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 9 May 2025 23:02:15 -0600 Subject: [PATCH 20/80] chore(issue edit): comments cleanup --- api/queries_issue.go | 1 + pkg/cmd/issue/edit/edit_test.go | 14 +------------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index 2b15a41b9..833faaa47 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -99,6 +99,7 @@ type ActorAssignees struct { TotalCount int } +// TODO kw: Display names for actors with special display names. func (a ActorAssignees) Logins() []string { logins := make([]string, len(a.Edges)) for i, a := range a.Edges { diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index d27bbdee6..5a06614bc 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -701,18 +701,6 @@ func mockIssueProjectItemsGet(_ *testing.T, reg *httpmock.Registry) { } func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) { - // reg.Register( - // httpmock.GraphQL(`query RepositoryAssignableUsers\b`), - // httpmock.StringResponse(` - // { "data": { "repository": { "assignableUsers": { - // "nodes": [ - // { "login": "hubot", "id": "HUBOTID" }, - // { "login": "MonaLisa", "id": "MONAID" } - // ], - // "pageInfo": { "hasNextPage": false } - // } } } } - // `)) - reg.Register( httpmock.GraphQL(`query RepositoryAssignableActors\b`), httpmock.StringResponse(` @@ -721,7 +709,7 @@ func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) { { "login": "hubot", "id": "HUBOTID" }, { "login": "MonaLisa", "id": "MONAID" } ], - "pageInfo": { "hasNextPage": false, "endCursor": "Mg" } + "pageInfo": { "hasNextPage": false } } } } } `)) From 218152f7c5d454bffd019eb63e58645cd1f54639 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 9 May 2025 23:26:06 -0600 Subject: [PATCH 21/80] fix(issue edit): resolve race condition in actor assignment --- pkg/cmd/pr/shared/editable_http.go | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/pr/shared/editable_http.go b/pkg/cmd/pr/shared/editable_http.go index 2c09f2e69..1475f978c 100644 --- a/pkg/cmd/pr/shared/editable_http.go +++ b/pkg/cmd/pr/shared/editable_http.go @@ -61,19 +61,13 @@ func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR // updateIssue mutation does not support Actors so assignment needs to // be in a separate request when our assignees are Actors. if options.Assignees.Edited && options.Assignees.ActorAssignees { - apiClient := api.NewClientFromHTTP(httpClient) - assigneeIds, err := options.AssigneeIds(apiClient, repo) - if err != nil { - return err - } - - // Disable the edited flag for assignees so that it doesn't - // trigger a second update in the replaceIssueFields function. - // It's important that this is done AFTER the call to - // options.AssigneeIds() above because otherwise that just exits. - options.Assignees.Edited = false - wg.Go(func() error { + apiClient := api.NewClientFromHTTP(httpClient) + assigneeIds, err := options.AssigneeIds(apiClient, repo) + if err != nil { + return err + } + return replaceActorAssigneesForEditable(apiClient, repo, id, assigneeIds) }) } @@ -115,16 +109,20 @@ func replaceActorAssigneesForEditable(apiClient *api.Client, repo ghrepo.Interfa func replaceIssueFields(httpClient *http.Client, repo ghrepo.Interface, id string, isPR bool, options Editable) error { apiClient := api.NewClientFromHTTP(httpClient) - assigneeIds, err := options.AssigneeIds(apiClient, repo) - if err != nil { - return err - } projectIds, err := options.ProjectIds() if err != nil { return err } + var assigneeIds *[]string + if !options.Assignees.ActorAssignees { + assigneeIds, err = options.AssigneeIds(apiClient, repo) + if err != nil { + return err + } + } + milestoneId, err := options.MilestoneId() if err != nil { return err From 3af32fb07026c08b6c7ba4d48f818ab1365a9e3c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sat, 10 May 2025 10:46:35 -0600 Subject: [PATCH 22/80] fix(preview): remove needless newlines --- pkg/cmd/preview/prompter/prompter.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go index 5b9b91f23..e07ec61d8 100644 --- a/pkg/cmd/preview/prompter/prompter.go +++ b/pkg/cmd/preview/prompter/prompter.go @@ -120,8 +120,6 @@ func prompterRun(opts *prompterOptions) error { if err := f(p); err != nil { return err } - // Newline for readability - fmt.Println() } return nil From c4ab455ebfbcc23b1f7e8483b6bdeb17d52525ba Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sat, 10 May 2025 11:34:14 -0600 Subject: [PATCH 23/80] fix(prompter): update prompter create for changes in trunk --- pkg/cmd/preview/prompter/prompter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go index e07ec61d8..b63ac2d04 100644 --- a/pkg/cmd/preview/prompter/prompter.go +++ b/pkg/cmd/preview/prompter/prompter.go @@ -114,7 +114,7 @@ func prompterRun(opts *prompterOptions) error { return err } - p := prompter.New(editor, opts.IO.In, opts.IO.Out, opts.IO.ErrOut) + p := prompter.New(editor, opts.IO) for _, f := range opts.PromptsToRun { if err := f(p); err != nil { From f305cb13de20cfeb205735a2a1b315358505db1c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sat, 10 May 2025 11:43:44 -0600 Subject: [PATCH 24/80] fix(prompter): print to iostreams stdout --- pkg/cmd/preview/prompter/prompter.go | 68 ++++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go index b63ac2d04..5b44a5cbf 100644 --- a/pkg/cmd/preview/prompter/prompter.go +++ b/pkg/cmd/preview/prompter/prompter.go @@ -15,7 +15,7 @@ type prompterOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) - PromptsToRun []func(prompter.Prompter) error + PromptsToRun []func(prompter.Prompter, *iostreams.IOStreams) error } func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobra.Command { @@ -36,7 +36,7 @@ func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobr markdownEditorPrompt = "markdown-editor" ) - prompterTypeFuncMap := map[string]func(prompter.Prompter) error{ + prompterTypeFuncMap := map[string]func(prompter.Prompter, *iostreams.IOStreams) error{ selectPrompt: runSelect, multiSelectPrompt: runMultiSelect, inputPrompt: runInput, @@ -117,7 +117,7 @@ func prompterRun(opts *prompterOptions) error { p := prompter.New(editor, opts.IO) for _, f := range opts.PromptsToRun { - if err := f(p); err != nil { + if err := f(p, opts.IO); err != nil { return err } } @@ -125,112 +125,112 @@ func prompterRun(opts *prompterOptions) error { return nil } -func runSelect(p prompter.Prompter) error { - fmt.Println("Demonstrating Single Select") +func runSelect(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Single Select") cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} favorite, err := p.Select("Favorite cuisine?", "Italian", cuisines) if err != nil { return err } - fmt.Printf("Favorite cuisine: %s\n", cuisines[favorite]) + fmt.Fprintf(io.Out, "Favorite cuisine: %s\n", cuisines[favorite]) return nil } -func runMultiSelect(p prompter.Prompter) error { - fmt.Println("Demonstrating Multi Select") +func runMultiSelect(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Multi Select") cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} favorites, err := p.MultiSelect("Favorite cuisines?", []string{}, cuisines) if err != nil { return err } for _, f := range favorites { - fmt.Printf("Favorite cuisine: %s\n", cuisines[f]) + fmt.Fprintf(io.Out, "Favorite cuisine: %s\n", cuisines[f]) } return nil } -func runInput(p prompter.Prompter) error { - fmt.Println("Demonstrating Text Input") +func runInput(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Text Input") text, err := p.Input("Favorite meal?", "Breakfast") if err != nil { return err } - fmt.Printf("You typed: %s\n", text) + fmt.Fprintf(io.Out, "You typed: %s\n", text) return nil } -func runPassword(p prompter.Prompter) error { - fmt.Println("Demonstrating Password Input") +func runPassword(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Password Input") safeword, err := p.Password("Safe word?") if err != nil { return err } - fmt.Printf("Safe word: %s\n", safeword) + fmt.Fprintf(io.Out, "Safe word: %s\n", safeword) return nil } -func runConfirm(p prompter.Prompter) error { - fmt.Println("Demonstrating Confirmation") +func runConfirm(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Confirmation") confirmation, err := p.Confirm("Are you sure?", true) if err != nil { return err } - fmt.Printf("Confirmation: %t\n", confirmation) + fmt.Fprintf(io.Out, "Confirmation: %t\n", confirmation) return nil } -func runAuthToken(p prompter.Prompter) error { - fmt.Println("Demonstrating Auth Token (can't be blank)") +func runAuthToken(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Auth Token (can't be blank)") token, err := p.AuthToken() if err != nil { return err } - fmt.Printf("Auth token: %s\n", token) + fmt.Fprintf(io.Out, "Auth token: %s\n", token) return nil } -func runConfirmDeletion(p prompter.Prompter) error { - fmt.Println("Demonstrating Deletion Confirmation") +func runConfirmDeletion(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Deletion Confirmation") err := p.ConfirmDeletion("delete-me") if err != nil { return err } - fmt.Println("Item deleted") + fmt.Fprintln(io.Out, "Item deleted") return nil } -func runInputHostname(p prompter.Prompter) error { - fmt.Println("Demonstrating Hostname") +func runInputHostname(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Hostname") hostname, err := p.InputHostname() if err != nil { return err } - fmt.Printf("Hostname: %s\n", hostname) + fmt.Fprintf(io.Out, "Hostname: %s\n", hostname) return nil } -func runMarkdownEditor(p prompter.Prompter) error { +func runMarkdownEditor(p prompter.Prompter, io *iostreams.IOStreams) error { defaultText := "default text value" - fmt.Println("Demonstrating Markdown Editor with blanks allowed and default text") + fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks allowed and default text") editorText, err := p.MarkdownEditor("Edit your text:", defaultText, true) if err != nil { return err } - fmt.Printf("Returned text: %s\n\n", editorText) + fmt.Fprintf(io.Out, "Returned text: %s\n\n", editorText) - fmt.Println("Demonstrating Markdown Editor with blanks disallowed and default text") + fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks disallowed and default text") editorText2, err := p.MarkdownEditor("Edit your text:", defaultText, false) if err != nil { return err } - fmt.Printf("Returned text: %s\n\n", editorText2) + fmt.Fprintf(io.Out, "Returned text: %s\n\n", editorText2) - fmt.Println("Demonstrating Markdown Editor with blanks disallowed and no default text") + fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks disallowed and no default text") editorText3, err := p.MarkdownEditor("Edit your text:", "", false) if err != nil { return err } - fmt.Printf("Returned text: %s\n", editorText3) + fmt.Fprintf(io.Out, "Returned text: %s\n", editorText3) return nil } From 175767cc59db46e2ac235859af6c12a57248bf58 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sat, 10 May 2025 11:54:32 -0600 Subject: [PATCH 25/80] doc(preview): add long description --- pkg/cmd/preview/preview.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/cmd/preview/preview.go b/pkg/cmd/preview/preview.go index 5c3209722..791c100b6 100644 --- a/pkg/cmd/preview/preview.go +++ b/pkg/cmd/preview/preview.go @@ -1,6 +1,7 @@ package preview import ( + "github.com/MakeNowJust/heredoc" cmdPrompter "github.com/cli/cli/v2/pkg/cmd/preview/prompter" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" @@ -10,6 +11,10 @@ func NewCmdPreview(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "preview ", Short: "Execute previews for gh features", + Long: heredoc.Doc(` + Preview commands are for testing, demonstrative, and development purposes only. + They should be considered unstable and can change at any time. + `), } cmdutil.DisableAuthCheck(cmd) From 4985d0ff44e8f45caf1c77e0551077bfd4a9c893 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 12 May 2025 15:06:11 +0100 Subject: [PATCH 26/80] fix bug when removing all PR reviwers Signed-off-by: Babak K. Shandiz --- pkg/cmd/pr/edit/edit.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 0428d13d6..cc5eefb9e 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -283,8 +283,7 @@ func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, id if err != nil { return err } - if (userIds == nil || len(*userIds) == 0) && - (teamIds == nil || len(*teamIds) == 0) { + if userIds == nil && teamIds == nil { return nil } union := githubv4.Boolean(false) From ce710aa24928db04adb97b6d7b8e47a10092be32 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 12 May 2025 15:15:43 +0100 Subject: [PATCH 27/80] Add test to verify removing all reviewers Signed-off-by: Babak K. Shandiz --- pkg/cmd/pr/edit/edit_test.go | 88 +++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index b09ee104e..ed2231c5d 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -446,6 +446,64 @@ func Test_editRun(t *testing.T) { }, stdout: "https://github.com/OWNER/REPO/pull/123\n", }, + { + name: "non-interactive remove all reviewers", + input: &EditOptions{ + SelectorArg: "123", + Finder: shared.NewMockFinder("123", &api.PullRequest{ + URL: "https://github.com/OWNER/REPO/pull/123", + }, ghrepo.New("OWNER", "REPO")), + Interactive: false, + Editable: shared.Editable{ + Title: shared.EditableString{ + Value: "new title", + Edited: true, + }, + Body: shared.EditableString{ + Value: "new body", + Edited: true, + }, + Base: shared.EditableString{ + Value: "base-branch-name", + Edited: true, + }, + Reviewers: shared.EditableSlice{ + Remove: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot", "dependabot"}, + Edited: true, + }, + Assignees: shared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, + Labels: shared.EditableSlice{ + Add: []string{"feature", "TODO", "bug"}, + Remove: []string{"docs"}, + Edited: true, + }, + Projects: shared.EditableProjects{ + EditableSlice: shared.EditableSlice{ + Add: []string{"Cleanup", "CleanupV2"}, + Remove: []string{"Roadmap", "RoadmapV2"}, + Edited: true, + }, + }, + Milestone: shared.EditableString{ + Value: "GA", + Edited: true, + }, + }, + Fetcher: testFetcher{}, + }, + httpStubs: func(reg *httpmock.Registry) { + mockRepoMetadata(reg, false) + mockPullRequestUpdate(reg) + mockPullRequestReviewersUpdate(reg) + mockPullRequestUpdateLabels(reg) + mockProjectV2ItemUpdate(reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123\n", + }, { name: "interactive", input: &EditOptions{ @@ -487,6 +545,27 @@ func Test_editRun(t *testing.T) { }, stdout: "https://github.com/OWNER/REPO/pull/123\n", }, + { + name: "interactive remove all reviewers", + input: &EditOptions{ + SelectorArg: "123", + Finder: shared.NewMockFinder("123", &api.PullRequest{ + URL: "https://github.com/OWNER/REPO/pull/123", + }, ghrepo.New("OWNER", "REPO")), + Interactive: true, + Surveyor: testSurveyor{removeAllReviewers: true}, + Fetcher: testFetcher{}, + EditorRetriever: testEditorRetriever{}, + }, + httpStubs: func(reg *httpmock.Registry) { + mockRepoMetadata(reg, false) + mockPullRequestUpdate(reg) + mockPullRequestReviewersUpdate(reg) + mockPullRequestUpdateLabels(reg) + mockProjectV2ItemUpdate(reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123\n", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -658,7 +737,8 @@ func mockProjectV2ItemUpdate(reg *httpmock.Registry) { type testFetcher struct{} type testSurveyor struct { - skipReviewers bool + skipReviewers bool + removeAllReviewers bool } type testEditorRetriever struct{} @@ -683,7 +763,11 @@ func (s testSurveyor) EditFields(e *shared.Editable, _ string) error { e.Title.Value = "new title" e.Body.Value = "new body" if !s.skipReviewers { - e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"} + if s.removeAllReviewers { + e.Reviewers.Remove = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"} + } else { + e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"} + } } e.Assignees.Value = []string{"monalisa", "hubot"} e.Labels.Value = []string{"feature", "TODO", "bug"} From 29241cb7a51a8d672bfe00c706cb9ea09f84b6f4 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 12 May 2025 11:34:31 -0600 Subject: [PATCH 28/80] refactor(issue edit): improve actor type handling This improves actor type handling while fetching repository assignable actors. --- api/queries_repo.go | 37 +++++++++++++++++++++++---------- pkg/cmd/issue/edit/edit_test.go | 8 +++---- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 99041e49f..a5b926fc0 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -1235,17 +1235,25 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, // RepoAssignableActors fetches all the assignable actors for a repository on // GitHub hosts that support Actor assignees. func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) { - type repoActorAssignee struct { - ID string - Login string + type repoBotAssignee struct { + ID string + Login string + TypeName string `graphql:"__typename"` + } + + type repoUserAssignee struct { + ID string + Login string + Name string + TypeName string `graphql:"__typename"` } type responseData struct { Repository struct { SuggestedActors struct { Nodes []struct { - User RepoAssignee `graphql:"... on User"` - Bot repoActorAssignee `graphql:"... on Bot"` + User repoUserAssignee `graphql:"... on User"` + Bot repoBotAssignee `graphql:"... on Bot"` } PageInfo struct { HasNextPage bool @@ -1270,13 +1278,20 @@ func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]RepoAssignee } for _, node := range query.Repository.SuggestedActors.Nodes { - // Edge case if the Actor is not a Bot or a User, - // it won't be unmarshalled properly, and we'll have an - // zero value node. - if node.User.ID == "" || node.User.Login == "" { - continue + if node.User.TypeName == "User" { + actor := RepoAssignee{ + ID: node.User.ID, + Login: node.User.Login, + Name: node.User.Name, + } + actors = append(actors, actor) + } else if node.Bot.TypeName == "Bot" { + actor := RepoAssignee{ + ID: node.Bot.ID, + Login: node.Bot.Login, + } + actors = append(actors, actor) } - actors = append(actors, node.User) } if !query.Repository.SuggestedActors.PageInfo.HasNextPage { diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index 5a06614bc..bf2a1c417 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -528,8 +528,8 @@ func Test_editRun(t *testing.T) { httpmock.StringResponse(` { "data": { "repository": { "suggestedActors": { "nodes": [ - { "login": "hubot", "id": "HUBOTID" }, - { "login": "MonaLisa", "id": "MONAID" } + { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, + { "login": "MonaLisa", "id": "MONAID", "__typename": "User" } ], "pageInfo": { "hasNextPage": false, "endCursor": "Mg" } } } } } @@ -706,8 +706,8 @@ func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) { httpmock.StringResponse(` { "data": { "repository": { "suggestedActors": { "nodes": [ - { "login": "hubot", "id": "HUBOTID" }, - { "login": "MonaLisa", "id": "MONAID" } + { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, + { "login": "MonaLisa", "id": "MONAID", "__typename": "User" } ], "pageInfo": { "hasNextPage": false } } } } } From 73e5589059eebefedbc9116bae7adaad1c8f8768 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 12 May 2025 11:37:37 -0600 Subject: [PATCH 29/80] doc(issue): comment why assignable actors disabled --- internal/featuredetection/feature_detection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index 5ff08a573..a2f34a60b 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -73,7 +73,7 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) { features := IssueFeatures{ StateReason: false, - ActorIsAssignable: false, + ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES } var featureDetection struct { From cff2fa71e2b044befe0ea865b174cd08e9ab91d6 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 12 May 2025 11:49:44 -0600 Subject: [PATCH 30/80] doc(repo queries): clarify reviewer actor fetching logic --- api/queries_repo.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index a5b926fc0..6b9f690eb 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -929,7 +929,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput return err }) - // If reviewers are requested, we still need to fetch the assignable users + // If reviewers are also requested, we still need to fetch the assignable users // since commands use assignable users for reviewers too, // but Actors are not supported for requesting review (need to confirm this). // TODO KW: find out how to do this in the above query so we don't need to @@ -938,6 +938,9 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput // by having a name property. Maybe we can use the Name to filter out // non-user Actors and populate the users list for reviewers based on // that. + // Note: this only matters for `gh pr` flows, which currently does not + // request actor assignees, so we probably won't hit this until + // `gh pr` reqeuests actor assignees. if input.Reviewers { g.Go(func() error { users, err := RepoAssignableUsers(client, repo) From 5dca16fd4114fe79eae276fec9b2c244cd578753 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 13 May 2025 10:13:45 +0100 Subject: [PATCH 31/80] Update test case for removing all reviewers Signed-off-by: Babak K. Shandiz --- pkg/cmd/pr/edit/edit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index ed2231c5d..63e380486 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -764,7 +764,7 @@ func (s testSurveyor) EditFields(e *shared.Editable, _ string) error { e.Body.Value = "new body" if !s.skipReviewers { if s.removeAllReviewers { - e.Reviewers.Remove = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"} + e.Reviewers.Remove = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external", "dependabot"} } else { e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"} } From 5db8cf7c1db7487038647da2e57a75a460839694 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 13 May 2025 12:38:45 +0100 Subject: [PATCH 32/80] Increase `beforePasswordSendTimeout` to 100 us (#10977) Signed-off-by: Babak K. Shandiz --- internal/prompter/accessible_prompter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index ec19a43e0..2b8104e9a 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -34,7 +34,7 @@ import ( // but doesn't mandate that prompts always look exactly the same. func TestAccessiblePrompter(t *testing.T) { - beforePasswordSendTimeout := 20 * time.Microsecond + beforePasswordSendTimeout := 100 * time.Microsecond t.Run("Select", func(t *testing.T) { console := newTestVirtualTerminal(t) From 35792827ad3e0bd142bf91d2dc38fac9195fa526 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 13 May 2025 07:09:04 -0600 Subject: [PATCH 33/80] refactor(issue edit): rename AssignedActors to ActorAssignees --- api/queries_issue.go | 2 +- pkg/cmd/issue/edit/edit.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index 833faaa47..69e93c974 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -38,7 +38,7 @@ type Issue struct { Comments Comments Author Author Assignees Assignees - AssignedActors ActorAssignees + ActorAssignees ActorAssignees Labels Labels ProjectCards ProjectCards ProjectItems ProjectItems diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 9177f60ad..b86f20550 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -280,7 +280,7 @@ func editRun(opts *EditOptions) error { // We use Actors as the default assignees if Actors are assignable // on this GitHub host. if editable.Assignees.ActorAssignees { - editable.Assignees.Default = issue.AssignedActors.Logins() + editable.Assignees.Default = issue.ActorAssignees.Logins() } else { editable.Assignees.Default = issue.Assignees.Logins() } From 3bed77836a5ff2f95dab3d01f5c819d19c729bd3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 13 May 2025 07:11:24 -0600 Subject: [PATCH 34/80] refactor(issue edit): rename loop variable for clarity --- api/queries_repo.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 6b9f690eb..ea60319a3 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -704,9 +704,9 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { } // Look for ID in assignable actors if not found in assignable users - for _, u := range m.AssignableActors { - if strings.EqualFold(assigneeLogin, u.Login) { - ids = append(ids, u.ID) + for _, a := range m.AssignableActors { + if strings.EqualFold(assigneeLogin, a.Login) { + ids = append(ids, a.ID) found = true break } From 261297f0a2ccb0322c4dce8bb2fc99ae3e5c0c49 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 13 May 2025 07:28:42 -0600 Subject: [PATCH 35/80] refactor(issue edit): add assignedActors to lookupFields --- api/query_builder.go | 2 ++ pkg/cmd/issue/edit/edit.go | 15 +-------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/api/query_builder.go b/api/query_builder.go index 47fb4c225..0ef44a347 100644 --- a/api/query_builder.go +++ b/api/query_builder.go @@ -366,6 +366,8 @@ func IssueGraphQL(fields []string) string { q = append(q, `headRepository{id,name}`) case "assignees": q = append(q, `assignees(first:100){nodes{id,login,name},totalCount}`) + case "assignedActors": + q = append(q, `assignedActors(first: 10){edges{node{...on Actor{login}}},totalCount}`) case "labels": q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`) case "projectCards": diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index b86f20550..a2e824ebf 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -211,20 +211,7 @@ func editRun(opts *EditOptions) error { if editable.Assignees.Edited { if issueFeatures.ActorIsAssignable { editable.Assignees.ActorAssignees = true - - // At the time of writing, only 10 Actors can be assigned to an issue. - assignedActors := heredoc.Doc(` - assignedActors(first: 10) { - edges { - node { - ... on Actor { - login - } - } - } - } - `) - lookupFields = append(lookupFields, assignedActors) + lookupFields = append(lookupFields, `assignedActors`) } else { lookupFields = append(lookupFields, "assignees") } From 21bd797c6c31859d65edc37cdb6b0fa82bbb1f68 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 13 May 2025 11:45:40 -0600 Subject: [PATCH 36/80] fix(issue edit): use double quotes for assignedActors --- pkg/cmd/issue/edit/edit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index a2e824ebf..c9c76746b 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -211,7 +211,7 @@ func editRun(opts *EditOptions) error { if editable.Assignees.Edited { if issueFeatures.ActorIsAssignable { editable.Assignees.ActorAssignees = true - lookupFields = append(lookupFields, `assignedActors`) + lookupFields = append(lookupFields, "assignedActors") } else { lookupFields = append(lookupFields, "assignees") } From 1e5c3c7426144987f7dece2913c799ad7ca9a454 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 13 May 2025 13:02:20 -0600 Subject: [PATCH 37/80] fix(issue edit): revert rename of ActorAssignees --- api/queries_issue.go | 6 +++--- pkg/cmd/issue/edit/edit.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index 69e93c974..701b92039 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -38,7 +38,7 @@ type Issue struct { Comments Comments Author Author Assignees Assignees - ActorAssignees ActorAssignees + AssignedActors AssignedActors Labels Labels ProjectCards ProjectCards ProjectItems ProjectItems @@ -92,7 +92,7 @@ func (a Assignees) Logins() []string { return logins } -type ActorAssignees struct { +type AssignedActors struct { Edges []struct { Node Actor } @@ -100,7 +100,7 @@ type ActorAssignees struct { } // TODO kw: Display names for actors with special display names. -func (a ActorAssignees) Logins() []string { +func (a AssignedActors) Logins() []string { logins := make([]string, len(a.Edges)) for i, a := range a.Edges { logins[i] = a.Node.Login diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index c9c76746b..5cb789543 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -267,7 +267,7 @@ func editRun(opts *EditOptions) error { // We use Actors as the default assignees if Actors are assignable // on this GitHub host. if editable.Assignees.ActorAssignees { - editable.Assignees.Default = issue.ActorAssignees.Logins() + editable.Assignees.Default = issue.AssignedActors.Logins() } else { editable.Assignees.Default = issue.Assignees.Logins() } From dfd600713f001e3c1385ee9ea21597d78eaca535 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 14 May 2025 08:53:47 -0600 Subject: [PATCH 38/80] refactor(api): rename assignable user types and methods --- api/queries_repo.go | 148 +++++++++++++++++++++++-------- api/queries_repo_test.go | 6 +- pkg/cmd/pr/shared/completion.go | 8 +- pkg/cmd/pr/shared/editable.go | 4 +- pkg/cmd/pr/shared/survey.go | 2 +- pkg/cmd/pr/shared/survey_test.go | 6 +- 6 files changed, 123 insertions(+), 51 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index ea60319a3..9e822767d 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -682,8 +682,8 @@ func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Reposit type RepoMetadataResult struct { CurrentLogin string - AssignableUsers []RepoAssignee - AssignableActors []RepoAssignee + AssignableUsers []AssignableUser + AssignableActors []AssignableActor Labels []RepoLabel Projects []RepoProject ProjectsV2 []ProjectV2 @@ -696,8 +696,8 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { for _, assigneeLogin := range names { found := false for _, u := range m.AssignableUsers { - if strings.EqualFold(assigneeLogin, u.Login) { - ids = append(ids, u.ID) + if strings.EqualFold(assigneeLogin, (u.Login())) { + ids = append(ids, u.ID()) found = true break } @@ -705,8 +705,8 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { // Look for ID in assignable actors if not found in assignable users for _, a := range m.AssignableActors { - if strings.EqualFold(assigneeLogin, a.Login) { - ids = append(ids, a.ID) + if strings.EqualFold(assigneeLogin, a.Login()) { + ids = append(ids, a.ID()) found = true break } @@ -1126,12 +1126,16 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes result.Teams = append(result.Teams, t) } default: - user := RepoAssignee{} + user := struct { + Id string + Login string + Name string + }{} err := json.Unmarshal(v, &user) if err != nil { return result, err } - result.AssignableUsers = append(result.AssignableUsers, user) + result.AssignableUsers = append(result.AssignableUsers, NewAssignableUser(user.Id, user.Login, user.Name)) } } @@ -1183,26 +1187,86 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) return projects, nil } -type RepoAssignee struct { - ID string - Login string - Name string +type AssignableActor interface { + DisplayName() string + ID() string + Login() string + + sealedAssignableActor() +} + +// Always a user +type AssignableUser struct { + id string + login string + name string +} + +// NewAssignableUser is a test helper to create a new AssignableUser +// since the ID and Login are private fields +func NewAssignableUser(id, login, name string) AssignableUser { + return AssignableUser{ + id: id, + login: login, + name: name, + } } // DisplayName returns a formatted string that uses Login and Name to be displayed e.g. 'Login (Name)' or 'Login' -func (ra RepoAssignee) DisplayName() string { - if ra.Name != "" { - return fmt.Sprintf("%s (%s)", ra.Login, ra.Name) +func (u AssignableUser) DisplayName() string { + if u.name != "" { + return fmt.Sprintf("%s (%s)", u.login, u.name) } - return ra.Login + return u.login } +func (u AssignableUser) ID() string { + return u.id +} + +func (u AssignableUser) Login() string { + return u.login +} + +func (u AssignableUser) Name() string { + return u.name +} + +func (u AssignableUser) sealedAssignableActor() {} + +type AssignableBot struct { + id string + login string +} + +func (b AssignableBot) DisplayName() string { + return b.Login() +} + +func (b AssignableBot) ID() string { + return b.id +} + +func (b AssignableBot) Login() string { + return b.login +} + +func (b AssignableBot) Name() string { + return "" +} + +func (b AssignableBot) sealedAssignableActor() {} + // RepoAssignableUsers fetches all the assignable users for a repository -func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) { +func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]AssignableUser, error) { type responseData struct { Repository struct { AssignableUsers struct { - Nodes []RepoAssignee + Nodes []struct { + ID string + Login string + Name string + } PageInfo struct { HasNextPage bool EndCursor string @@ -1217,7 +1281,7 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, "endCursor": (*githubv4.String)(nil), } - var users []RepoAssignee + var users []AssignableUser for { var query responseData err := client.Query(repo.RepoHost(), "RepositoryAssignableUsers", &query, variables) @@ -1225,7 +1289,15 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, return nil, err } - users = append(users, query.Repository.AssignableUsers.Nodes...) + for _, node := range query.Repository.AssignableUsers.Nodes { + user := AssignableUser{ + id: node.ID, + login: node.Login, + name: node.Name, + } + + users = append(users, user) + } if !query.Repository.AssignableUsers.PageInfo.HasNextPage { break } @@ -1237,26 +1309,26 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, // RepoAssignableActors fetches all the assignable actors for a repository on // GitHub hosts that support Actor assignees. -func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) { - type repoBotAssignee struct { - ID string - Login string - TypeName string `graphql:"__typename"` - } - - type repoUserAssignee struct { +func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]AssignableActor, error) { + type assignableUser struct { ID string Login string Name string TypeName string `graphql:"__typename"` } + type assignableBot struct { + ID string + Login string + TypeName string `graphql:"__typename"` + } + type responseData struct { Repository struct { SuggestedActors struct { Nodes []struct { - User repoUserAssignee `graphql:"... on User"` - Bot repoBotAssignee `graphql:"... on Bot"` + User assignableUser `graphql:"... on User"` + Bot assignableBot `graphql:"... on Bot"` } PageInfo struct { HasNextPage bool @@ -1272,7 +1344,7 @@ func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]RepoAssignee "endCursor": (*githubv4.String)(nil), } - var actors []RepoAssignee + var actors []AssignableActor for { var query responseData err := client.Query(repo.RepoHost(), "RepositoryAssignableActors", &query, variables) @@ -1282,16 +1354,16 @@ func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]RepoAssignee for _, node := range query.Repository.SuggestedActors.Nodes { if node.User.TypeName == "User" { - actor := RepoAssignee{ - ID: node.User.ID, - Login: node.User.Login, - Name: node.User.Name, + actor := AssignableUser{ + id: node.User.ID, + login: node.User.Login, + name: node.User.Name, } actors = append(actors, actor) } else if node.Bot.TypeName == "Bot" { - actor := RepoAssignee{ - ID: node.Bot.ID, - Login: node.Bot.Login, + actor := AssignableBot{ + id: node.Bot.ID, + login: node.Bot.Login, } actors = append(actors, actor) } diff --git a/api/queries_repo_test.go b/api/queries_repo_test.go index 01fc7a4c7..9040a0018 100644 --- a/api/queries_repo_test.go +++ b/api/queries_repo_test.go @@ -526,17 +526,17 @@ func Test_RepoMilestones(t *testing.T) { func TestDisplayName(t *testing.T) { tests := []struct { name string - assignee RepoAssignee + assignee AssignableUser want string }{ { name: "assignee with name", - assignee: RepoAssignee{"123", "octocat123", "Octavious Cath"}, + assignee: AssignableUser{"123", "octocat123", "Octavious Cath"}, want: "octocat123 (Octavious Cath)", }, { name: "assignee without name", - assignee: RepoAssignee{"123", "octocat123", ""}, + assignee: AssignableUser{"123", "octocat123", ""}, want: "octocat123", }, } diff --git a/pkg/cmd/pr/shared/completion.go b/pkg/cmd/pr/shared/completion.go index e07abc5a7..c1296be71 100644 --- a/pkg/cmd/pr/shared/completion.go +++ b/pkg/cmd/pr/shared/completion.go @@ -21,13 +21,13 @@ func RequestableReviewersForCompletion(httpClient *http.Client, repo ghrepo.Inte results := []string{} for _, user := range metadata.AssignableUsers { - if strings.EqualFold(user.Login, metadata.CurrentLogin) { + if strings.EqualFold(user.Login(), metadata.CurrentLogin) { continue } - if user.Name != "" { - results = append(results, fmt.Sprintf("%s\t%s", user.Login, user.Name)) + if user.Name() != "" { + results = append(results, fmt.Sprintf("%s\t%s", user.Login(), user.Name())) } else { - results = append(results, user.Login) + results = append(results, user.Login()) } } for _, team := range metadata.Teams { diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index 238939efe..d51405acd 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -407,11 +407,11 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) var users []string for _, u := range metadata.AssignableUsers { - users = append(users, u.Login) + users = append(users, u.Login()) } var actors []string for _, a := range metadata.AssignableActors { - actors = append(actors, a.Login) + actors = append(actors, a.Login()) } var teams []string for _, t := range metadata.Teams { diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go index bf4476ca1..b6c927a2d 100644 --- a/pkg/cmd/pr/shared/survey.go +++ b/pkg/cmd/pr/shared/survey.go @@ -192,7 +192,7 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface var reviewers []string for _, u := range metadataResult.AssignableUsers { - if u.Login != metadataResult.CurrentLogin { + if u.Login() != metadataResult.CurrentLogin { reviewers = append(reviewers, u.DisplayName()) } } diff --git a/pkg/cmd/pr/shared/survey_test.go b/pkg/cmd/pr/shared/survey_test.go index 6895b52ac..7097d0761 100644 --- a/pkg/cmd/pr/shared/survey_test.go +++ b/pkg/cmd/pr/shared/survey_test.go @@ -28,9 +28,9 @@ func TestMetadataSurvey_selectAll(t *testing.T) { fetcher := &metadataFetcher{ metadataResult: &api.RepoMetadataResult{ - AssignableUsers: []api.RepoAssignee{ - {Login: "hubot"}, - {Login: "monalisa"}, + AssignableUsers: []api.AssignableUser{ + api.NewAssignableUser("", "hubot", ""), + api.NewAssignableUser("", "monalisa", ""), }, Labels: []api.RepoLabel{ {Name: "help wanted"}, From 712eeabd4acbef9af03a297c38eec4e476cc993f Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 14 May 2025 08:55:55 -0600 Subject: [PATCH 39/80] doc(api): remove needless comment --- api/queries_repo.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 9e822767d..e6286bb0b 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -1202,8 +1202,6 @@ type AssignableUser struct { name string } -// NewAssignableUser is a test helper to create a new AssignableUser -// since the ID and Login are private fields func NewAssignableUser(id, login, name string) AssignableUser { return AssignableUser{ id: id, From 08cd1dc7db690477fcfc32aec3f8c171fc53bb80 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 14 May 2025 11:43:29 -0600 Subject: [PATCH 40/80] feat(issue edit): replacing actor assignee is done synchronously with updateIssue --- pkg/cmd/issue/edit/edit_test.go | 8 -------- pkg/cmd/pr/shared/editable_http.go | 29 +++++++++++++++++------------ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index bf2a1c417..0df6d5e9f 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -574,14 +574,6 @@ func Test_editRun(t *testing.T) { { "errors": [ { "message": "test error" } ] }`, func(inputs map[string]interface{}) {}), ) - reg.Register( - httpmock.GraphQLMutationMatcher(`mutation IssueUpdate\b`, func(m map[string]interface{}) bool { - return m["id"] == "456" - }), - httpmock.GraphQLMutation(` - { "errors": [ { "message": "test error" } ] }`, - func(inputs map[string]interface{}) {}), - ) }, stdout: heredoc.Doc(` https://github.com/OWNER/REPO/issue/123 diff --git a/pkg/cmd/pr/shared/editable_http.go b/pkg/cmd/pr/shared/editable_http.go index 1475f978c..465bfda6c 100644 --- a/pkg/cmd/pr/shared/editable_http.go +++ b/pkg/cmd/pr/shared/editable_http.go @@ -58,23 +58,28 @@ func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR }) } - // updateIssue mutation does not support Actors so assignment needs to - // be in a separate request when our assignees are Actors. - if options.Assignees.Edited && options.Assignees.ActorAssignees { + if dirtyExcludingLabels(options) { wg.Go(func() error { - apiClient := api.NewClientFromHTTP(httpClient) - assigneeIds, err := options.AssigneeIds(apiClient, repo) + // updateIssue mutation does not support Actors so assignment needs to + // be in a separate request when our assignees are Actors. + if options.Assignees.Edited && options.Assignees.ActorAssignees { + apiClient := api.NewClientFromHTTP(httpClient) + assigneeIds, err := options.AssigneeIds(apiClient, repo) + if err != nil { + return err + } + + err = replaceActorAssigneesForEditable(apiClient, repo, id, assigneeIds) + if err != nil { + return err + } + } + err := replaceIssueFields(httpClient, repo, id, isPR, options) if err != nil { return err } - return replaceActorAssigneesForEditable(apiClient, repo, id, assigneeIds) - }) - } - - if dirtyExcludingLabels(options) { - wg.Go(func() error { - return replaceIssueFields(httpClient, repo, id, isPR, options) + return nil }) } From 5dc854c75e1735fb906640963dcaea462000f4c9 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 14 May 2025 11:50:11 -0600 Subject: [PATCH 41/80] doc(issue edit): clarify synchronous handling of assignees --- pkg/cmd/pr/shared/editable_http.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/pr/shared/editable_http.go b/pkg/cmd/pr/shared/editable_http.go index 465bfda6c..cd183e565 100644 --- a/pkg/cmd/pr/shared/editable_http.go +++ b/pkg/cmd/pr/shared/editable_http.go @@ -62,6 +62,10 @@ func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR wg.Go(func() error { // updateIssue mutation does not support Actors so assignment needs to // be in a separate request when our assignees are Actors. + // Note: this is intentionally done synchronously with updating + // other issue fields to ensure consistency with how legacy + // user assignees are handled. + // https://github.com/cli/cli/pull/10960#discussion_r2086725348 if options.Assignees.Edited && options.Assignees.ActorAssignees { apiClient := api.NewClientFromHTTP(httpClient) assigneeIds, err := options.AssigneeIds(apiClient, repo) From 71f22d8843a8f5f862be19e7bd3f4cd53b82c118 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 13 May 2025 13:34:58 -0600 Subject: [PATCH 42/80] feat(pr edit): fetch assigned actors --- api/queries_pr.go | 1 + pkg/cmd/pr/edit/edit.go | 41 ++++++++++++++++++++++++++++++++++-- pkg/cmd/pr/edit/edit_test.go | 2 ++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 5b941bb42..525418a11 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -84,6 +84,7 @@ type PullRequest struct { } Assignees Assignees + AssignedActors AssignedActors Labels Labels ProjectCards ProjectCards ProjectItems ProjectItems diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 3c8d73ad3..23a75dd49 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -3,9 +3,11 @@ package edit import ( "fmt" "net/http" + "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" @@ -25,6 +27,8 @@ type EditOptions struct { Fetcher EditableOptionsFetcher EditorRetriever EditorRetriever Prompter shared.EditPrompter + Detector fd.Detector + BaseRepo func() (ghrepo.Interface, error) SelectorArg string Interactive bool @@ -69,6 +73,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.Finder = shared.NewFinder(f) + opts.BaseRepo = f.BaseRepo if len(args) > 0 { opts.SelectorArg = args[0] @@ -192,8 +197,35 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman func editRun(opts *EditOptions) error { findOptions := shared.FindOptions{ Selector: opts.SelectorArg, - Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "assignees", "labels", "projectCards", "projectItems", "milestone"}, + Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"}, } + + if opts.Detector == nil { + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + baseRepo, err := opts.BaseRepo() + if err != nil { + return err + } + + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) + } + + issueFeatures, err := opts.Detector.IssueFeatures() + if err != nil { + return err + } + + if issueFeatures.ActorIsAssignable { + findOptions.Fields = append(findOptions.Fields, "assignedActors") + } else { + findOptions.Fields = append(findOptions.Fields, "assignees") + } + pr, repo, err := opts.Finder.Find(findOptions) if err != nil { return err @@ -205,7 +237,12 @@ func editRun(opts *EditOptions) error { editable.Body.Default = pr.Body editable.Base.Default = pr.BaseRefName editable.Reviewers.Default = pr.ReviewRequests.Logins() - editable.Assignees.Default = pr.Assignees.Logins() + if issueFeatures.ActorIsAssignable { + // editable.Assignees.ActorAssignees = true + editable.Assignees.Default = pr.AssignedActors.Logins() + } else { + editable.Assignees.Default = pr.Assignees.Logins() + } editable.Labels.Default = pr.Labels.Names() editable.Projects.Default = append(pr.ProjectCards.ProjectNames(), pr.ProjectItems.ProjectTitles()...) projectItems := map[string]string{} diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index f45984c76..64231fa70 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -507,9 +507,11 @@ func Test_editRun(t *testing.T) { tt.httpStubs(reg) httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } + baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } tt.input.IO = ios tt.input.HttpClient = httpClient + tt.input.BaseRepo = baseRepo err := editRun(tt.input) assert.NoError(t, err) From d4832ba0159f6a564d55a93918cd038d9d8acab2 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 14 May 2025 11:04:19 -0600 Subject: [PATCH 43/80] feat(pr edit): fetch assignable actors --- api/queries_repo.go | 42 +++++++++------------- pkg/cmd/pr/edit/edit.go | 2 +- pkg/cmd/pr/edit/edit_test.go | 70 +++++++++++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index e6286bb0b..8f2646159 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -918,39 +918,30 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput var g errgroup.Group if input.Assignees || input.Reviewers { - if input.ActorAssignees { g.Go(func() error { actors, err := RepoAssignableActors(client, repo) if err != nil { - err = fmt.Errorf("error fetching assignees: %w", err) + return fmt.Errorf("error fetching assignees: %w", err) } result.AssignableActors = actors - return err - }) - // If reviewers are also requested, we still need to fetch the assignable users - // since commands use assignable users for reviewers too, - // but Actors are not supported for requesting review (need to confirm this). - // TODO KW: find out how to do this in the above query so we don't need to - // run two potentially expensive queries. When we fetch Actors, this - // should still return Users - Users are distinguishable from other Actors - // by having a name property. Maybe we can use the Name to filter out - // non-user Actors and populate the users list for reviewers based on - // that. - // Note: this only matters for `gh pr` flows, which currently does not - // request actor assignees, so we probably won't hit this until - // `gh pr` reqeuests actor assignees. - if input.Reviewers { - g.Go(func() error { - users, err := RepoAssignableUsers(client, repo) - if err != nil { - err = fmt.Errorf("error fetching assignees: %w", err) + // Processing the bots out from the actors + // because requesting a reviewer leverages + // result.AssignableUsers. + // Note: this prevents us from needing to make another + // request to fetch assignable users when the user has + // selected to modify both reviewers and assignees. + var users []AssignableUser + for _, a := range actors { + if _, ok := a.(AssignableUser); !ok { + continue } - result.AssignableUsers = users - return err - }) - } + users = append(users, a.(AssignableUser)) + } + result.AssignableUsers = users + return nil + }) } else { // Not using Actors, fetch legacy assignable users. g.Go(func() error { @@ -962,7 +953,6 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput return err }) } - } if input.Reviewers { diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 23a75dd49..d484f74ed 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -238,7 +238,7 @@ func editRun(opts *EditOptions) error { editable.Base.Default = pr.BaseRefName editable.Reviewers.Default = pr.ReviewRequests.Logins() if issueFeatures.ActorIsAssignable { - // editable.Assignees.ActorAssignees = true + editable.Assignees.ActorAssignees = true editable.Assignees.Default = pr.AssignedActors.Logins() } else { editable.Assignees.Default = pr.Assignees.Logins() diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 64231fa70..5c2e65eba 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" @@ -392,6 +393,7 @@ func Test_editRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { mockRepoMetadata(reg, false) mockPullRequestUpdate(reg) + mockPullRequestUpdateActorAssignees(reg) mockPullRequestReviewersUpdate(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) @@ -448,6 +450,7 @@ func Test_editRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { mockRepoMetadata(reg, true) mockPullRequestUpdate(reg) + mockPullRequestUpdateActorAssignees(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) }, @@ -468,6 +471,7 @@ func Test_editRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { mockRepoMetadata(reg, false) mockPullRequestUpdate(reg) + mockPullRequestUpdateActorAssignees(reg) mockPullRequestReviewersUpdate(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) @@ -489,11 +493,50 @@ func Test_editRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { mockRepoMetadata(reg, true) mockPullRequestUpdate(reg) + mockPullRequestUpdateActorAssignees(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) }, stdout: "https://github.com/OWNER/REPO/pull/123\n", }, + { + name: "Legacy assignee user are fetched and updated on unsupported GitHub Hosts", + input: &EditOptions{ + Detector: &fd.DisabledDetectorMock{}, + SelectorArg: "123", + Finder: shared.NewMockFinder("123", &api.PullRequest{ + URL: "https://github.com/OWNER/REPO/pull/123", + }, ghrepo.New("OWNER", "REPO")), + Interactive: false, + Editable: shared.Editable{ + Assignees: shared.EditableAssignees{ + EditableSlice: shared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, + }, + }, + Fetcher: testFetcher{}, + }, + httpStubs: func(reg *httpmock.Registry) { + // Notice there is no call to mockReplaceActorsForAssignable() + // and no GraphQL call to RepositoryAssignableActors below. + reg.Register( + httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.StringResponse(` + { "data": { "repository": { "assignableUsers": { + "nodes": [ + { "login": "hubot", "id": "HUBOTID" }, + { "login": "MonaLisa", "id": "MONAID" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + mockPullRequestUpdate(reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123\n", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -523,16 +566,16 @@ func Test_editRun(t *testing.T) { func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { reg.Register( - httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.GraphQL(`query RepositoryAssignableActors\b`), httpmock.StringResponse(` - { "data": { "repository": { "assignableUsers": { - "nodes": [ - { "login": "hubot", "id": "HUBOTID" }, - { "login": "MonaLisa", "id": "MONAID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) + { "data": { "repository": { "suggestedActors": { + "nodes": [ + { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, + { "login": "MonaLisa", "id": "MONAID", "__typename": "User" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) reg.Register( httpmock.GraphQL(`query RepositoryLabelList\b`), httpmock.StringResponse(` @@ -635,6 +678,15 @@ func mockPullRequestUpdate(reg *httpmock.Registry) { httpmock.StringResponse(`{}`)) } +func mockPullRequestUpdateActorAssignees(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`), + httpmock.GraphQLMutation(` + { "data": { "replaceActorsForAssignable": { "__typename": "" } } }`, + func(inputs map[string]interface{}) {}), + ) +} + func mockPullRequestReviewersUpdate(reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`mutation PullRequestUpdateRequestReviews\b`), From eace1e889ae41e7b17d8e3c3caf6922ad2c4633b Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 14 May 2025 20:26:53 -0600 Subject: [PATCH 44/80] feat(editable): update assigned actors to use display names - Refactored AssignedActors to return display names instead of logins. - Updated related functions to ensure consistency in display names. - Enhanced comments for clarity on display name logic and actor types. --- api/queries_issue.go | 49 ++++++++++++++++++++++++++++------- api/queries_repo.go | 28 +++++++++++++++++--- api/query_builder.go | 21 ++++++++++++++- pkg/cmd/issue/edit/edit.go | 2 +- pkg/cmd/pr/edit/edit.go | 2 +- pkg/cmd/pr/shared/editable.go | 2 +- 6 files changed, 87 insertions(+), 17 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index 701b92039..bfdd223e5 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -93,19 +93,50 @@ func (a Assignees) Logins() []string { } type AssignedActors struct { - Edges []struct { - Node Actor - } + Nodes []Actor TotalCount int } -// TODO kw: Display names for actors with special display names. -func (a AssignedActors) Logins() []string { - logins := make([]string, len(a.Edges)) - for i, a := range a.Edges { - logins[i] = a.Node.Login +// DisplayNames returns a list of display names for the assigned actors. +func (a AssignedActors) DisplayNames() []string { + // These display names are used for populating the "default" assigned actors + // from the AssignedActors type. But, this is only one piece of the puzzle + // as later, other queries will fetch the full list of possible assignable + // actors from the repository, and the two lists will be reconciled. + // + // It's important that the display names are the same between the defaults + // (the values returned here) and the full list (the values returned by + // other repository queries). Any discrepancy would result in an + // "invalid default", which means an assigned actor will not be matched + // to an assignable actor and not presented as a "default" selection. + // Not being presented as a default would cause the actor to be potentially + // unassigned if the edits were submitted. + // + // To prevent this, we need shared logic to look up an actor's display name. + // However, our API types between assignedActors and the full list of + // assignableActors are different. So, as an attempt to maintain + // consistency we convert the assignedActors to the same types as the + // repository's assignableActors, treating the assignableActors DisplayName + // methods as the sources of truth. + // TODO KW: make this comment less of a wall of text if needed. + displayNames := make([]string, len(a.Nodes)) + for _, a := range a.Nodes { + if a.TypeName == "User" { + u := NewAssignableUser( + a.ID, + a.Login, + a.Name, + ) + displayNames = append(displayNames, u.DisplayName()) + } else if a.TypeName == "Bot" { + b := NewAssignableBot( + a.ID, + a.Login, + ) + displayNames = append(displayNames, b.DisplayName()) + } } - return logins + return displayNames } type Labels struct { diff --git a/api/queries_repo.go b/api/queries_repo.go index 8f2646159..dd9027ad4 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -146,11 +146,16 @@ type GitHubUser struct { Name string `json:"name"` } -// Actor is a superset of User and Bot -// At the time of writing, it does not support Name. +// Actor is a superset of User and Bot, among others. +// At the time of writing, some of these fields +// are not directly supported by the Actor type and +// instead are only available on the User or Bot types +// directly. type Actor struct { - ID string `json:"id"` - Login string `json:"login"` + ID string `json:"id"` + Login string `json:"login"` + Name string `json:"name"` + TypeName string `json:"__typename"` } // BranchRef is the branch name in a GitHub repository @@ -710,6 +715,11 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { found = true break } + if strings.EqualFold(assigneeLogin, a.DisplayName()) { + ids = append(ids, a.ID()) + found = true + break + } } if !found { @@ -1227,7 +1237,17 @@ type AssignableBot struct { login string } +func NewAssignableBot(id, login string) AssignableBot { + return AssignableBot{ + id: id, + login: login, + } +} + func (b AssignableBot) DisplayName() string { + if b.login == "copilot-swe-agent" { + return "Copilot (AI)" + } return b.Login() } diff --git a/api/query_builder.go b/api/query_builder.go index 0ef44a347..a2432673b 100644 --- a/api/query_builder.go +++ b/api/query_builder.go @@ -20,6 +20,25 @@ func shortenQuery(q string) string { return strings.Map(squeeze, q) } +var assignedActors = shortenQuery(` + assignedActors(first: 10) { + nodes { + ...on User { + id, + login, + name, + __typename + } + ...on Bot { + id, + login, + __typename + } + }, + totalCount + } +`) + var issueComments = shortenQuery(` comments(first: 100) { nodes { @@ -367,7 +386,7 @@ func IssueGraphQL(fields []string) string { case "assignees": q = append(q, `assignees(first:100){nodes{id,login,name},totalCount}`) case "assignedActors": - q = append(q, `assignedActors(first: 10){edges{node{...on Actor{login}}},totalCount}`) + q = append(q, assignedActors) case "labels": q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`) case "projectCards": diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 5cb789543..f3c4e46ad 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -267,7 +267,7 @@ func editRun(opts *EditOptions) error { // We use Actors as the default assignees if Actors are assignable // on this GitHub host. if editable.Assignees.ActorAssignees { - editable.Assignees.Default = issue.AssignedActors.Logins() + editable.Assignees.Default = issue.AssignedActors.DisplayNames() } else { editable.Assignees.Default = issue.Assignees.Logins() } diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index d484f74ed..3002c8dbe 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -239,7 +239,7 @@ func editRun(opts *EditOptions) error { editable.Reviewers.Default = pr.ReviewRequests.Logins() if issueFeatures.ActorIsAssignable { editable.Assignees.ActorAssignees = true - editable.Assignees.Default = pr.AssignedActors.Logins() + editable.Assignees.Default = pr.AssignedActors.DisplayNames() } else { editable.Assignees.Default = pr.Assignees.Logins() } diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index d51405acd..cc11812ae 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -411,7 +411,7 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) } var actors []string for _, a := range metadata.AssignableActors { - actors = append(actors, a.Login()) + actors = append(actors, a.DisplayName()) } var teams []string for _, t := range metadata.Teams { From da40e087460c0d01f741fba8200b1c5d825b2bee Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 08:33:30 -0600 Subject: [PATCH 45/80] doc(api): code comment typo Co-authored-by: Andy Feller --- api/queries_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index e6286bb0b..aa180a015 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -940,7 +940,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput // that. // Note: this only matters for `gh pr` flows, which currently does not // request actor assignees, so we probably won't hit this until - // `gh pr` reqeuests actor assignees. + // `gh pr` requests actor assignees. if input.Reviewers { g.Go(func() error { users, err := RepoAssignableUsers(client, repo) From ea85b92b35dccaae5a4b359a33c74d5a83c21707 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 09:09:36 -0600 Subject: [PATCH 46/80] refactor(api): remove needless parenthesis Co-authored-by: Andy Feller --- api/queries_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index aa180a015..978e844c4 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -696,7 +696,7 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { for _, assigneeLogin := range names { found := false for _, u := range m.AssignableUsers { - if strings.EqualFold(assigneeLogin, (u.Login())) { + if strings.EqualFold(assigneeLogin, u.Login()) { ids = append(ids, u.ID()) found = true break From bcd47f1fb178c60ad7e87f75d3cfbe00f00545d1 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 09:13:46 -0600 Subject: [PATCH 47/80] fix(api): correct var name capitalization --- pkg/cmd/pr/shared/editable_http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/shared/editable_http.go b/pkg/cmd/pr/shared/editable_http.go index cd183e565..8cd51c349 100644 --- a/pkg/cmd/pr/shared/editable_http.go +++ b/pkg/cmd/pr/shared/editable_http.go @@ -103,7 +103,7 @@ func replaceActorAssigneesForEditable(apiClient *api.Client, repo ghrepo.Interfa var mutation struct { ReplaceActorsForAssignable struct { - Typename string `graphql:"__typename"` + TypeName string `graphql:"__typename"` } `graphql:"replaceActorsForAssignable(input: $input)"` } From 0db7ae872f71ec7926dc96c34430a60aa75fc5bf Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 09:09:36 -0600 Subject: [PATCH 48/80] refactor(api): remove needless parenthesis Co-authored-by: Andy Feller --- api/queries_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index e6286bb0b..732d89c8c 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -696,7 +696,7 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { for _, assigneeLogin := range names { found := false for _, u := range m.AssignableUsers { - if strings.EqualFold(assigneeLogin, (u.Login())) { + if strings.EqualFold(assigneeLogin, u.Login()) { ids = append(ids, u.ID()) found = true break From 6162a7c524d4507412e312e275ebff2d0a59da33 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 09:13:46 -0600 Subject: [PATCH 49/80] fix(api): correct var name capitalization --- pkg/cmd/pr/shared/editable_http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/shared/editable_http.go b/pkg/cmd/pr/shared/editable_http.go index cd183e565..8cd51c349 100644 --- a/pkg/cmd/pr/shared/editable_http.go +++ b/pkg/cmd/pr/shared/editable_http.go @@ -103,7 +103,7 @@ func replaceActorAssigneesForEditable(apiClient *api.Client, repo ghrepo.Interfa var mutation struct { ReplaceActorsForAssignable struct { - Typename string `graphql:"__typename"` + TypeName string `graphql:"__typename"` } `graphql:"replaceActorsForAssignable(input: $input)"` } From f55906810645e4a22eeb227d4b189ae9a9a0acaa Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 13 May 2025 13:34:58 -0600 Subject: [PATCH 50/80] feat(pr edit): fetch assigned actors --- api/queries_pr.go | 1 + pkg/cmd/pr/edit/edit.go | 41 ++++++++++++++++++++++++++++++++++-- pkg/cmd/pr/edit/edit_test.go | 2 ++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 5b941bb42..525418a11 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -84,6 +84,7 @@ type PullRequest struct { } Assignees Assignees + AssignedActors AssignedActors Labels Labels ProjectCards ProjectCards ProjectItems ProjectItems diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 3c8d73ad3..23a75dd49 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -3,9 +3,11 @@ package edit import ( "fmt" "net/http" + "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" @@ -25,6 +27,8 @@ type EditOptions struct { Fetcher EditableOptionsFetcher EditorRetriever EditorRetriever Prompter shared.EditPrompter + Detector fd.Detector + BaseRepo func() (ghrepo.Interface, error) SelectorArg string Interactive bool @@ -69,6 +73,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.Finder = shared.NewFinder(f) + opts.BaseRepo = f.BaseRepo if len(args) > 0 { opts.SelectorArg = args[0] @@ -192,8 +197,35 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman func editRun(opts *EditOptions) error { findOptions := shared.FindOptions{ Selector: opts.SelectorArg, - Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "assignees", "labels", "projectCards", "projectItems", "milestone"}, + Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"}, } + + if opts.Detector == nil { + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + baseRepo, err := opts.BaseRepo() + if err != nil { + return err + } + + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) + } + + issueFeatures, err := opts.Detector.IssueFeatures() + if err != nil { + return err + } + + if issueFeatures.ActorIsAssignable { + findOptions.Fields = append(findOptions.Fields, "assignedActors") + } else { + findOptions.Fields = append(findOptions.Fields, "assignees") + } + pr, repo, err := opts.Finder.Find(findOptions) if err != nil { return err @@ -205,7 +237,12 @@ func editRun(opts *EditOptions) error { editable.Body.Default = pr.Body editable.Base.Default = pr.BaseRefName editable.Reviewers.Default = pr.ReviewRequests.Logins() - editable.Assignees.Default = pr.Assignees.Logins() + if issueFeatures.ActorIsAssignable { + // editable.Assignees.ActorAssignees = true + editable.Assignees.Default = pr.AssignedActors.Logins() + } else { + editable.Assignees.Default = pr.Assignees.Logins() + } editable.Labels.Default = pr.Labels.Names() editable.Projects.Default = append(pr.ProjectCards.ProjectNames(), pr.ProjectItems.ProjectTitles()...) projectItems := map[string]string{} diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index f45984c76..64231fa70 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -507,9 +507,11 @@ func Test_editRun(t *testing.T) { tt.httpStubs(reg) httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } + baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } tt.input.IO = ios tt.input.HttpClient = httpClient + tt.input.BaseRepo = baseRepo err := editRun(tt.input) assert.NoError(t, err) From d72018d3120dde1fb783afa3f612cd9255e1eb32 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 14 May 2025 11:04:19 -0600 Subject: [PATCH 51/80] feat(pr edit): fetch assignable actors --- api/queries_repo.go | 42 +++++++++------------- pkg/cmd/pr/edit/edit.go | 2 +- pkg/cmd/pr/edit/edit_test.go | 70 +++++++++++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 732d89c8c..437261c21 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -918,39 +918,30 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput var g errgroup.Group if input.Assignees || input.Reviewers { - if input.ActorAssignees { g.Go(func() error { actors, err := RepoAssignableActors(client, repo) if err != nil { - err = fmt.Errorf("error fetching assignees: %w", err) + return fmt.Errorf("error fetching assignees: %w", err) } result.AssignableActors = actors - return err - }) - // If reviewers are also requested, we still need to fetch the assignable users - // since commands use assignable users for reviewers too, - // but Actors are not supported for requesting review (need to confirm this). - // TODO KW: find out how to do this in the above query so we don't need to - // run two potentially expensive queries. When we fetch Actors, this - // should still return Users - Users are distinguishable from other Actors - // by having a name property. Maybe we can use the Name to filter out - // non-user Actors and populate the users list for reviewers based on - // that. - // Note: this only matters for `gh pr` flows, which currently does not - // request actor assignees, so we probably won't hit this until - // `gh pr` reqeuests actor assignees. - if input.Reviewers { - g.Go(func() error { - users, err := RepoAssignableUsers(client, repo) - if err != nil { - err = fmt.Errorf("error fetching assignees: %w", err) + // Processing the bots out from the actors + // because requesting a reviewer leverages + // result.AssignableUsers. + // Note: this prevents us from needing to make another + // request to fetch assignable users when the user has + // selected to modify both reviewers and assignees. + var users []AssignableUser + for _, a := range actors { + if _, ok := a.(AssignableUser); !ok { + continue } - result.AssignableUsers = users - return err - }) - } + users = append(users, a.(AssignableUser)) + } + result.AssignableUsers = users + return nil + }) } else { // Not using Actors, fetch legacy assignable users. g.Go(func() error { @@ -962,7 +953,6 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput return err }) } - } if input.Reviewers { diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 23a75dd49..d484f74ed 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -238,7 +238,7 @@ func editRun(opts *EditOptions) error { editable.Base.Default = pr.BaseRefName editable.Reviewers.Default = pr.ReviewRequests.Logins() if issueFeatures.ActorIsAssignable { - // editable.Assignees.ActorAssignees = true + editable.Assignees.ActorAssignees = true editable.Assignees.Default = pr.AssignedActors.Logins() } else { editable.Assignees.Default = pr.Assignees.Logins() diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 64231fa70..5c2e65eba 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" @@ -392,6 +393,7 @@ func Test_editRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { mockRepoMetadata(reg, false) mockPullRequestUpdate(reg) + mockPullRequestUpdateActorAssignees(reg) mockPullRequestReviewersUpdate(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) @@ -448,6 +450,7 @@ func Test_editRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { mockRepoMetadata(reg, true) mockPullRequestUpdate(reg) + mockPullRequestUpdateActorAssignees(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) }, @@ -468,6 +471,7 @@ func Test_editRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { mockRepoMetadata(reg, false) mockPullRequestUpdate(reg) + mockPullRequestUpdateActorAssignees(reg) mockPullRequestReviewersUpdate(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) @@ -489,11 +493,50 @@ func Test_editRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { mockRepoMetadata(reg, true) mockPullRequestUpdate(reg) + mockPullRequestUpdateActorAssignees(reg) mockPullRequestUpdateLabels(reg) mockProjectV2ItemUpdate(reg) }, stdout: "https://github.com/OWNER/REPO/pull/123\n", }, + { + name: "Legacy assignee user are fetched and updated on unsupported GitHub Hosts", + input: &EditOptions{ + Detector: &fd.DisabledDetectorMock{}, + SelectorArg: "123", + Finder: shared.NewMockFinder("123", &api.PullRequest{ + URL: "https://github.com/OWNER/REPO/pull/123", + }, ghrepo.New("OWNER", "REPO")), + Interactive: false, + Editable: shared.Editable{ + Assignees: shared.EditableAssignees{ + EditableSlice: shared.EditableSlice{ + Add: []string{"monalisa", "hubot"}, + Remove: []string{"octocat"}, + Edited: true, + }, + }, + }, + Fetcher: testFetcher{}, + }, + httpStubs: func(reg *httpmock.Registry) { + // Notice there is no call to mockReplaceActorsForAssignable() + // and no GraphQL call to RepositoryAssignableActors below. + reg.Register( + httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.StringResponse(` + { "data": { "repository": { "assignableUsers": { + "nodes": [ + { "login": "hubot", "id": "HUBOTID" }, + { "login": "MonaLisa", "id": "MONAID" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + mockPullRequestUpdate(reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123\n", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -523,16 +566,16 @@ func Test_editRun(t *testing.T) { func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) { reg.Register( - httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.GraphQL(`query RepositoryAssignableActors\b`), httpmock.StringResponse(` - { "data": { "repository": { "assignableUsers": { - "nodes": [ - { "login": "hubot", "id": "HUBOTID" }, - { "login": "MonaLisa", "id": "MONAID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) + { "data": { "repository": { "suggestedActors": { + "nodes": [ + { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, + { "login": "MonaLisa", "id": "MONAID", "__typename": "User" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) reg.Register( httpmock.GraphQL(`query RepositoryLabelList\b`), httpmock.StringResponse(` @@ -635,6 +678,15 @@ func mockPullRequestUpdate(reg *httpmock.Registry) { httpmock.StringResponse(`{}`)) } +func mockPullRequestUpdateActorAssignees(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`), + httpmock.GraphQLMutation(` + { "data": { "replaceActorsForAssignable": { "__typename": "" } } }`, + func(inputs map[string]interface{}) {}), + ) +} + func mockPullRequestReviewersUpdate(reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`mutation PullRequestUpdateRequestReviews\b`), From 54f48cfe46e231a7f1d45bdab0706ed999690f60 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 09:47:30 -0600 Subject: [PATCH 52/80] fix(pr edit): remove merge conflict artifact, extra detector --- pkg/cmd/pr/edit/edit.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 75b8c4e09..8d2ff8ddb 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -21,9 +21,6 @@ import ( type EditOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams - // TODO projectsV1Deprecation - // Remove this detector since it is only used for test validation. - Detector fd.Detector Finder shared.PRFinder Surveyor Surveyor From 9a5ea87d75bfb8ea2bb0c4d81f9237301cab2e3d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 09:56:35 -0600 Subject: [PATCH 53/80] fix(issues): fix non-interactive assignee matching to logins&IDs --- api/queries_issue.go | 10 +++++++++- pkg/cmd/issue/edit/edit.go | 1 + pkg/cmd/pr/shared/editable.go | 7 ++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index bfdd223e5..24e0b4f4c 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -97,6 +97,14 @@ type AssignedActors struct { TotalCount int } +func (a AssignedActors) Logins() []string { + logins := make([]string, len(a.Nodes)) + for i, a := range a.Nodes { + logins[i] = a.Login + } + return logins +} + // DisplayNames returns a list of display names for the assigned actors. func (a AssignedActors) DisplayNames() []string { // These display names are used for populating the "default" assigned actors @@ -119,7 +127,7 @@ func (a AssignedActors) DisplayNames() []string { // repository's assignableActors, treating the assignableActors DisplayName // methods as the sources of truth. // TODO KW: make this comment less of a wall of text if needed. - displayNames := make([]string, len(a.Nodes)) + var displayNames []string for _, a := range a.Nodes { if a.TypeName == "User" { u := NewAssignableUser( diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index f3c4e46ad..8fc8a2e41 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -268,6 +268,7 @@ func editRun(opts *EditOptions) error { // on this GitHub host. if editable.Assignees.ActorAssignees { editable.Assignees.Default = issue.AssignedActors.DisplayNames() + editable.Assignees.DefaultLogins = issue.AssignedActors.Logins() } else { editable.Assignees.Default = issue.Assignees.Logins() } diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index cc11812ae..cfe022c78 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -43,6 +43,7 @@ type EditableSlice struct { type EditableAssignees struct { EditableSlice ActorAssignees bool + DefaultLogins []string } // ProjectsV2 mutations require a mapping of an item ID to a project ID. @@ -115,7 +116,11 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str if len(e.Assignees.Add) != 0 || len(e.Assignees.Remove) != 0 { meReplacer := NewMeReplacer(client, repo.RepoHost()) s := set.NewStringSet() - s.AddValues(e.Assignees.Default) + if e.Assignees.ActorAssignees { + s.AddValues(e.Assignees.DefaultLogins) + } else { + s.AddValues(e.Assignees.Default) + } add, err := meReplacer.ReplaceSlice(e.Assignees.Add) if err != nil { return nil, err From 52b6ebff9cf1f4c9a5570cc0d105447cfc7c938c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 10:36:57 -0600 Subject: [PATCH 54/80] doc(pr edit): Add comments describing the use of DefaultLogins --- pkg/cmd/pr/shared/editable.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index cfe022c78..77bab8440 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -43,7 +43,7 @@ type EditableSlice struct { type EditableAssignees struct { EditableSlice ActorAssignees bool - DefaultLogins []string + DefaultLogins []string // For disambiguating actors from display names } // ProjectsV2 mutations require a mapping of an item ID to a project ID. @@ -113,9 +113,20 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str if !e.Assignees.Edited { return nil, nil } + + // If assignees came in from command line flags, we need to + // curate the final list of assignees from the default list. if len(e.Assignees.Add) != 0 || len(e.Assignees.Remove) != 0 { meReplacer := NewMeReplacer(client, repo.RepoHost()) s := set.NewStringSet() + // This check below is required because in a non-interactive flow, + // the user gives us a login and not the DisplayName, and when + // we have actor assignees e.Assignees.Default will contain + // DisplayNames and not logins (this is to accommodate special actor + // display names in the interactive flow). + // So, we need to add the default logins here instead of the DisplayNames. + // Otherwise, the value the user provided won't be found in the + // set to be added or removed, causing unexpected behavior. if e.Assignees.ActorAssignees { s.AddValues(e.Assignees.DefaultLogins) } else { From 375c6cd28f7ded11fe8b53974735ec10404ea865 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 12:50:03 -0600 Subject: [PATCH 55/80] feat(issue/pr edit): support @copilot in assignee flags - Introduced CopilotReplacer to handle `@copilot` mentions in assignee lists. --- pkg/cmd/pr/shared/editable.go | 40 +++++++++++++++++++----- pkg/cmd/pr/shared/params.go | 27 ++++++++++++++++ pkg/cmd/pr/shared/params_test.go | 53 ++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index 77bab8440..1bbcd3113 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -118,7 +118,28 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str // curate the final list of assignees from the default list. if len(e.Assignees.Add) != 0 || len(e.Assignees.Remove) != 0 { meReplacer := NewMeReplacer(client, repo.RepoHost()) - s := set.NewStringSet() + copilotReplacer := NewCopilotReplacer() + + // A closure to replace special assignee names with the actual logins. + replaceSpecialAssigneeNames := func(value []string) ([]string, error) { + replaced, err := meReplacer.ReplaceSlice(value) + if err != nil { + return nil, err + } + + // Only suppported for actor assignees. + if e.Assignees.ActorAssignees { + replaced, err = copilotReplacer.ReplaceSlice(replaced) + if err != nil { + return nil, err + } + } + + return replaced, nil + } + + assigneeSet := set.NewStringSet() + // This check below is required because in a non-interactive flow, // the user gives us a login and not the DisplayName, and when // we have actor assignees e.Assignees.Default will contain @@ -128,21 +149,24 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str // Otherwise, the value the user provided won't be found in the // set to be added or removed, causing unexpected behavior. if e.Assignees.ActorAssignees { - s.AddValues(e.Assignees.DefaultLogins) + assigneeSet.AddValues(e.Assignees.DefaultLogins) } else { - s.AddValues(e.Assignees.Default) + assigneeSet.AddValues(e.Assignees.Default) } - add, err := meReplacer.ReplaceSlice(e.Assignees.Add) + + add, err := replaceSpecialAssigneeNames(e.Assignees.Add) if err != nil { return nil, err } - s.AddValues(add) - remove, err := meReplacer.ReplaceSlice(e.Assignees.Remove) + assigneeSet.AddValues(add) + + remove, err := replaceSpecialAssigneeNames(e.Assignees.Remove) if err != nil { return nil, err } - s.RemoveValues(remove) - e.Assignees.Value = s.ToSlice() + assigneeSet.RemoveValues(remove) + + e.Assignees.Value = assigneeSet.ToSlice() } a, err := e.Metadata.MembersToIDs(e.Assignees.Value) return &a, err diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index 08968939d..7ea364707 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -312,3 +312,30 @@ func (r *MeReplacer) ReplaceSlice(handles []string) ([]string, error) { } return res, nil } + +// CopilotReplacer resolves usages of `@copilot` to Copilot's login. +type CopilotReplacer struct{} + +func NewCopilotReplacer() *CopilotReplacer { + return &CopilotReplacer{} +} + +func (r *CopilotReplacer) replace(handle string) (string, error) { + if strings.EqualFold(handle, "@copilot") { + return "copilot-swe-agent", nil + } + return handle, nil +} + +// Replace replaces usages of `@copilot` in a slice with Copilot's login. +func (r *CopilotReplacer) ReplaceSlice(handles []string) ([]string, error) { + res := make([]string, len(handles)) + for i, h := range handles { + var err error + res[i], err = r.replace(h) + if err != nil { + return nil, err + } + } + return res, nil +} diff --git a/pkg/cmd/pr/shared/params_test.go b/pkg/cmd/pr/shared/params_test.go index 15f00ca4f..b1e3d32d6 100644 --- a/pkg/cmd/pr/shared/params_test.go +++ b/pkg/cmd/pr/shared/params_test.go @@ -187,6 +187,59 @@ func TestMeReplacer_Replace(t *testing.T) { } } +func TestCopilotReplacer_ReplaceSlice(t *testing.T) { + type args struct { + handles []string + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "replaces @copilot with copilot-swe-agent", + args: args{ + handles: []string{"monalisa", "@copilot", "hubot"}, + }, + want: []string{"monalisa", "copilot-swe-agent", "hubot"}, + wantErr: false, + }, + { + name: "handles no @copilot mentions", + args: args{ + handles: []string{"monalisa", "user", "hubot"}, + }, + want: []string{"monalisa", "user", "hubot"}, + wantErr: false, + }, + { + name: "replaces multiple @copilot mentions", + args: args{ + handles: []string{"@copilot", "user", "@copilot"}, + }, + want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"}, + wantErr: false, + }, + { + name: "handles @copilot case-insensitively", + args: args{ + handles: []string{"@Copilot", "user", "@CoPiLoT"}, + }, + want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewCopilotReplacer() + got, err := r.ReplaceSlice(tt.args.handles) + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + func Test_QueryHasStateClause(t *testing.T) { tests := []struct { searchQuery string From f6bb1ca75653975d42a7d43c7c7ee24784a79d8a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 13:40:23 -0600 Subject: [PATCH 56/80] refactor(pr edit): move httpclient initialization --- pkg/cmd/pr/edit/edit.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 8d2ff8ddb..ff293a511 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -201,12 +201,12 @@ func editRun(opts *EditOptions) error { Detector: opts.Detector, } - if opts.Detector == nil { - httpClient, err := opts.HttpClient() - if err != nil { - return err - } + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + if opts.Detector == nil { baseRepo, err := opts.BaseRepo() if err != nil { return err @@ -262,10 +262,6 @@ func editRun(opts *EditOptions) error { } } - httpClient, err := opts.HttpClient() - if err != nil { - return err - } apiClient := api.NewClientFromHTTP(httpClient) opts.IO.StartProgressIndicator() From ec7c8ed844ec1532b6026af5d0b57bb0d238ed58 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 13:41:38 -0600 Subject: [PATCH 57/80] test(pr edit): fix typo in test name Co-authored-by: Andy Feller --- pkg/cmd/pr/edit/edit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 0f15d6bd1..76fb40f88 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -500,7 +500,7 @@ func Test_editRun(t *testing.T) { stdout: "https://github.com/OWNER/REPO/pull/123\n", }, { - name: "Legacy assignee user are fetched and updated on unsupported GitHub Hosts", + name: "Legacy assignee users are fetched and updated on unsupported GitHub Hosts", input: &EditOptions{ Detector: &fd.DisabledDetectorMock{}, SelectorArg: "123", From 981da867018817f7ea08dae2259e022f2f231fe0 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 13:47:51 -0600 Subject: [PATCH 58/80] doc(pr edit): condense comment for reviewer/user filtering --- api/queries_repo.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 437261c21..1bd814746 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -926,12 +926,8 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput } result.AssignableActors = actors - // Processing the bots out from the actors - // because requesting a reviewer leverages - // result.AssignableUsers. - // Note: this prevents us from needing to make another - // request to fetch assignable users when the user has - // selected to modify both reviewers and assignees. + // Filter actors for users to use for pull request reviewers, + // skip retrieving the same info through RepoAssignableUsers(). var users []AssignableUser for _, a := range actors { if _, ok := a.(AssignableUser); !ok { From 8bd77c0a956680b16cfc0610e61f803bbe05b7ca Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 15 May 2025 14:01:10 -0600 Subject: [PATCH 59/80] fix(pr edit): clarify error messages for assignee actors and users --- api/queries_repo.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 1bd814746..fd5ae4679 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -922,7 +922,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput g.Go(func() error { actors, err := RepoAssignableActors(client, repo) if err != nil { - return fmt.Errorf("error fetching assignees: %w", err) + return fmt.Errorf("error fetching assignable actors: %w", err) } result.AssignableActors = actors @@ -943,7 +943,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput g.Go(func() error { users, err := RepoAssignableUsers(client, repo) if err != nil { - err = fmt.Errorf("error fetching assignees: %w", err) + err = fmt.Errorf("error fetching assignable users: %w", err) } result.AssignableUsers = users return err From a22a1bbde475be05875e3a2ac2524e2ae2a51bce Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 09:20:58 -0600 Subject: [PATCH 60/80] test(editable): prompts use assignee display names --- pkg/cmd/issue/edit/edit_test.go | 147 +++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index 0df6d5e9f..4840cbf7a 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -620,6 +620,123 @@ func Test_editRun(t *testing.T) { }, stdout: "https://github.com/OWNER/REPO/issue/123\n", }, + { + name: "interactive prompts with actor assignee display names when actors available", + input: &EditOptions{ + IssueNumbers: []int{123}, + Interactive: true, + FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error { + eo.Assignees.Edited = true + return nil + }, + EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error { + // Checking that the display name is being used in the prompt. + require.Equal(t, eo.Assignees.Default, []string{"hubot", "MonaLisa (Mona Display Name)"}) + + // Mocking a selection of only MonaLisa in the prompt. + eo.Assignees.Value = []string{"MonaLisa (Mona Display Name)"} + return nil + }, + FetchOptions: prShared.FetchOptions, + DetermineEditor: func() (string, error) { return "vim", nil }, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + mockIsssueNumberGetWithAssignedActors(t, reg, 123) + reg.Register( + httpmock.GraphQL(`query RepositoryAssignableActors\b`), + httpmock.StringResponse(` + { "data": { "repository": { "suggestedActors": { + "nodes": [ + { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, + { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + mockIssueUpdate(t, reg) + reg.Register( + httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`), + httpmock.GraphQLMutation(` + { "data": { "replaceActorsForAssignable": { "__typename": "" } } }`, + func(inputs map[string]interface{}) { + // Checking that despite the display name being returned + // from the EditFieldsSurvey, the ID is still + // used in the mutation. + require.Contains(t, inputs["actorIds"], "MONAID") + }), + ) + }, + stdout: "https://github.com/OWNER/REPO/issue/123\n", + }, + { + name: "interactive prompts with user assignee logins when actors unavailable", + input: &EditOptions{ + IssueNumbers: []int{123}, + Interactive: true, + FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error { + eo.Assignees.Edited = true + return nil + }, + EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error { + // Checking that only the login is used in the prompt (no display name) + require.Equal(t, eo.Assignees.Default, []string{"hubot", "MonaLisa"}) + + // Mocking a selection of only MonaLisa in the prompt. + eo.Assignees.Value = []string{"MonaLisa"} + return nil + }, + FetchOptions: prShared.FetchOptions, + DetermineEditor: func() (string, error) { return "vim", nil }, + Detector: &fd.DisabledDetectorMock{}, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(fmt.Sprintf(` + { "data": { "repository": { "hasIssuesEnabled": true, "issue": { + "id": "%[1]d", + "number": %[1]d, + "url": "https://github.com/OWNER/REPO/issue/123", + "assignees": { + "nodes": [ + { + "id": "HUBOTID", + "login": "hubot", + "name": "" + }, + { + "id": "MONAID", + "login": "MonaLisa", + "name": "Mona Display Name" + } + ], + "totalCount": 2 + } + } } } }`, 123)), + ) + reg.Register( + httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.StringResponse(` + { "data": { "repository": { "assignableUsers": { + "nodes": [ + { "login": "hubot", "id": "HUBOTID", "name": "" }, + { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + reg.Register( + httpmock.GraphQL(`mutation IssueUpdate\b`), + httpmock.GraphQLMutation(` + { "data": { "updateIssue": { "__typename": "" } } }`, + func(inputs map[string]interface{}) { + // Checking that we still assigned the expected ID. + require.Contains(t, inputs["assigneeIds"], "MONAID") + }), + ) + }, + stdout: "https://github.com/OWNER/REPO/issue/123\n", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -678,6 +795,34 @@ func mockIssueNumberGet(_ *testing.T, reg *httpmock.Registry, number int) { ) } +func mockIsssueNumberGetWithAssignedActors(_ *testing.T, reg *httpmock.Registry, number int) { + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(fmt.Sprintf(` + { "data": { "repository": { "hasIssuesEnabled": true, "issue": { + "id": "%[1]d", + "number": %[1]d, + "url": "https://github.com/OWNER/REPO/issue/%[1]d", + "assignedActors": { + "nodes": [ + { + "id": "HUBOTID", + "login": "hubot", + "__typename": "Bot" + }, + { + "id": "MONAID", + "login": "MonaLisa", + "name": "Mona Display Name", + "__typename": "User" + } + ], + "totalCount": 2 + } + } } } }`, number)), + ) +} + func mockIssueProjectItemsGet(_ *testing.T, reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`query IssueProjectItems\b`), @@ -699,7 +844,7 @@ func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) { { "data": { "repository": { "suggestedActors": { "nodes": [ { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, - { "login": "MonaLisa", "id": "MONAID", "__typename": "User" } + { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" } ], "pageInfo": { "hasNextPage": false } } } } } From c5f2d7bde70115a15135578a026b765f5c4c3429 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 09:37:20 -0600 Subject: [PATCH 61/80] doc(editable): remove needless comment --- pkg/cmd/pr/shared/editable.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index 1bbcd3113..4b190cae8 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -120,7 +120,6 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str meReplacer := NewMeReplacer(client, repo.RepoHost()) copilotReplacer := NewCopilotReplacer() - // A closure to replace special assignee names with the actual logins. replaceSpecialAssigneeNames := func(value []string) ([]string, error) { replaced, err := meReplacer.ReplaceSlice(value) if err != nil { From 871671be526e7b3e40f90a5a562a5eb75136f0a6 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 09:40:29 -0600 Subject: [PATCH 62/80] fix(params): remove needless err return --- pkg/cmd/pr/shared/editable.go | 5 +---- pkg/cmd/pr/shared/params.go | 16 ++++++---------- pkg/cmd/pr/shared/params_test.go | 3 +-- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index 4b190cae8..c71f89b52 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -128,10 +128,7 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str // Only suppported for actor assignees. if e.Assignees.ActorAssignees { - replaced, err = copilotReplacer.ReplaceSlice(replaced) - if err != nil { - return nil, err - } + replaced = copilotReplacer.ReplaceSlice(replaced) } return replaced, nil diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index 7ea364707..e53ad7a06 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -320,22 +320,18 @@ func NewCopilotReplacer() *CopilotReplacer { return &CopilotReplacer{} } -func (r *CopilotReplacer) replace(handle string) (string, error) { +func (r *CopilotReplacer) replace(handle string) string { if strings.EqualFold(handle, "@copilot") { - return "copilot-swe-agent", nil + return "copilot-swe-agent" } - return handle, nil + return handle } // Replace replaces usages of `@copilot` in a slice with Copilot's login. -func (r *CopilotReplacer) ReplaceSlice(handles []string) ([]string, error) { +func (r *CopilotReplacer) ReplaceSlice(handles []string) []string { res := make([]string, len(handles)) for i, h := range handles { - var err error - res[i], err = r.replace(h) - if err != nil { - return nil, err - } + res[i] = r.replace(h) } - return res, nil + return res } diff --git a/pkg/cmd/pr/shared/params_test.go b/pkg/cmd/pr/shared/params_test.go index b1e3d32d6..4a353e97e 100644 --- a/pkg/cmd/pr/shared/params_test.go +++ b/pkg/cmd/pr/shared/params_test.go @@ -233,8 +233,7 @@ func TestCopilotReplacer_ReplaceSlice(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := NewCopilotReplacer() - got, err := r.ReplaceSlice(tt.args.handles) - require.NoError(t, err) + got := r.ReplaceSlice(tt.args.handles) require.Equal(t, tt.want, got) }) } From debf0bbaf22b7293da97b10d0ad01f1c64e05904 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 09:50:24 -0600 Subject: [PATCH 63/80] test(params): enhance Copilot replacer tests for edge cases - Added tests for handling nil and empty slices in the Copilot replacer. - Simplified test structure by removing unnecessary wantErr field. --- pkg/cmd/pr/shared/params_test.go | 33 ++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/pr/shared/params_test.go b/pkg/cmd/pr/shared/params_test.go index 4a353e97e..53eb6328f 100644 --- a/pkg/cmd/pr/shared/params_test.go +++ b/pkg/cmd/pr/shared/params_test.go @@ -192,42 +192,51 @@ func TestCopilotReplacer_ReplaceSlice(t *testing.T) { handles []string } tests := []struct { - name string - args args - want []string - wantErr bool + name string + args args + want []string }{ { name: "replaces @copilot with copilot-swe-agent", args: args{ handles: []string{"monalisa", "@copilot", "hubot"}, }, - want: []string{"monalisa", "copilot-swe-agent", "hubot"}, - wantErr: false, + want: []string{"monalisa", "copilot-swe-agent", "hubot"}, }, { name: "handles no @copilot mentions", args: args{ handles: []string{"monalisa", "user", "hubot"}, }, - want: []string{"monalisa", "user", "hubot"}, - wantErr: false, + want: []string{"monalisa", "user", "hubot"}, }, { name: "replaces multiple @copilot mentions", args: args{ handles: []string{"@copilot", "user", "@copilot"}, }, - want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"}, - wantErr: false, + want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"}, }, { name: "handles @copilot case-insensitively", args: args{ handles: []string{"@Copilot", "user", "@CoPiLoT"}, }, - want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"}, - wantErr: false, + want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"}, + }, + { + name: "handles nil slice", + args: args{ + handles: nil, + }, + want: []string{}, + }, + { + name: "handles empty slice", + args: args{ + handles: []string{}, + }, + want: []string{}, }, } for _, tt := range tests { From 532aa1c32a8602653f2e12e665e3b50f66d57714 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 09:55:17 -0600 Subject: [PATCH 64/80] doc(issue/pr edit): doc `@copilot` assignee --- pkg/cmd/issue/edit/edit.go | 6 ++++++ pkg/cmd/pr/edit/edit.go | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 8fc8a2e41..36260cb1b 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -60,11 +60,17 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman Editing issues' projects requires authorization with the %[1]sproject%[1]s scope. To authorize, run %[1]sgh auth refresh -s project%[1]s. + + The %[1]s--add-assignee%[1]s and %[1]s--remove-assignee%[1]s flags both support + the following special values: + - %[1]s@me%[1]s: assign or unassign yourself + - %[1]s@copilot%[1]s: assign or unassign Copilot `, "`"), Example: heredoc.Doc(` $ gh issue edit 23 --title "I found a bug" --body "Nothing works" $ gh issue edit 23 --add-label "bug,help wanted" --remove-label "core" $ gh issue edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot + $ gh issue edit 23 --add-assignee "@copilot" $ gh issue edit 23 --add-project "Roadmap" --remove-project v1,v2 $ gh issue edit 23 --milestone "Version 1" $ gh issue edit 23 --remove-milestone diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 155f08897..92f40645e 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -60,12 +60,18 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman Editing a pull request's projects requires authorization with the %[1]sproject%[1]s scope. To authorize, run %[1]sgh auth refresh -s project%[1]s. + + The %[1]s--add-assignee%[1]s and %[1]s--remove-assignee%[1]s flags both support + the following special values: + - %[1]s@me%[1]s: assign or unassign yourself + - %[1]s@copilot%[1]s: assign or unassign Copilot `, "`"), Example: heredoc.Doc(` $ gh pr edit 23 --title "I found a bug" --body "Nothing works" $ gh pr edit 23 --add-label "bug,help wanted" --remove-label "core" $ gh pr edit 23 --add-reviewer monalisa,hubot --remove-reviewer myorg/team-name $ gh pr edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot + $ gh pr edit 23 --add-assignee "@copilot" $ gh pr edit 23 --add-project "Roadmap" --remove-project v1,v2 $ gh pr edit 23 --milestone "Version 1" $ gh pr edit 23 --remove-milestone From 748afb6e85cf56bd6ad4ae6f348dcfd7d105192f Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 12:18:23 -0600 Subject: [PATCH 65/80] doc(issue/pr edit): clarify @copilot usage --- pkg/cmd/issue/edit/edit.go | 2 +- pkg/cmd/pr/edit/edit.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 36260cb1b..e959cde2b 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -64,7 +64,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman The %[1]s--add-assignee%[1]s and %[1]s--remove-assignee%[1]s flags both support the following special values: - %[1]s@me%[1]s: assign or unassign yourself - - %[1]s@copilot%[1]s: assign or unassign Copilot + - %[1]s@copilot%[1]s: assign or unassign Copilot (not supported on GitHub Enterprise Server) `, "`"), Example: heredoc.Doc(` $ gh issue edit 23 --title "I found a bug" --body "Nothing works" diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 92f40645e..c0dc61199 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -64,7 +64,10 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman The %[1]s--add-assignee%[1]s and %[1]s--remove-assignee%[1]s flags both support the following special values: - %[1]s@me%[1]s: assign or unassign yourself - - %[1]s@copilot%[1]s: assign or unassign Copilot + - %[1]s@copilot%[1]s: assign or unassign Copilot (not supported on GitHub Enterprise Server) + + The %[1]s--add-reviewer%[1]s and %[1]s--remove-reviewer%[1]s flags do not support + these special values. `, "`"), Example: heredoc.Doc(` $ gh pr edit 23 --title "I found a bug" --body "Nothing works" From a28e2d87840f275a2c4e1d216bce06aeabb7b011 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 12:28:46 -0600 Subject: [PATCH 66/80] refactor(api): use constant for Copilot login --- api/queries_repo.go | 7 ++++++- pkg/cmd/pr/shared/params.go | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index f72277fb2..28d4374b7 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -1183,6 +1183,11 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) return projects, nil } +// Expected login for Copilot when retrieved as an Actor +// This is returned from assignable actors and issue/pr assigned actors. +// We use this to check if the actor is Copilot. +var CopilotActorLogin = "copilot-swe-agent" + type AssignableActor interface { DisplayName() string ID() string @@ -1241,7 +1246,7 @@ func NewAssignableBot(id, login string) AssignableBot { } func (b AssignableBot) DisplayName() string { - if b.login == "copilot-swe-agent" { + if b.login == CopilotActorLogin { return "Copilot (AI)" } return b.Login() diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index e53ad7a06..ba9901f77 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -322,7 +322,7 @@ func NewCopilotReplacer() *CopilotReplacer { func (r *CopilotReplacer) replace(handle string) string { if strings.EqualFold(handle, "@copilot") { - return "copilot-swe-agent" + return api.CopilotActorLogin } return handle } From 6f1906073f2281ae1a0ce63566d9c5911f5d19b5 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 12:51:09 -0600 Subject: [PATCH 67/80] doc(params): incorrect func name in comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cmd/pr/shared/params.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index ba9901f77..1fa45652a 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -327,7 +327,7 @@ func (r *CopilotReplacer) replace(handle string) string { return handle } -// Replace replaces usages of `@copilot` in a slice with Copilot's login. +// ReplaceSlice replaces usages of `@copilot` in a slice with Copilot's login. func (r *CopilotReplacer) ReplaceSlice(handles []string) []string { res := make([]string, len(handles)) for i, h := range handles { From 53d0d5d192dcb3f0edf16b70bc1586b6ea71faa1 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 12:53:56 -0600 Subject: [PATCH 68/80] fix(editable): include DefaultLogins in EditableAssignees clone --- pkg/cmd/pr/shared/editable.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index c71f89b52..2f51f2ae8 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -292,6 +292,7 @@ func (ea *EditableAssignees) clone() EditableAssignees { return EditableAssignees{ EditableSlice: ea.EditableSlice.clone(), ActorAssignees: ea.ActorAssignees, + DefaultLogins: ea.DefaultLogins, } } From e0f3f0f2fde2f0ae0695d53f0b00e8f15006dd47 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 14:57:49 -0600 Subject: [PATCH 69/80] refactor(api): change CopilotActorLogin to constant --- api/queries_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 28d4374b7..2b2a1eb96 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -1186,7 +1186,7 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) // Expected login for Copilot when retrieved as an Actor // This is returned from assignable actors and issue/pr assigned actors. // We use this to check if the actor is Copilot. -var CopilotActorLogin = "copilot-swe-agent" +const CopilotActorLogin = "copilot-swe-agent" type AssignableActor interface { DisplayName() string From 0788a015176a73adbc5db76a82b6f854a0480742 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 16 May 2025 15:17:39 -0600 Subject: [PATCH 70/80] refactor(api): inline struct definitions in RepoAssignableActors --- api/queries_repo.go | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 2b2a1eb96..efbcfcb19 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -1319,25 +1319,21 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]AssignableUse // RepoAssignableActors fetches all the assignable actors for a repository on // GitHub hosts that support Actor assignees. func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]AssignableActor, error) { - type assignableUser struct { - ID string - Login string - Name string - TypeName string `graphql:"__typename"` - } - - type assignableBot struct { - ID string - Login string - TypeName string `graphql:"__typename"` - } - type responseData struct { Repository struct { SuggestedActors struct { Nodes []struct { - User assignableUser `graphql:"... on User"` - Bot assignableBot `graphql:"... on Bot"` + User struct { + ID string + Login string + Name string + TypeName string `graphql:"__typename"` + } `graphql:"... on User"` + Bot struct { + ID string + Login string + TypeName string `graphql:"__typename"` + } `graphql:"... on Bot"` } PageInfo struct { HasNextPage bool From b639d6d5149c9f1fc61837b92c26e0bc7dff0ad7 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 20 May 2025 08:37:01 -0600 Subject: [PATCH 71/80] doc(pr): format allowed values and defaults in help --- pkg/cmd/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 66051f83a..92d0ca1ba 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -20,10 +20,10 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command { for _, co := range config.Options { longDoc.WriteString(fmt.Sprintf("- `%s`: %s", co.Key, co.Description)) if len(co.AllowedValues) > 0 { - longDoc.WriteString(fmt.Sprintf(" {%s}", strings.Join(co.AllowedValues, "|"))) + longDoc.WriteString(fmt.Sprintf(" `{%s}`", strings.Join(co.AllowedValues, " | "))) } if co.DefaultValue != "" { - longDoc.WriteString(fmt.Sprintf(" (default %s)", co.DefaultValue)) + longDoc.WriteString(fmt.Sprintf(" (default `%s`)", co.DefaultValue)) } longDoc.WriteRune('\n') } From 944543863af83332ffaea84692eaec130a89a9cb Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 20 May 2025 08:54:30 -0600 Subject: [PATCH 72/80] Revert "[gh config] Escape pipe symbol in Long desc for website manual" --- internal/docs/markdown.go | 3 +-- pkg/cmd/config/config.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/docs/markdown.go b/internal/docs/markdown.go index 05d8686b8..7ae8c6862 100644 --- a/internal/docs/markdown.go +++ b/internal/docs/markdown.go @@ -142,8 +142,7 @@ func genMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine()) } if hasLong { - longWithEscapedPipe := strings.ReplaceAll(cmd.Long, "|", "|") - fmt.Fprintf(w, "%s\n\n", longWithEscapedPipe) + fmt.Fprintf(w, "%s\n\n", cmd.Long) } for _, g := range root.GroupedCommands(cmd) { diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 66051f83a..2661c3369 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -16,7 +16,7 @@ import ( func NewCmdConfig(f *cmdutil.Factory) *cobra.Command { longDoc := strings.Builder{} longDoc.WriteString("Display or change configuration settings for gh.\n\n") - longDoc.WriteString("Current respected settings:\n\n") + longDoc.WriteString("Current respected settings:\n") for _, co := range config.Options { longDoc.WriteString(fmt.Sprintf("- `%s`: %s", co.Key, co.Description)) if len(co.AllowedValues) > 0 { From 53d589223bed7f801d8b2fed67891d87acdebe18 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 21 May 2025 12:17:05 +0100 Subject: [PATCH 73/80] test: ensure proper usage of pipes in docs Signed-off-by: Babak K. Shandiz --- pkg/cmd/root/help_test.go | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/pkg/cmd/root/help_test.go b/pkg/cmd/root/help_test.go index d24a84a0d..de82479a8 100644 --- a/pkg/cmd/root/help_test.go +++ b/pkg/cmd/root/help_test.go @@ -2,6 +2,18 @@ package root import ( "testing" + + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" ) func TestDedent(t *testing.T) { @@ -44,3 +56,67 @@ func TestDedent(t *testing.T) { } } } + +// Since our online docs website renders pages by using the kramdown (a superset +// of Markdown) engine, we have to check against some known quirks of the +// syntax. +func TestKramdownCompatibleDocs(t *testing.T) { + ios, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: ios, + Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, + Browser: &browser.Stub{}, + ExtensionManager: &extensions.ExtensionManagerMock{ + ListFunc: func() []extensions.Extension { + return nil + }, + }, + } + + cmd, err := NewCmdRoot(f, "N/A", "") + require.NoError(t, err) + + var walk func(*cobra.Command) + walk = func(cmd *cobra.Command) { + t.Run(cmd.UseLine(), func(t *testing.T) { + assertProperUsageOfPipes(t, cmd) + }) + for _, child := range cmd.Commands() { + walk(child) + } + } + + walk(cmd) +} + +// If not in a code block, kramdown treats pipes ("|") as table column +// separators, even if there's no table header, or left/right table row borders +// (i.e. lines starting and ending with a pipe). +// +// We need to assert there's no pipe in the text unless it's in a code-block or +// code-span. +// +// (See https://github.com/cli/cli/issues/10348) +func assertProperUsageOfPipes(t *testing.T, cmd *cobra.Command) { + md := goldmark.New() + reader := text.NewReader([]byte(cmd.Long)) + doc := md.Parser().Parse(reader) + + var checkNode func(node ast.Node) + checkNode = func(node ast.Node) { + if node.Kind() == ast.KindCodeSpan || node.Kind() == ast.KindCodeBlock { + return + } + + if node.Kind() == ast.KindText { + text := string(node.(*ast.Text).Segment.Value(reader.Source())) + require.NotContains(t, text, "|", `found pipe ("|") in plain text in %q docs`, cmd.CommandPath()) + } + + for child := node.FirstChild(); child != nil; child = child.NextSibling() { + checkNode(child) + } + } + + checkNode(doc) +} From 34431ab07486a180bd85cfd6bc976f891deb1614 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 21 May 2025 12:20:08 +0100 Subject: [PATCH 74/80] chore: run `go mod tidy` Signed-off-by: Babak K. Shandiz --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index bf8b033fe..f95c8a7c2 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 + github.com/yuin/goldmark v1.7.8 github.com/zalando/go-keyring v0.2.5 golang.org/x/crypto v0.37.0 golang.org/x/sync v0.13.0 @@ -171,7 +172,6 @@ require ( github.com/transparency-dev/merkle v0.0.2 // indirect github.com/vbatts/tar-split v0.11.6 // 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 From 960aadf2b9a9e2005d599801ff15be27451d8c40 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 22 May 2025 17:04:04 +0100 Subject: [PATCH 75/80] test: improve test case naming Signed-off-by: Babak K. Shandiz --- pkg/cmd/root/help_test.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/root/help_test.go b/pkg/cmd/root/help_test.go index de82479a8..40f333159 100644 --- a/pkg/cmd/root/help_test.go +++ b/pkg/cmd/root/help_test.go @@ -1,6 +1,7 @@ package root import ( + "fmt" "testing" "github.com/cli/cli/v2/internal/browser" @@ -78,8 +79,9 @@ func TestKramdownCompatibleDocs(t *testing.T) { var walk func(*cobra.Command) walk = func(cmd *cobra.Command) { - t.Run(cmd.UseLine(), func(t *testing.T) { - assertProperUsageOfPipes(t, cmd) + name := fmt.Sprintf("%q: test pipes are in code blocks", cmd.UseLine()) + t.Run(name, func(t *testing.T) { + assertPipesAreInCodeBlocks(t, cmd) }) for _, child := range cmd.Commands() { walk(child) @@ -89,15 +91,15 @@ func TestKramdownCompatibleDocs(t *testing.T) { walk(cmd) } -// If not in a code block, kramdown treats pipes ("|") as table column -// separators, even if there's no table header, or left/right table row borders -// (i.e. lines starting and ending with a pipe). +// If not in a code block or a code span, kramdown treats pipes ("|") as table +// column separators, even if there's no table header, or left/right table row +// borders (i.e. lines starting and ending with a pipe). // // We need to assert there's no pipe in the text unless it's in a code-block or // code-span. // // (See https://github.com/cli/cli/issues/10348) -func assertProperUsageOfPipes(t *testing.T, cmd *cobra.Command) { +func assertPipesAreInCodeBlocks(t *testing.T, cmd *cobra.Command) { md := goldmark.New() reader := text.NewReader([]byte(cmd.Long)) doc := md.Parser().Parse(reader) From 1e6a2b1affb7d6a7850f4bc0e7a43cf14b80caf5 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 23 May 2025 14:55:41 +0500 Subject: [PATCH 76/80] Add `--compact` flag to `run watch` (#10629) * [gh run watch] Support `--compact` flag * [gh run watch] Support `--compact` flag * Add changes for updated AC * Incorporate review changes * docs(run watch): fill up the line to 80 chars Signed-off-by: Babak K. Shandiz --------- Signed-off-by: Babak K. Shandiz Co-authored-by: Babak K. Shandiz --- pkg/cmd/run/shared/presentation.go | 42 ++++++++++++++++++++++++++++++ pkg/cmd/run/watch/watch.go | 15 +++++++++-- pkg/cmd/run/watch/watch_test.go | 9 +++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/run/shared/presentation.go b/pkg/cmd/run/shared/presentation.go index a3556d743..983979d8a 100644 --- a/pkg/cmd/run/shared/presentation.go +++ b/pkg/cmd/run/shared/presentation.go @@ -47,6 +47,48 @@ func RenderJobs(cs *iostreams.ColorScheme, jobs []Job, verbose bool) string { return strings.Join(lines, "\n") } +func RenderJobsCompact(cs *iostreams.ColorScheme, jobs []Job) string { + lines := []string{} + for _, job := range jobs { + elapsed := job.CompletedAt.Sub(job.StartedAt) + elapsedStr := fmt.Sprintf(" in %s", elapsed) + if elapsed < 0 { + elapsedStr = "" + } + symbol, symbolColor := Symbol(cs, job.Status, job.Conclusion) + id := cs.Cyanf("%d", job.ID) + lines = append(lines, fmt.Sprintf("%s %s%s (ID %s)", symbolColor(symbol), cs.Bold(job.Name), elapsedStr, id)) + + if job.Status == Completed && job.Conclusion == Success { + continue + } + + var inProgressStepLine string + var failedStepLines []string + + for _, step := range job.Steps { + stepSymbol, stepSymColor := Symbol(cs, step.Status, step.Conclusion) + stepLine := fmt.Sprintf(" %s %s", stepSymColor(stepSymbol), step.Name) + + if IsFailureState(step.Conclusion) { + failedStepLines = append(failedStepLines, stepLine) + } + + if step.Status == InProgress { + inProgressStepLine = stepLine + } + } + + lines = append(lines, failedStepLines...) + + if inProgressStepLine != "" { + lines = append(lines, inProgressStepLine) + } + } + + return strings.Join(lines, "\n") +} + func RenderAnnotations(cs *iostreams.ColorScheme, annotations []Annotation) string { lines := []string{} diff --git a/pkg/cmd/run/watch/watch.go b/pkg/cmd/run/watch/watch.go index 57be01f64..a73a91e1a 100644 --- a/pkg/cmd/run/watch/watch.go +++ b/pkg/cmd/run/watch/watch.go @@ -28,6 +28,7 @@ type WatchOptions struct { RunID string Interval int ExitStatus bool + Compact bool Prompt bool @@ -48,6 +49,9 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm Long: heredoc.Docf(` Watch a run until it completes, showing its progress. + By default, all steps are displayed. The %[1]s--compact%[1]s option can be used to only + show the relevant/failed steps. + 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. `, "`"), @@ -55,6 +59,9 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm # Watch a run until it's done $ gh run watch + # Watch a run in compact mode + $ gh run watch --compact + # Run some other command when the run is finished $ gh run watch && notify-send 'run is done!' `), @@ -78,6 +85,7 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm }, } cmd.Flags().BoolVar(&opts.ExitStatus, "exit-status", false, "Exit with non-zero status if run fails") + cmd.Flags().BoolVar(&opts.Compact, "compact", false, "Show only relevant/failed steps") cmd.Flags().IntVarP(&opts.Interval, "interval", "i", defaultInterval, "Refresh interval in seconds") return cmd @@ -252,8 +260,11 @@ func renderRun(out io.Writer, opts WatchOptions, client *api.Client, repo ghrepo } fmt.Fprintln(out, cs.Bold("JOBS")) - - fmt.Fprintln(out, shared.RenderJobs(cs, jobs, true)) + if opts.Compact { + fmt.Fprintln(out, shared.RenderJobsCompact(cs, jobs)) + } else { + fmt.Fprintln(out, shared.RenderJobs(cs, jobs, true)) + } if missingAnnotationsPermissions { fmt.Fprintln(out) diff --git a/pkg/cmd/run/watch/watch_test.go b/pkg/cmd/run/watch/watch_test.go index 49e56217b..1471f64cf 100644 --- a/pkg/cmd/run/watch/watch_test.go +++ b/pkg/cmd/run/watch/watch_test.go @@ -57,6 +57,15 @@ func TestNewCmdWatch(t *testing.T) { ExitStatus: true, }, }, + { + name: "compact status", + cli: "1234 --compact", + wants: WatchOptions{ + Interval: defaultInterval, + RunID: "1234", + Compact: true, + }, + }, } for _, tt := range tests { From 9594937d589b1c7d38a7273f58a1180823d48725 Mon Sep 17 00:00:00 2001 From: Han Date: Sun, 25 May 2025 18:45:56 +0900 Subject: [PATCH 77/80] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cefe1abb0..8257cf566 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ For [installation options see below](#installation), for usage instructions [see ## Contributing -If anything feels off, or if you feel that some functionality is missing, please check out the [contributing page][contributing]. There you will find instructions for sharing your feedback, building the tool locally, and submitting pull requests to the project. +If anything feels off or if you feel that some functionality is missing, please check out the [contributing page][contributing]. There you will find instructions for sharing your feedback, building the tool locally, and submitting pull requests to the project. If you are a hubber and are interested in shipping new commands for the CLI, check out our [doc on internal contributions][intake-doc]. @@ -58,7 +58,7 @@ Additional Conda installation options available on the [gh-feedstock page](https | ----------------------------------- | ---------------- | | `curl -sS https://webi.sh/gh \| sh` | `webi gh@stable` | -For more information about the Webi installer see [its homepage](https://webinstall.dev/). +For more information about the Webi installer, see [its homepage](https://webinstall.dev/). #### Flox @@ -127,9 +127,9 @@ Download packaged binaries from the [releases page][]. #### Verification of binaries -Since version 2.50.0 `gh` has been producing [Build Provenance Attestation](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/) enabling a cryptographically verifiable paper-trail back to the origin GitHub repository, git revision and build instructions used. The build provenance attestations are signed and relies on Public Good [Sigstore](https://www.sigstore.dev/) for PKI. +Since version 2.50.0, `gh` has been producing [Build Provenance Attestation](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/), enabling a cryptographically verifiable paper-trail back to the origin GitHub repository, git revision, and build instructions used. The build provenance attestations are signed and rely on Public Good [Sigstore](https://www.sigstore.dev/) for PKI. -There are two common ways to verify a downloaded release, depending if `gh` is already installed or not. If `gh` is installed, it's trivial to verify a new release: +There are two common ways to verify a downloaded release, depending on whether `gh` is already installed or not. If `gh` is installed, it's trivial to verify a new release: - **Option 1: Using `gh` if already installed:** From 1ebed2678ad621054727cd0088945f72c38de7a3 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 27 May 2025 09:27:12 -0600 Subject: [PATCH 78/80] update sigstore-go to v1 Signed-off-by: Meredith Lancaster --- go.mod | 37 +++++------ go.sum | 190 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 115 insertions(+), 112 deletions(-) diff --git a/go.mod b/go.mod index 3d572a349..108c5722d 100644 --- a/go.mod +++ b/go.mod @@ -44,17 +44,17 @@ require ( github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc github.com/sigstore/protobuf-specs v0.4.1 - github.com/sigstore/sigstore-go v0.7.2 + github.com/sigstore/sigstore-go v1.0.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 github.com/yuin/goldmark v1.7.8 github.com/zalando/go-keyring v0.2.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.1 + golang.org/x/crypto v0.38.0 + golang.org/x/sync v0.14.0 + golang.org/x/term v0.32.0 + golang.org/x/text v0.25.0 + google.golang.org/grpc v1.72.0 google.golang.org/protobuf v1.36.6 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 @@ -73,6 +73,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/catppuccin/go v0.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // 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 @@ -155,9 +156,9 @@ require ( github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect - github.com/sigstore/rekor v1.3.9 // indirect - github.com/sigstore/sigstore v1.9.1 // indirect - github.com/sigstore/timestamp-authority v1.2.5 // indirect + github.com/sigstore/rekor v1.3.10 // indirect + github.com/sigstore/sigstore v1.9.4 // indirect + github.com/sigstore/timestamp-authority v1.2.7 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect @@ -166,7 +167,7 @@ require ( 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 - github.com/theupdateframework/go-tuf/v2 v2.0.2 // indirect + github.com/theupdateframework/go-tuf/v2 v2.1.1 // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect @@ -175,17 +176,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.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.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/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect golang.org/x/tools v0.29.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 + google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect k8s.io/klog/v2 v2.130.1 // indirect ) diff --git a/go.sum b/go.sum index 0b00af3b2..8d50af689 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,17 @@ -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 v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= +cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU= +cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 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.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= +cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs= +cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo= +cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE= +cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w= +cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw= +cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= 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= @@ -20,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.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/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= 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/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/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= @@ -58,10 +58,10 @@ 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/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= +github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 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= @@ -74,14 +74,14 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b 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/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/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= @@ -100,6 +100,8 @@ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/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.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= @@ -227,8 +229,8 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ 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-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= +github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 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= @@ -253,8 +255,6 @@ 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= -github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= 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= @@ -415,8 +415,8 @@ 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.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= -github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= @@ -457,22 +457,22 @@ github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZV github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/sigstore/protobuf-specs v0.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.2 h1:CN4xPasChSEb0QBMxMW5dLcXdA9KD4QiRyVnMkhXj6U= -github.com/sigstore/sigstore-go v0.7.2/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/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU= +github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A= +github.com/sigstore/sigstore v1.9.4 h1:64+OGed80+A4mRlNzRd055vFcgBeDghjZw24rPLZgDU= +github.com/sigstore/sigstore v1.9.4/go.mod h1:Q7tGTC3gbtK7c3jcxEmGc2MmK4rRpIRzi3bxRFWKvEY= +github.com/sigstore/sigstore-go v1.0.0 h1:4N07S2zLxf09nTRwaPKyAxbKzpM8WJYUS8lWWaYxneU= +github.com/sigstore/sigstore-go v1.0.0/go.mod h1:UYsZ/XHE4eltv1o1Lu+n6poW1Z5to3f0+emvfXNxIN8= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.4 h1:kQqUJ1VuWdJltMkinFXAHTlJrzMRPoNgL+dy6WyJ/dA= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.4/go.mod h1:9miLz7c69vj/7VH7UpCKHDia41HCTIDJWJWf4Ex5yUk= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.4 h1:MHRm7YQuF4zFyoXRLgUdLaNxqVO6JlLGnkDUI9fm9ow= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.4/go.mod h1:899VNYSSnQ0QtcuhkW0gznzxn0cqhowTL3nzc/xnym8= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.4 h1:C2nSyTmTxpuamUmLCWWZwz+0Y1IQIig9XwAJ4UAn/SI= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.4/go.mod h1:vjDahU0sEw/WMkKkygZNH72EMg86iaFNLAaJFXhItXU= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.4 h1:t9yfb6yteIDv8CNRT6OHdqgTV6TSj+CdOtZP9dVhpsQ= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.4/go.mod h1:m7sQxVJmDa+rsmS1m6biQxaLX83pzNS7ThUEyjOqkCU= +github.com/sigstore/timestamp-authority v1.2.7 h1:HP/VT4wnL4uzP0fVo3eHXlt0reuNgW3PLt78+BV0I5I= +github.com/sigstore/timestamp-authority v1.2.7/go.mod h1:te4ThQ3Q/CX1bzVsf5mMN0K7Z/cgc2OcoEGxAJiFqqI= 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= @@ -499,16 +499,18 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= -github.com/theupdateframework/go-tuf/v2 v2.0.2 h1:PyNnjV9BJNzN1ZE6BcWK+5JbF+if370jjzO84SS+Ebo= -github.com/theupdateframework/go-tuf/v2 v2.0.2/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA= +github.com/theupdateframework/go-tuf/v2 v2.1.1 h1:OWcoHItwsGO+7m0wLa7FDWPR4oB1cj0zOr1kosE4G+I= +github.com/theupdateframework/go-tuf/v2 v2.1.1/go.mod h1:V675cQGhZONR0OGQ8r1feO0uwtsTBYPDWHzAAPn5rjE= github.com/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/tink-crypto/tink-go-hcvault/v2 v2.3.0 h1:6nAX1aRGnkg2SEUMwO5toB2tQkP0Jd6cbmZ/K5Le1V0= +github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0/go.mod h1:HOC5NWW1wBI2Vke1FGcRBvDATkEYE7AUDiYbXqi2sBw= +github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4dtyHNh0= +github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw= 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= @@ -529,22 +531,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.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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 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.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.step.sm/crypto v0.63.0 h1:U1QGELQqJ85oDfeNFE2V52cow1rvy0m3MekG3wFmyXY= +go.step.sm/crypto v0.63.0/go.mod h1:aj3LETmCZeSil1DMq3BlbhDBcN86+mmKrHZtXWyc0L4= 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= @@ -553,24 +555,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.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/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg= +golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -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/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.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= @@ -581,19 +583,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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 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= @@ -602,16 +604,16 @@ 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.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc= -google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= +google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM= +google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ= 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.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= -google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY= +google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 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= From dcca4b2940657dc044302ecc158c14364e30fe99 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 27 May 2025 09:34:35 -0600 Subject: [PATCH 79/80] replace deprecated type Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/sigstore.go | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index 190ea5c0f..995771d55 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -46,9 +46,9 @@ type SigstoreVerifier interface { type LiveSigstoreVerifier struct { Logger *io.Handler NoPublicGood bool - PublicGood *verify.SignedEntityVerifier - GitHub *verify.SignedEntityVerifier - Custom map[string]*verify.SignedEntityVerifier + PublicGood *verify.Verifier + GitHub *verify.Verifier + Custom map[string]*verify.Verifier } var ErrNoAttestationsVerified = errors.New("no attestations were verified") @@ -86,13 +86,13 @@ func NewLiveSigstoreVerifier(config SigstoreConfig) (*LiveSigstoreVerifier, erro return liveVerifier, nil } -func createCustomVerifiers(trustedRoot string, noPublicGood bool) (map[string]*verify.SignedEntityVerifier, error) { +func createCustomVerifiers(trustedRoot string, noPublicGood bool) (map[string]*verify.Verifier, error) { customTrustRoots, err := os.ReadFile(trustedRoot) if err != nil { return nil, fmt.Errorf("unable to read file %s: %v", trustedRoot, err) } - verifiers := make(map[string]*verify.SignedEntityVerifier) + verifiers := make(map[string]*verify.Verifier) reader := bufio.NewReader(bytes.NewReader(customTrustRoots)) var line []byte var readError error @@ -189,7 +189,7 @@ func getBundleIssuer(b *bundle.Bundle) (string, error) { return leafCert.Issuer.Organization[0], nil } -func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEntityVerifier, error) { +func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.Verifier, error) { // if no custom trusted root is set, return either the Public Good or GitHub verifier // If the chosen verifier has not yet been created, create it as a LiveSigstoreVerifier field for use in future calls if v.Custom != nil { @@ -291,7 +291,7 @@ func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve return results, nil } -func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) { +func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) { // All we know about this trust root is its configuration so make some // educated guesses as to what the policy should be. verifierConfig := []verify.VerifierOption{} @@ -314,7 +314,7 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerif return gv, nil } -func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string]) (*verify.SignedEntityVerifier, error) { +func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string]) (*verify.Verifier, error) { var tr string opts := GitHubTUFOptions(tufMetadataDir) @@ -339,7 +339,7 @@ func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string]) (*ve return newGitHubVerifierWithTrustedRoot(trustedRoot) } -func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) { +func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) { gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1)) if err != nil { return nil, fmt.Errorf("failed to create GitHub verifier: %v", err) @@ -348,7 +348,7 @@ func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Si return gv, nil } -func newPublicGoodVerifier(tufMetadataDir o.Option[string]) (*verify.SignedEntityVerifier, error) { +func newPublicGoodVerifier(tufMetadataDir o.Option[string]) (*verify.Verifier, error) { opts := DefaultOptionsWithCacheSetting(tufMetadataDir) client, err := tuf.New(opts) if err != nil { @@ -362,7 +362,7 @@ func newPublicGoodVerifier(tufMetadataDir o.Option[string]) (*verify.SignedEntit return newPublicGoodVerifierWithTrustedRoot(trustedRoot) } -func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) { +func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) { sv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) if err != nil { return nil, fmt.Errorf("failed to create Public Good verifier: %v", err) From a154ff5cfa220ced0426115909e783b584f43190 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 27 May 2025 09:38:46 -0600 Subject: [PATCH 80/80] replace deprecated func Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/sigstore.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index 995771d55..a8a76e234 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -306,7 +306,7 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1)) } - gv, err := verify.NewSignedEntityVerifier(trustedRoot, verifierConfig...) + gv, err := verify.NewVerifier(trustedRoot, verifierConfig...) if err != nil { return nil, fmt.Errorf("failed to create custom verifier: %v", err) } @@ -340,7 +340,7 @@ func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string]) (*ve } func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) { - gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1)) + gv, err := verify.NewVerifier(trustedRoot, verify.WithSignedTimestamps(1)) if err != nil { return nil, fmt.Errorf("failed to create GitHub verifier: %v", err) } @@ -363,7 +363,7 @@ func newPublicGoodVerifier(tufMetadataDir o.Option[string]) (*verify.Verifier, e } func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) { - sv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) + sv, err := verify.NewVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) if err != nil { return nil, fmt.Errorf("failed to create Public Good verifier: %v", err) }