From 03f1ba75ac5e00f0aa006e0605f52cdc76717e3c Mon Sep 17 00:00:00 2001 From: Kevin Lee <21070577+kevhlee@users.noreply.github.com> Date: Wed, 3 May 2023 14:17:39 -0700 Subject: [PATCH 01/23] Add `alias import` command (#7118) --- pkg/cmd/alias/alias.go | 2 + pkg/cmd/alias/imports/import.go | 195 +++++++++++++ pkg/cmd/alias/imports/import_test.go | 346 +++++++++++++++++++++++ pkg/cmd/alias/set/set.go | 29 +- pkg/cmd/alias/set/set_test.go | 2 +- pkg/cmd/alias/shared/validations.go | 32 +++ pkg/cmd/alias/shared/validations_test.go | 40 +++ 7 files changed, 622 insertions(+), 24 deletions(-) create mode 100644 pkg/cmd/alias/imports/import.go create mode 100644 pkg/cmd/alias/imports/import_test.go create mode 100644 pkg/cmd/alias/shared/validations.go create mode 100644 pkg/cmd/alias/shared/validations_test.go diff --git a/pkg/cmd/alias/alias.go b/pkg/cmd/alias/alias.go index 46d7e2bc8..713e7d1db 100644 --- a/pkg/cmd/alias/alias.go +++ b/pkg/cmd/alias/alias.go @@ -3,6 +3,7 @@ package alias import ( "github.com/MakeNowJust/heredoc" deleteCmd "github.com/cli/cli/v2/pkg/cmd/alias/delete" + importCmd "github.com/cli/cli/v2/pkg/cmd/alias/imports" listCmd "github.com/cli/cli/v2/pkg/cmd/alias/list" setCmd "github.com/cli/cli/v2/pkg/cmd/alias/set" "github.com/cli/cli/v2/pkg/cmdutil" @@ -23,6 +24,7 @@ func NewCmdAlias(f *cmdutil.Factory) *cobra.Command { cmdutil.DisableAuthCheck(cmd) cmd.AddCommand(deleteCmd.NewCmdDelete(f, nil)) + cmd.AddCommand(importCmd.NewCmdImport(f, nil)) cmd.AddCommand(listCmd.NewCmdList(f, nil)) cmd.AddCommand(setCmd.NewCmdSet(f, nil)) diff --git a/pkg/cmd/alias/imports/import.go b/pkg/cmd/alias/imports/import.go new file mode 100644 index 000000000..f724e2699 --- /dev/null +++ b/pkg/cmd/alias/imports/import.go @@ -0,0 +1,195 @@ +package imports + +import ( + "fmt" + "sort" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmd/alias/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +type ImportOptions struct { + Config func() (config.Config, error) + IO *iostreams.IOStreams + + Filename string + OverwriteExisting bool + + existingCommand func(string) bool +} + +func NewCmdImport(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Command { + opts := &ImportOptions{ + IO: f.IOStreams, + Config: f.Config, + } + + cmd := &cobra.Command{ + Use: "import [ | -]", + Short: "Import aliases from a YAML file", + Long: heredoc.Doc(` + Import aliases from the contents of a YAML file. + + Aliases should be defined as a map in YAML, where the keys represent aliases and + the values represent the corresponding expansions. An example file should look like + the following: + + bugs: issue list --label=bug + igrep: '!gh issue list --label="$1" | grep "$2"' + features: |- + issue list + --label=enhancement + + Use "-" to read aliases (in YAML format) from standard input. + + The output from the gh command "alias list" can be used to produce a YAML file + containing your aliases, which you can use to import them from one machine to + another. Run "gh help alias list" to learn more. + `), + Example: heredoc.Doc(` + # Import aliases from a file + $ gh alias import aliases.yml + + # Import aliases from standard input + $ gh alias import - + `), + Args: func(cmd *cobra.Command, args []string) error { + if len(args) > 1 { + return cmdutil.FlagErrorf("too many arguments") + } + if len(args) == 0 && opts.IO.IsStdinTTY() { + return cmdutil.FlagErrorf("no filename passed and nothing on STDIN") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + opts.Filename = "-" + if len(args) > 0 { + opts.Filename = args[0] + } + + opts.existingCommand = shared.ExistingCommandFunc(f, cmd) + + if runF != nil { + return runF(opts) + } + + return importRun(opts) + }, + } + + cmd.Flags().BoolVar(&opts.OverwriteExisting, "clobber", false, "Overwrite existing aliases of the same name") + + return cmd +} + +func importRun(opts *ImportOptions) error { + cs := opts.IO.ColorScheme() + cfg, err := opts.Config() + if err != nil { + return err + } + + aliasCfg := cfg.Aliases() + + b, err := cmdutil.ReadFile(opts.Filename, opts.IO.In) + if err != nil { + return err + } + + aliasMap := map[string]string{} + if err = yaml.Unmarshal(b, &aliasMap); err != nil { + return err + } + + isTerminal := opts.IO.IsStdoutTTY() + if isTerminal { + if opts.Filename == "-" { + fmt.Fprintf(opts.IO.ErrOut, "- Importing aliases from standard input\n") + } else { + fmt.Fprintf(opts.IO.ErrOut, "- Importing aliases from file %q\n", opts.Filename) + } + } + + var msg strings.Builder + + for _, alias := range getSortedKeys(aliasMap) { + if opts.existingCommand(alias) { + msg.WriteString( + fmt.Sprintf("%s Could not import alias %s: already a gh command\n", + cs.FailureIcon(), + cs.Bold(alias), + ), + ) + + continue + } + + expansion := aliasMap[alias] + + if !(strings.HasPrefix(expansion, "!") || opts.existingCommand(expansion)) { + msg.WriteString( + fmt.Sprintf("%s Could not import alias %s: expansion does not correspond to a gh command\n", + cs.FailureIcon(), + cs.Bold(alias), + ), + ) + + continue + } + + if _, err := aliasCfg.Get(alias); err == nil { + if opts.OverwriteExisting { + aliasCfg.Add(alias, expansion) + + msg.WriteString( + fmt.Sprintf("%s Changed alias %s\n", + cs.WarningIcon(), + cs.Bold(alias), + ), + ) + } else { + msg.WriteString( + fmt.Sprintf("%s Could not import alias %s: name already taken\n", + cs.FailureIcon(), + cs.Bold(alias), + ), + ) + } + } else { + aliasCfg.Add(alias, expansion) + + msg.WriteString( + fmt.Sprintf("%s Added alias %s\n", + cs.SuccessIcon(), + cs.Bold(alias), + ), + ) + } + } + + if err := cfg.Write(); err != nil { + return err + } + + if isTerminal { + fmt.Fprintln(opts.IO.ErrOut, msg.String()) + } + + return nil +} + +func getSortedKeys(m map[string]string) []string { + keys := []string{} + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} diff --git a/pkg/cmd/alias/imports/import_test.go b/pkg/cmd/alias/imports/import_test.go new file mode 100644 index 000000000..8837a52db --- /dev/null +++ b/pkg/cmd/alias/imports/import_test.go @@ -0,0 +1,346 @@ +package imports + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmd/alias/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCmdImport(t *testing.T) { + tests := []struct { + name string + cli string + tty bool + wants ImportOptions + wantsError string + }{ + { + name: "no filename and stdin tty", + cli: "", + tty: true, + wants: ImportOptions{ + Filename: "", + OverwriteExisting: false, + }, + wantsError: "no filename passed and nothing on STDIN", + }, + { + name: "no filename and stdin is not tty", + cli: "", + tty: false, + wants: ImportOptions{ + Filename: "-", + OverwriteExisting: false, + }, + }, + { + name: "stdin arg", + cli: "-", + wants: ImportOptions{ + Filename: "-", + OverwriteExisting: false, + }, + }, + { + name: "multiple filenames", + cli: "aliases1 aliases2", + wants: ImportOptions{ + Filename: "aliases1 aliases2", + OverwriteExisting: false, + }, + wantsError: "too many arguments", + }, + { + name: "clobber flag", + cli: "aliases --clobber", + wants: ImportOptions{ + Filename: "aliases", + OverwriteExisting: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + ios.SetStdinTTY(tt.tty) + f := &cmdutil.Factory{IOStreams: ios} + + argv, err := shlex.Split(tt.cli) + assert.NoError(t, err) + + var gotOpts *ImportOptions + cmd := NewCmdImport(f, func(opts *ImportOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + _, err = cmd.ExecuteC() + if tt.wantsError != "" { + assert.EqualError(t, err, tt.wantsError) + return + } + assert.NoError(t, err) + + assert.Equal(t, tt.wants.Filename, gotOpts.Filename) + assert.Equal(t, tt.wants.OverwriteExisting, gotOpts.OverwriteExisting) + }) + } +} + +func TestImportRun(t *testing.T) { + tmpFile := filepath.Join(t.TempDir(), "aliases.yml") + importFileMsg := fmt.Sprintf(`- Importing aliases from file %q`, tmpFile) + importStdinMsg := "- Importing aliases from standard input" + + tests := []struct { + name string + opts *ImportOptions + stdin string + fileContents string + initConfig string + wantConfig string + wantStderr string + }{ + { + name: "with no existing aliases", + opts: &ImportOptions{ + Filename: tmpFile, + OverwriteExisting: false, + }, + fileContents: heredoc.Doc(` + co: pr checkout + igrep: '!gh issue list --label="$1" | grep "$2"' + `), + wantConfig: heredoc.Doc(` + aliases: + co: pr checkout + igrep: '!gh issue list --label="$1" | grep "$2"' + `), + wantStderr: importFileMsg + "\n✓ Added alias co\n✓ Added alias igrep\n\n", + }, + { + name: "with existing aliases", + opts: &ImportOptions{ + Filename: tmpFile, + OverwriteExisting: false, + }, + fileContents: heredoc.Doc(` + users: |- + api graphql -F name="$1" -f query=' + query ($name: String!) { + user(login: $name) { + name + } + }' + co: pr checkout + `), + initConfig: heredoc.Doc(` + aliases: + igrep: '!gh issue list --label="$1" | grep "$2"' + editor: vim + `), + wantConfig: heredoc.Doc(` + aliases: + igrep: '!gh issue list --label="$1" | grep "$2"' + co: pr checkout + users: |- + api graphql -F name="$1" -f query=' + query ($name: String!) { + user(login: $name) { + name + } + }' + editor: vim + `), + wantStderr: importFileMsg + "\n✓ Added alias co\n✓ Added alias users\n\n", + }, + { + name: "from stdin", + opts: &ImportOptions{ + Filename: "-", + OverwriteExisting: false, + }, + stdin: heredoc.Doc(` + co: pr checkout + features: |- + issue list + --label=enhancement + igrep: '!gh issue list --label="$1" | grep "$2"' + `), + wantConfig: heredoc.Doc(` + aliases: + co: pr checkout + features: |- + issue list + --label=enhancement + igrep: '!gh issue list --label="$1" | grep "$2"' + `), + wantStderr: importStdinMsg + "\n✓ Added alias co\n✓ Added alias features\n✓ Added alias igrep\n\n", + }, + { + name: "already taken aliases", + opts: &ImportOptions{ + Filename: tmpFile, + OverwriteExisting: false, + }, + fileContents: heredoc.Doc(` + co: pr checkout -R cool/repo + igrep: '!gh issue list --label="$1" | grep "$2"' + `), + initConfig: heredoc.Doc(` + aliases: + co: pr checkout + editor: vim + `), + wantConfig: heredoc.Doc(` + aliases: + co: pr checkout + igrep: '!gh issue list --label="$1" | grep "$2"' + editor: vim + `), + wantStderr: importFileMsg + "\nX Could not import alias co: name already taken\n✓ Added alias igrep\n\n", + }, + { + name: "override aliases", + opts: &ImportOptions{ + Filename: tmpFile, + OverwriteExisting: true, + }, + fileContents: heredoc.Doc(` + co: pr checkout -R cool/repo + igrep: '!gh issue list --label="$1" | grep "$2"' + `), + initConfig: heredoc.Doc(` + aliases: + co: pr checkout + editor: vim + `), + wantConfig: heredoc.Doc(` + aliases: + co: pr checkout -R cool/repo + igrep: '!gh issue list --label="$1" | grep "$2"' + editor: vim + `), + wantStderr: importFileMsg + "\n! Changed alias co\n✓ Added alias igrep\n\n", + }, + { + name: "alias is a gh command", + opts: &ImportOptions{ + Filename: tmpFile, + OverwriteExisting: false, + }, + fileContents: heredoc.Doc(` + pr: pr checkout + issue: issue list + api: api graphql + `), + wantStderr: strings.Join( + []string{ + importFileMsg, + "X Could not import alias api: already a gh command", + "X Could not import alias issue: already a gh command", + "X Could not import alias pr: already a gh command\n\n", + }, + "\n", + ), + }, + { + name: "invalid expansion", + opts: &ImportOptions{ + Filename: tmpFile, + OverwriteExisting: false, + }, + fileContents: heredoc.Doc(` + alias1: + alias2: ps checkout + `), + wantStderr: strings.Join( + []string{ + importFileMsg, + "X Could not import alias alias1: expansion does not correspond to a gh command", + "X Could not import alias alias2: expansion does not correspond to a gh command\n\n", + }, + "\n", + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.fileContents != "" { + err := os.WriteFile(tmpFile, []byte(tt.fileContents), 0600) + require.NoError(t, err) + } + + ios, stdin, _, stderr := iostreams.Test() + ios.SetStdinTTY(true) + ios.SetStdoutTTY(true) + ios.SetStderrTTY(true) + tt.opts.IO = ios + + readConfigs := config.StubWriteConfig(t) + cfg := config.NewFromString(tt.initConfig) + tt.opts.Config = func() (config.Config, error) { + return cfg, nil + } + + // Create fake command factory for testing. + f := &cmdutil.Factory{ + IOStreams: ios, + ExtensionManager: &extensions.ExtensionManagerMock{ + ListFunc: func() []extensions.Extension { + return []extensions.Extension{} + }, + }, + } + + // Create fake command structure for testing. + rootCmd := &cobra.Command{} + prCmd := &cobra.Command{Use: "pr"} + prCmd.AddCommand(&cobra.Command{Use: "checkout"}) + prCmd.AddCommand(&cobra.Command{Use: "status"}) + rootCmd.AddCommand(prCmd) + issueCmd := &cobra.Command{Use: "issue"} + issueCmd.AddCommand(&cobra.Command{Use: "list"}) + rootCmd.AddCommand(issueCmd) + apiCmd := &cobra.Command{Use: "api"} + apiCmd.AddCommand(&cobra.Command{Use: "graphql"}) + rootCmd.AddCommand(apiCmd) + + tt.opts.existingCommand = shared.ExistingCommandFunc(f, rootCmd) + + if tt.stdin != "" { + stdin.WriteString(tt.stdin) + } + + err := importRun(tt.opts) + require.NoError(t, err) + + configOut := bytes.Buffer{} + readConfigs(&configOut, io.Discard) + + assert.Equal(t, tt.wantStderr, stderr.String()) + assert.Equal(t, tt.wantConfig, configOut.String()) + }) + } +} diff --git a/pkg/cmd/alias/set/set.go b/pkg/cmd/alias/set/set.go index 6d09787a4..871737881 100644 --- a/pkg/cmd/alias/set/set.go +++ b/pkg/cmd/alias/set/set.go @@ -7,9 +7,9 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmd/alias/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/google/shlex" "github.com/spf13/cobra" ) @@ -21,7 +21,7 @@ type SetOptions struct { Expansion string IsShell bool - validCommand func(string) bool + existingCommand func(string) bool } func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command { @@ -70,29 +70,12 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command opts.Name = args[0] opts.Expansion = args[1] - opts.validCommand = func(args string) bool { - split, err := shlex.Split(args) - if err != nil { - return false - } - - rootCmd := cmd.Root() - cmd, _, err := rootCmd.Traverse(split) - if err == nil && cmd != rootCmd { - return true - } - - for _, ext := range f.ExtensionManager.List() { - if ext.Name() == split[0] { - return true - } - } - return false - } + opts.existingCommand = shared.ExistingCommandFunc(f, cmd) if runF != nil { return runF(opts) } + return setRun(opts) }, } @@ -127,11 +110,11 @@ func setRun(opts *SetOptions) error { } isShell = strings.HasPrefix(expansion, "!") - if opts.validCommand(opts.Name) { + if opts.existingCommand(opts.Name) { return fmt.Errorf("could not create alias: %q is already a gh command", opts.Name) } - if !isShell && !opts.validCommand(expansion) { + if !isShell && !opts.existingCommand(expansion) { return fmt.Errorf("could not create alias: %s does not correspond to a gh command", expansion) } diff --git a/pkg/cmd/alias/set/set_test.go b/pkg/cmd/alias/set/set_test.go index 72d6fb413..bec03cba4 100644 --- a/pkg/cmd/alias/set/set_test.go +++ b/pkg/cmd/alias/set/set_test.go @@ -38,7 +38,7 @@ func runCommand(cfg config.Config, isTTY bool, cli string, in string) (*test.Cmd cmd := NewCmdSet(factory, nil) - // fake command nesting structure needed for validCommand + // Create fake command structure for testing. rootCmd := &cobra.Command{} rootCmd.AddCommand(cmd) prCmd := &cobra.Command{Use: "pr"} diff --git a/pkg/cmd/alias/shared/validations.go b/pkg/cmd/alias/shared/validations.go new file mode 100644 index 000000000..6557f66ca --- /dev/null +++ b/pkg/cmd/alias/shared/validations.go @@ -0,0 +1,32 @@ +package shared + +import ( + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/google/shlex" + "github.com/spf13/cobra" +) + +// ExistingCommandFunc returns a function that will check if the given string +// corresponds to an existing command. +func ExistingCommandFunc(f *cmdutil.Factory, cmd *cobra.Command) func(string) bool { + return func(args string) bool { + split, err := shlex.Split(args) + if err != nil || len(split) == 0 { + return false + } + + rootCmd := cmd.Root() + cmd, _, err = rootCmd.Traverse(split) + if err == nil && cmd != rootCmd { + return true + } + + for _, ext := range f.ExtensionManager.List() { + if ext.Name() == split[0] { + return true + } + } + + return false + } +} diff --git a/pkg/cmd/alias/shared/validations_test.go b/pkg/cmd/alias/shared/validations_test.go new file mode 100644 index 000000000..faa0e497d --- /dev/null +++ b/pkg/cmd/alias/shared/validations_test.go @@ -0,0 +1,40 @@ +package shared + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestExistingCommandFunc(t *testing.T) { + // Create fake command factory for testing. + factory := &cmdutil.Factory{ + ExtensionManager: &extensions.ExtensionManagerMock{ + ListFunc: func() []extensions.Extension { + return []extensions.Extension{} + }, + }, + } + + // Create fake command structure for testing. + issueCmd := &cobra.Command{Use: "issue"} + prCmd := &cobra.Command{Use: "pr"} + prCmd.AddCommand(&cobra.Command{Use: "checkout"}) + + cmd := &cobra.Command{} + cmd.AddCommand(prCmd) + cmd.AddCommand(issueCmd) + + f := ExistingCommandFunc(factory, cmd) + + assert.True(t, f("pr")) + assert.True(t, f("pr checkout")) + assert.True(t, f("issue")) + + assert.False(t, f("ps")) + assert.False(t, f("checkout")) + assert.False(t, f("repo list")) +} From d349ba185a5163e4b6c1d89fa5cdf46fcf1a6299 Mon Sep 17 00:00:00 2001 From: vwkd <33468089+vwkd@users.noreply.github.com> Date: Thu, 4 May 2023 01:21:12 +0400 Subject: [PATCH 02/23] fix empty default description for local repository (#7383) --- pkg/cmd/repo/create/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 640fee632..fe2269e7e 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -730,7 +730,7 @@ func interactiveRepoInfo(client *http.Client, hostname string, prompter iprompte name = fmt.Sprintf("%s/%s", owner, name) } - description, err := prompter.Input("Description", defaultName) + description, err := prompter.Input("Description", "") if err != nil { return "", "", "", err } From bf3ffba8ef0dc3fbdbaeb0f8498ad4cd534a379b Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Wed, 3 May 2023 22:36:21 +0100 Subject: [PATCH 03/23] Add PR auto-merge status info to PR queries (#7384) --- api/queries_pr.go | 12 ++++++++++++ api/query_builder.go | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/api/queries_pr.go b/api/queries_pr.go index 87ffd658d..ae8f7c0c6 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -39,6 +39,8 @@ type PullRequest struct { ClosedAt *time.Time MergedAt *time.Time + AutoMergeRequest *AutoMergeRequest + MergeCommit *Commit PotentialMergeCommit *Commit @@ -136,6 +138,16 @@ type PRRepository struct { Name string `json:"name"` } +type AutoMergeRequest struct { + AuthorEmail *string `json:"authorEmail"` + CommitBody *string `json:"commitBody"` + CommitHeadline *string `json:"commitHeadline"` + // MERGE, REBASE, SQUASH + MergeMethod string `json:"mergeMethod"` + EnabledAt time.Time `json:"enabledAt"` + EnabledBy Author `json:"enabledBy"` +} + // Commit loads just the commit SHA and nothing else type Commit struct { OID string `json:"oid"` diff --git a/api/query_builder.go b/api/query_builder.go index 877f79788..3e54f4c60 100644 --- a/api/query_builder.go +++ b/api/query_builder.go @@ -132,6 +132,17 @@ var prCommits = shortenQuery(` } `) +var autoMergeRequest = shortenQuery(` + autoMergeRequest { + authorEmail, + commitBody, + commitHeadline, + mergeMethod, + enabledAt, + enabledBy{login,...on User{id,name}} + } +`) + func StatusCheckRollupGraphQL(after string) string { var afterClause string if after != "" { @@ -231,6 +242,7 @@ var IssueFields = []string{ var PullRequestFields = append(IssueFields, "additions", + "autoMergeRequest", "baseRefName", "changedFiles", "commits", @@ -285,6 +297,8 @@ func IssueGraphQL(fields []string) string { q = append(q, `mergeCommit{oid}`) case "potentialMergeCommit": q = append(q, `potentialMergeCommit{oid}`) + case "autoMergeRequest": + q = append(q, autoMergeRequest) case "comments": q = append(q, issueComments) case "lastComment": // pseudo-field From b01c27aba251c25353b570142d998cfa0ba2c00c Mon Sep 17 00:00:00 2001 From: Josh Kraft <30248392+joshkraft@users.noreply.github.com> Date: Sun, 7 May 2023 15:28:37 -0600 Subject: [PATCH 04/23] Add progress labels for `gh release download` (#7380) --- pkg/cmd/release/download/download.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/release/download/download.go b/pkg/cmd/release/download/download.go index b957ba509..2c7f4a18a 100644 --- a/pkg/cmd/release/download/download.go +++ b/pkg/cmd/release/download/download.go @@ -140,7 +140,7 @@ func downloadRun(opts *DownloadOptions) error { return err } - opts.IO.StartProgressIndicator() + opts.IO.StartProgressIndicatorWithLabel("Finding assets to download") defer opts.IO.StopProgressIndicator() ctx := context.Background() @@ -196,7 +196,7 @@ func downloadRun(opts *DownloadOptions) error { stdout: opts.IO.Out, } - return downloadAssets(&dest, httpClient, toDownload, opts.Concurrency, isArchive) + return downloadAssets(&dest, httpClient, toDownload, opts.Concurrency, isArchive, opts.IO) } func matchAny(patterns []string, name string) bool { @@ -208,7 +208,7 @@ func matchAny(patterns []string, name string) bool { return false } -func downloadAssets(dest *destinationWriter, httpClient *http.Client, toDownload []shared.ReleaseAsset, numWorkers int, isArchive bool) error { +func downloadAssets(dest *destinationWriter, httpClient *http.Client, toDownload []shared.ReleaseAsset, numWorkers int, isArchive bool, io *iostreams.IOStreams) error { if numWorkers == 0 { return errors.New("the number of concurrent workers needs to be greater than 0") } @@ -223,6 +223,7 @@ func downloadAssets(dest *destinationWriter, httpClient *http.Client, toDownload for w := 1; w <= numWorkers; w++ { go func() { for a := range jobs { + io.StartProgressIndicatorWithLabel(fmt.Sprintf("Downloading %s", a.Name)) results <- downloadAsset(dest, httpClient, a.APIURL, a.Name, isArchive) } }() @@ -240,6 +241,8 @@ func downloadAssets(dest *destinationWriter, httpClient *http.Client, toDownload } } + io.StopProgressIndicator() + return downloadError } From 8b987e2deb12b4ace29efb9b4eebc2ebb0428f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 25 Apr 2023 15:36:41 +0200 Subject: [PATCH 05/23] New deployment workflow Add ability to trigger deployments without having to push a git tag --- .github/workflows/deployment.yml | 321 +++++++++++++++++++++++++++++- .github/workflows/releases.yml | 222 --------------------- .gitignore | 2 + .goreleaser.yml | 67 ++++--- Makefile | 24 ++- build/windows/gh.wixproj | 4 - docs/releasing.md | 46 +++-- script/label-assets | 20 ++ script/release | 115 +++++++++++ script/scoop-gen | 31 --- script/sign | 65 ++++++ script/sign-windows-executable.sh | 25 --- script/sign.bat | 8 + script/sign.ps1 | 12 -- 14 files changed, 612 insertions(+), 350 deletions(-) delete mode 100644 .github/workflows/releases.yml create mode 100755 script/label-assets create mode 100755 script/release delete mode 100755 script/scoop-gen create mode 100755 script/sign delete mode 100755 script/sign-windows-executable.sh create mode 100644 script/sign.bat delete mode 100644 script/sign.ps1 diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index c011fc408..8d1cb2169 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -1,7 +1,11 @@ name: Deployment +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + permissions: - contents: read + contents: write on: workflow_dispatch: @@ -9,13 +13,25 @@ on: tag_name: required: true type: string + environment: + default: production + type: environment go_version: default: "1.19" type: string + platforms: + default: "linux,macos,windows" + type: string + release: + description: "Whether to create a GitHub Release" + type: boolean + default: true jobs: linux: runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + if: contains(inputs.platforms, 'linux') steps: - name: Checkout uses: actions/checkout@v3 @@ -23,3 +39,306 @@ jobs: uses: actions/setup-go@v4 with: go-version: ${{ inputs.go_version }} + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + version: "~1.17.1" + install-only: true + - name: Build release binaries + env: + TAG_NAME: ${{ inputs.tag_name }} + run: script/release --local "$TAG_NAME" --platform linux + - name: Generate web manual pages + run: | + go run ./cmd/gen-docs --website --doc-path dist/manual + tar -czvf dist/manual.tar.gz -C dist -- manual + - uses: actions/upload-artifact@v3 + with: + name: linux + if-no-files-found: error + retention-days: 7 + path: | + dist/*.tar.gz + dist/*.rpm + dist/*.deb + + macos: + runs-on: macos-latest + environment: ${{ inputs.environment }} + if: contains(inputs.platforms, 'macos') + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ inputs.go_version }} + - name: Configure macOS signing + if: inputs.environment == 'production' + env: + APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} + APPLE_APPLICATION_CERT: ${{ secrets.APPLE_APPLICATION_CERT }} + APPLE_APPLICATION_CERT_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERT_PASSWORD }} + run: | + keychain="$RUNNER_TEMP/buildagent.keychain" + keychain_password="password1" + + security create-keychain -p "$keychain_password" "$keychain" + security default-keychain -s "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + + base64 -D <<<"$APPLE_APPLICATION_CERT" > "$RUNNER_TEMP/cert.p12" + security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$APPLE_APPLICATION_CERT_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain" + rm "$RUNNER_TEMP/cert.p12" + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + version: "~1.17.1" + install-only: true + - name: Build release binaries + env: + TAG_NAME: ${{ inputs.tag_name }} + APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} + run: script/release --local "$TAG_NAME" --platform macos + - name: Notarize macOS archives + if: inputs.environment == 'production' + env: + APPLE_ID: ${{ vars.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} + run: | + shopt -s failglob + script/sign dist/gh_*_macOS_*.zip + - uses: actions/upload-artifact@v3 + with: + name: macos + if-no-files-found: error + retention-days: 7 + path: | + dist/*.tar.gz + dist/*.zip + + windows: + runs-on: windows-latest + environment: ${{ inputs.environment }} + if: contains(inputs.platforms, 'windows') + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ inputs.go_version }} + - name: Obtain signing certificate + id: obtain_cert + if: inputs.environment == 'production' + shell: bash + run: | + base64 -d <<<"$CERT_CONTENTS" > ./cert.pfx + printf "cert-file=%s\n" ".\\cert.pfx" >> $GITHUB_OUTPUT + env: + CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }} + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + version: "~1.17.1" + install-only: true + - name: Build release binaries + shell: bash + env: + TAG_NAME: ${{ inputs.tag_name }} + CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }} + CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }} + run: script/release --local "$TAG_NAME" --platform windows + - name: Set up MSBuild + id: setupmsbuild + uses: microsoft/setup-msbuild@v1.3.1 + - name: Build MSI + shell: bash + env: + MSBUILD_PATH: ${{ steps.setupmsbuild.outputs.msbuildPath }} + run: | + for ZIP_FILE in dist/gh_*_windows_*.zip; do + MSI_NAME="$(basename "$ZIP_FILE" ".zip")" + MSI_VERSION="$(cut -d_ -f2 <<<"$MSI_NAME" | cut -d- -f1)" + case "$MSI_NAME" in + *_386 ) + source_dir="$PWD/dist/windows_windows_386" + platform="x86" + ;; + *_amd64 ) + source_dir="$PWD/dist/windows_windows_amd64_v1" + platform="x64" + ;; + *_arm64 ) + echo "skipping building MSI for arm64 because WiX 3.11 doesn't support it: https://github.com/wixtoolset/issues/issues/6141" >&2 + continue + #source_dir="$PWD/dist/windows_windows_arm64" + #platform="arm64" + ;; + * ) + printf "unsupported architecture: %s\n" "$MSI_NAME" >&2 + exit 1 + ;; + esac + "${MSBUILD_PATH}\MSBuild.exe" ./build/windows/gh.wixproj -p:SourceDir="$source_dir" -p:OutputPath="$PWD/dist" -p:OutputName="$MSI_NAME" -p:ProductVersion="${MSI_VERSION#v}" -p:Platform="$platform" + done + - name: Sign MSI + if: inputs.environment == 'production' + shell: pwsh + run: | + Get-ChildItem -Path .\dist -Filter *.msi | ForEach-Object { + .\script\sign $_.FullName + } + env: + CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }} + CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }} + - uses: actions/upload-artifact@v3 + with: + name: windows + if-no-files-found: error + retention-days: 7 + path: | + dist/*.zip + dist/*.msi + + release: + runs-on: ubuntu-latest + needs: [linux, macos, windows] + if: inputs.release + steps: + - name: Checkout cli/cli + uses: actions/checkout@v3 + - name: Merge built artifacts + uses: actions/download-artifact@v3 + - name: Checkout documentation site + uses: actions/checkout@v3 + with: + repository: github/cli.github.com + path: site + fetch-depth: 0 + ssh-key: ${{ secrets.SITE_SSH_KEY }} + - name: Update site man pages + env: + GIT_COMMITTER_NAME: cli automation + GIT_AUTHOR_NAME: cli automation + GIT_COMMITTER_EMAIL: noreply@github.com + GIT_AUTHOR_EMAIL: noreply@github.com + TAG_NAME: ${{ inputs.tag_name }} + run: | + git -C site rm 'manual/gh*.md' 2>/dev/null || true + tar -xzvf linux/manual.tar.gz -C site + git -C site add 'manual/gh*.md' + sed -i.bak -E "s/(assign version = )\".+\"/\1\"${TAG_NAME#v}\"/" site/index.html + rm -f site/index.html.bak + git -C site add index.html + git -C site diff --quiet --cached || git -C site commit -m "gh ${TAG_NAME#v}" + - name: Prepare release assets + env: + TAG_NAME: ${{ inputs.tag_name }} + run: | + shopt -s failglob + rm -rf dist + mkdir dist + mv -v {linux,macos,windows}/gh_* dist/ + - name: Install packaging dependencies + run: sudo apt-get install -y rpm reprepro + - name: Set up GPG + if: inputs.environment == 'production' + env: + GPG_PUBKEY: ${{ secrets.GPG_PUBKEY }} + GPG_KEY: ${{ secrets.GPG_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_KEYGRIP: ${{ secrets.GPG_KEYGRIP }} + run: | + base64 -d <<<"$GPG_PUBKEY" | gpg --import --no-tty --batch --yes + base64 -d <<<"$GPG_KEY" | gpg --import --no-tty --batch --yes + echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf + gpg-connect-agent RELOADAGENT /bye + /usr/lib/gnupg2/gpg-preset-passphrase --preset "$GPG_KEYGRIP" <<<"$GPG_PASSPHRASE" + - name: Sign RPMs + if: inputs.environment == 'production' + run: | + cp script/rpmmacros ~/.rpmmacros + rpmsign --addsign dist/*.rpm + - name: Run createrepo + env: + GPG_SIGN: ${{ inputs.environment == 'production' }} + run: | + mkdir -p site/packages/rpm + cp dist/*.rpm site/packages/rpm/ + ./script/createrepo.sh + cp -r dist/repodata site/packages/rpm/ + pushd site/packages/rpm + [ "$GPG_SIGN" = "false" ] || gpg --yes --detach-sign --armor repodata/repomd.xml + popd + - name: Run reprepro + env: + GPG_SIGN: ${{ inputs.environment == 'production' }} + # We are no longer adding to the distribution list. + # All apt distributions should use "stable" according to our install documentation. + # In the future we will remove legacy distributions listed here. + RELEASES: "cosmic eoan disco groovy focal stable oldstable testing sid unstable buster bullseye stretch jessie bionic trusty precise xenial hirsute impish kali-rolling" + run: | + mkdir -p upload + [ "$GPG_SIGN" = "true" ] || sed -i.bak '/^SignWith:/d' script/distributions + for release in $RELEASES; do + for file in dist/*.deb; do + reprepro --confdir="+b/script" includedeb "$release" "$file" + done + done + cp -a dists/ pool/ upload/ + mkdir -p site/packages + cp -a upload/* site/packages/ + - name: Create the release + env: + DO_PUBLISH: ${{ inputs.environment == 'production' }} + TAG_NAME: ${{ inputs.tag_name }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + shopt -s failglob + pushd dist + shasum -a 256 gh_* > checksums.txt + mv checksums.txt gh_${TAG_NAME#v}_checksums.txt + popd + release_args=( + "$TAG_NAME" + --title "GitHub CLI ${TAG_NAME#v}" + --target "$GITHUB_SHA" + --generate-notes + ) + if [[ $TAG_NAME == *-* ]]; then + release_args+=( --prerelease ) + else + release_args+=( --discussion-category "General" ) + fi + action="echo" + [ "$DO_PUBLISH" = "false" ] || action="command" + script/label-assets dist/gh_* | xargs $action gh release create "${release_args[@]}" -- + - name: Publish site + env: + DO_PUBLISH: ${{ inputs.environment == 'production' && !contains(inputs.tag_name, '-') }} + TAG_NAME: ${{ inputs.tag_name }} + GIT_COMMITTER_NAME: cli automation + GIT_AUTHOR_NAME: cli automation + GIT_COMMITTER_EMAIL: noreply@github.com + GIT_AUTHOR_EMAIL: noreply@github.com + working-directory: ./site + run: | + git add packages + git commit -m "Add rpm and deb packages for $TAG_NAME" + if [ "$DO_PUBLISH" = "true" ]; then + git push + else + git log --oneline @{upstream}.. + git diff --name-status @{upstream}.. + fi + - name: Bump homebrew-core formula + uses: mislav/bump-homebrew-formula-action@v2 + if: inputs.environment == 'production' && !contains(inputs.tag_name, '-') + with: + formula-name: gh + tag-name: ${{ inputs.tag_name }} + env: + COMMITTER_TOKEN: ${{ secrets.UPLOAD_GITHUB_TOKEN }} diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml deleted file mode 100644 index 7ed0c3fa9..000000000 --- a/.github/workflows/releases.yml +++ /dev/null @@ -1,222 +0,0 @@ -name: goreleaser - -on: - push: - tags: - - "v*" - -permissions: - contents: write # publishing releases - repository-projects: write # move cards between columns - -jobs: - goreleaser: - runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Go 1.19 - uses: actions/setup-go@v4 - with: - go-version: 1.19 - - name: Generate changelog - id: changelog - run: | - echo "tag-name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - gh api repos/$GITHUB_REPOSITORY/releases/generate-notes \ - -f tag_name="${GITHUB_REF#refs/tags/}" \ - -f target_commitish=trunk \ - -q .body > CHANGELOG.md - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - - name: Install osslsigncode - run: sudo apt-get install -y osslsigncode - - name: Obtain signing cert - run: | - cert="$(mktemp -t cert.XXX)" - base64 -d <<<"$CERT_CONTENTS" > "$cert" - echo "CERT_FILE=$cert" >> $GITHUB_ENV - env: - CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }} - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v4 - with: - version: "~1.13.1" - args: release --release-notes=CHANGELOG.md - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - GORELEASER_CURRENT_TAG: ${{steps.changelog.outputs.tag-name}} - CERT_PASSWORD: ${{secrets.WINDOWS_CERT_PASSWORD}} - - name: Checkout documentation site - uses: actions/checkout@v3 - with: - repository: github/cli.github.com - path: site - fetch-depth: 0 - ssh-key: ${{secrets.SITE_SSH_KEY}} - - name: Update site man pages - env: - GIT_COMMITTER_NAME: cli automation - GIT_AUTHOR_NAME: cli automation - GIT_COMMITTER_EMAIL: noreply@github.com - GIT_AUTHOR_EMAIL: noreply@github.com - run: make site-bump - - name: Move project cards - continue-on-error: true - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - PENDING_COLUMN: 8189733 - DONE_COLUMN: 7110130 - run: | - api() { gh api -H 'accept: application/vnd.github.inertia-preview+json' "$@"; } - api-write() { [[ $GITHUB_REF == *-* ]] && echo "skipping: api $*" || api "$@"; } - cards=$(api --paginate projects/columns/$PENDING_COLUMN/cards | jq ".[].id") - for card in $cards; do - api-write --silent projects/columns/cards/$card/moves -f position=top -F column_id=$DONE_COLUMN - done - echo "moved ${#cards[@]} cards to the Done column" - - name: Install packaging dependencies - run: sudo apt-get install -y rpm reprepro - - name: Set up GPG - run: | - echo "${{secrets.GPG_PUBKEY}}" | base64 -d | gpg --import --no-tty --batch --yes - echo "${{secrets.GPG_KEY}}" | base64 -d | gpg --import --no-tty --batch --yes - echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf - gpg-connect-agent RELOADAGENT /bye - echo "${{secrets.GPG_PASSPHRASE}}" | /usr/lib/gnupg2/gpg-preset-passphrase --preset "${{secrets.GPG_KEYGRIP}}" - - name: Sign RPMs - run: | - cp script/rpmmacros ~/.rpmmacros - rpmsign --addsign dist/*.rpm - - name: Run createrepo - run: | - mkdir -p site/packages/rpm - cp dist/*.rpm site/packages/rpm/ - ./script/createrepo.sh - cp -r dist/repodata site/packages/rpm/ - pushd site/packages/rpm - gpg --yes --detach-sign --armor repodata/repomd.xml - popd - - name: Run reprepro - env: - # We are no longer adding to the distribution list. - # All apt distributions should use "stable" according to our install documentation. - # In the future we will remove legacy distributions listed here. - RELEASES: "cosmic eoan disco groovy focal stable oldstable testing sid unstable buster bullseye stretch jessie bionic trusty precise xenial hirsute impish kali-rolling" - run: | - mkdir -p upload - for release in $RELEASES; do - for file in dist/*.deb; do - reprepro --confdir="+b/script" includedeb "$release" "$file" - done - done - cp -a dists/ pool/ upload/ - mkdir -p site/packages - cp -a upload/* site/packages/ - - name: Publish site - env: - GIT_COMMITTER_NAME: cli automation - GIT_AUTHOR_NAME: cli automation - GIT_COMMITTER_EMAIL: noreply@github.com - GIT_AUTHOR_EMAIL: noreply@github.com - working-directory: ./site - run: | - git add packages - git commit -m "Add rpm and deb packages for ${GITHUB_REF#refs/tags/}" - if [[ $GITHUB_REF == *-* ]]; then - git log --oneline @{upstream}.. - git diff --name-status @{upstream}.. - else - git push - fi - - msi: - needs: goreleaser - runs-on: windows-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Download gh.exe - id: download_exe - shell: bash - run: | - hub release download "${GITHUB_REF#refs/tags/}" -i '*windows_amd64*.zip' - printf "zip=%s\n" *.zip >> $GITHUB_OUTPUT - unzip -o *.zip && rm -v *.zip - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - - name: Prepare PATH - id: setupmsbuild - uses: microsoft/setup-msbuild@v1.3.1 - - name: Build MSI - id: buildmsi - shell: bash - env: - ZIP_FILE: ${{ steps.download_exe.outputs.zip }} - MSBUILD_PATH: ${{ steps.setupmsbuild.outputs.msbuildPath }} - run: | - name="$(basename "$ZIP_FILE" ".zip")" - version="$(echo -e ${GITHUB_REF#refs/tags/v} | sed s/-.*$//)" - "${MSBUILD_PATH}\MSBuild.exe" ./build/windows/gh.wixproj -p:SourceDir="$PWD" -p:OutputPath="$PWD" -p:OutputName="$name" -p:ProductVersion="$version" - - name: Obtain signing cert - id: obtain_cert - shell: bash - run: | - base64 -d <<<"$CERT_CONTENTS" > ./cert.pfx - printf "cert-file=%s\n" ".\\cert.pfx" >> $GITHUB_OUTPUT - env: - CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }} - - name: Sign MSI - env: - CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }} - EXE_FILE: ${{ steps.buildmsi.outputs.msi }} - CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }} - run: .\script\signtool sign /d "GitHub CLI" /f $env:CERT_FILE /p $env:CERT_PASSWORD /fd sha256 /tr http://timestamp.digicert.com /v $env:EXE_FILE - - name: Upload MSI - shell: bash - run: | - tag_name="${GITHUB_REF#refs/tags/}" - hub release edit "$tag_name" -m "" -a "$MSI_FILE" - release_url="$(gh api repos/:owner/:repo/releases -q ".[]|select(.tag_name==\"${tag_name}\")|.url")" - publish_args=( -F draft=false ) - if [[ $GITHUB_REF != *-* ]]; then - publish_args+=( -f discussion_category_name="$DISCUSSION_CATEGORY" ) - fi - gh api -X PATCH "$release_url" "${publish_args[@]}" - env: - MSI_FILE: ${{ steps.buildmsi.outputs.msi }} - DISCUSSION_CATEGORY: General - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - - name: Bump homebrew-core formula - uses: mislav/bump-homebrew-formula-action@v2 - if: "!contains(github.ref, '-')" # skip prereleases - with: - formula-name: gh - env: - COMMITTER_TOKEN: ${{ secrets.UPLOAD_GITHUB_TOKEN }} - - name: Checkout scoop bucket - uses: actions/checkout@v3 - with: - repository: cli/scoop-gh - path: scoop-gh - fetch-depth: 0 - token: ${{secrets.UPLOAD_GITHUB_TOKEN}} - - name: Bump scoop bucket - shell: bash - run: | - hub release download "${GITHUB_REF#refs/tags/}" -i '*_checksums.txt' - script/scoop-gen "${GITHUB_REF#refs/tags/}" ./scoop-gh/gh.json < *_checksums.txt - git -C ./scoop-gh commit -m "gh ${GITHUB_REF#refs/tags/}" gh.json - if [[ $GITHUB_REF == *-* ]]; then - git -C ./scoop-gh show -m - else - git -C ./scoop-gh push - fi - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - GIT_COMMITTER_NAME: cli automation - GIT_AUTHOR_NAME: cli automation - GIT_COMMITTER_EMAIL: noreply@github.com - GIT_AUTHOR_EMAIL: noreply@github.com diff --git a/.gitignore b/.gitignore index c9a58ce00..b1e8e1fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,9 @@ /site .github/**/node_modules /CHANGELOG.md +/.goreleaser.generated.yml /script/build +/script/build.exe # VS Code .vscode diff --git a/.goreleaser.yml b/.goreleaser.yml index 26277d3c5..f441156e3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -7,57 +7,76 @@ release: before: hooks: - - go mod tidy - - make manpages GH_VERSION={{.Version}} - - make completions + - >- + {{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}} + - >- + {{ if ne .Runtime.Goos "linux" }}echo{{ end }} make completions builds: - - <<: &build_defaults - binary: bin/gh - main: ./cmd/gh - ldflags: - - -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}} - id: macos + - id: macos #build:macos goos: [darwin] goarch: [amd64, arm64] + hooks: + post: + - cmd: ./script/sign '{{ .Path }}' + output: true + binary: bin/gh + main: ./cmd/gh + ldflags: + - -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}} - - <<: *build_defaults - id: linux + - id: linux #build:linux goos: [linux] goarch: [386, arm, amd64, arm64] env: - CGO_ENABLED=0 + binary: bin/gh + main: ./cmd/gh + ldflags: + - -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}} - - <<: *build_defaults - id: windows + - id: windows #build:windows goos: [windows] goarch: [386, amd64, arm64] hooks: post: - - cmd: ./script/sign-windows-executable.sh '{{ .Path }}' - output: false + - cmd: >- + {{ if eq .Runtime.Goos "windows" }}.\script\sign{{ else }}./script/sign{{ end }} '{{ .Path }}' + output: true + binary: bin/gh + main: ./cmd/gh + ldflags: + - -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}} archives: - - id: nix - builds: [macos, linux] - <<: &archive_defaults - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + - id: linux-archive + builds: [linux] + name_template: "gh_{{ .Version }}_linux_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" wrap_in_directory: true - replacements: - darwin: macOS format: tar.gz + rlcp: true files: - LICENSE - ./share/man/man1/gh*.1 - - id: windows + - id: macos-archive + builds: [macos] + name_template: "gh_{{ .Version }}_macOS_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + wrap_in_directory: true + format: zip + rlcp: true + files: + - LICENSE + - ./share/man/man1/gh*.1 + - id: windows-archive builds: [windows] - <<: *archive_defaults + name_template: "gh_{{ .Version }}_windows_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" wrap_in_directory: false format: zip + rlcp: true files: - LICENSE -nfpms: +nfpms: #build:linux - license: MIT maintainer: GitHub homepage: https://github.com/cli/cli diff --git a/Makefile b/Makefile index ab1e28a01..e6bd74207 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ CGO_LDFLAGS ?= $(filter -g -L% -l% -O%,${LDFLAGS}) export CGO_LDFLAGS EXE = -ifeq ($(GOOS),windows) +ifeq ($(shell go env GOOS),windows) EXE = .exe endif @@ -16,23 +16,27 @@ endif bin/gh$(EXE): script/build @script/build $@ -script/build: script/build.go +script/build$(EXE): script/build.go +ifeq ($(EXE),) GOOS= GOARCH= GOARM= GOFLAGS= CGO_ENABLED= go build -o $@ $< +else + go build -o $@ $< +endif .PHONY: clean -clean: script/build - @script/build $@ +clean: script/build$(EXE) + @$< $@ .PHONY: manpages -manpages: script/build - @script/build $@ +manpages: script/build$(EXE) + @$< $@ .PHONY: completions -completions: +completions: bin/gh$(EXE) mkdir -p ./share/bash-completion/completions ./share/fish/vendor_completions.d ./share/zsh/site-functions - go run ./cmd/gh completion -s bash > ./share/bash-completion/completions/gh - go run ./cmd/gh completion -s fish > ./share/fish/vendor_completions.d/gh.fish - go run ./cmd/gh completion -s zsh > ./share/zsh/site-functions/_gh + bin/gh$(EXE) completion -s bash > ./share/bash-completion/completions/gh + bin/gh$(EXE) completion -s fish > ./share/fish/vendor_completions.d/gh.fish + bin/gh$(EXE) completion -s zsh > ./share/zsh/site-functions/_gh # just a convenience task around `go test` .PHONY: test diff --git a/build/windows/gh.wixproj b/build/windows/gh.wixproj index aa72d4da3..6c1e971d0 100644 --- a/build/windows/gh.wixproj +++ b/build/windows/gh.wixproj @@ -30,9 +30,5 @@ - - - - diff --git a/docs/releasing.md b/docs/releasing.md index e762d845e..d9b01ea41 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,36 +1,40 @@ # Releasing -Our build system automatically compiles and attaches cross-platform binaries to any git tag named `vX.Y.Z`. The changelog is [generated from git commit log](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes). +To initiate a new production deployment: +```sh +script/release vX.Y.Z +``` +See `script/release --help` for more information. -Users who run official builds of `gh` on their machines will get notified about the new version within a 24 hour period. +> **Note:** +> Every production release will request an approval by the select few people before it can proceed. -To test out the build system, publish a prerelease tag with a name such as `vX.Y.Z-pre.0` or `vX.Y.Z-rc.1`. Note that such a release will still be public, but it will be marked as a "prerelease", meaning that it will never show up as a "latest" release nor trigger upgrade notifications for users. +What this does is: +- Builds Linux binaries on Ubuntu; +- Builds and signs Windows binaries on Windows; +- Builds, signs, and notarizes macOS binaries on macOS; +- Uploads all release artifacts to a new GitHub Release; +- A new git tag `vX.Y.Z` is created in the remote repository; +- The changelog is [generated from the list of merged pull requests](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes); +- Updates cli.github.com with the contents of the new release; +- Updates our Homebrew formula in the homebrew-core repo. + +To test out the build system while avoiding creating an actual release: +```sh +script/release --staging vX.Y.Z --branch patch-1 -p macos +``` +The build artifacts will be available via `gh run download -n macos`. ## General guidelines * Features to be released should be reviewed and approved at least one day prior to the release. * Feature releases should bump up the minor version number. +* Breaking releases should bump up the major version number. These should generally be rare. -## Tagging a new release - -1. `git tag v1.2.3 && git push origin v1.2.3` -2. Wait several minutes for builds to run: -3. Verify release is displayed and has correct assets: -4. Scan generated release notes and optionally add a human touch by grouping items under topic sections -5. Verify the marketing site was updated: -6. (Optional) Delete any pre-releases related to this release - -A successful build will result in changes across several repositories: -* -* -* - -If the build fails, there is not a clean way to re-run it. The easiest way would be to start over by deleting the partial release on GitHub and re-publishing the tag. Note that this might be disruptive to users or tooling that were already notified about an upgrade. If a functional release and its binaries are already out there, it might be better to try to manually fix up only the specific workflow tasks that failed. Use your best judgement depending on the failure type. - -## Release locally for debugging +## Test the build system locally A local release can be created for testing without creating anything official on the release page. 1. Make sure GoReleaser is installed: `brew install goreleaser` -2. `goreleaser --skip-validate --skip-publish --rm-dist` +2. `script/release --local` 3. Find the built products under `dist/`. diff --git a/script/label-assets b/script/label-assets new file mode 100755 index 000000000..8c06c4b91 --- /dev/null +++ b/script/label-assets @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +if [ $# -eq 0 ]; then + echo "usage: script/label-assets dist/gh_*" >&2 + exit 1 +fi + +for asset; do + label="$(basename "$asset")" + label="${label%.*}" + label="${label%.tar}" + label="GitHub CLI $(tr '_' ' ' <<<"${label#gh_}")" + case "$asset" in + *.msi ) label="${label} installer" ;; + *.deb ) label="${label} deb" ;; + *.rpm ) label="${label} RPM" ;; + esac + printf "%s#%s\n" "$asset" "$label" +done \ No newline at end of file diff --git a/script/release b/script/release new file mode 100755 index 000000000..774a8c1dd --- /dev/null +++ b/script/release @@ -0,0 +1,115 @@ +#!/bin/bash +set -e + +print_help() { + cat < [--platform {linux|macos|windows}] [--branch ] + +To build staging binaries from the current branch: + script/release --current [--platform {linux|macos|windows}] + +To build binaries locally with goreleaser: + script/release --local --platform {linux|macos|windows} +EOF +} + +if [ $# -eq 0 ]; then + print_help >&2 + exit 1 +fi + +tag_name="" +is_local="" +do_push="" +platform="" +branch="trunk" +deploy_env="production" + +while [ $# -gt 0 ]; do + case "$1" in + -h | --help ) + print_help + exit 0 + ;; + -b | --branch ) + branch="$2" + shift 2 + ;; + -p | --platform ) + platform="$2" + shift 2 + ;; + --local ) + is_local=1 + shift 1 + ;; + --staging ) + deploy_env="staging" + shift 1 + ;; + --current ) + deploy_env="staging" + tag_name="$(git describe --tags --abbrev=0)" + branch="$(git rev-parse --symbolic-full-name '@{upstream}' 2>/dev/null || git branch --show-current)" + branch="${branch#refs/remotes/*/}" + do_push=1 + shift 1 + ;; + -* ) + printf "unrecognized flag: %s\n" "$1" >&2 + exit 1 + ;; + * ) + tag_name="$1" + shift 1 + ;; + esac +done + +announce() { + local tmpdir="${TMPDIR:-/tmp}" + echo "$*" | sed "s:${tmpdir%/}:\$TMPDIR:" + "$@" +} + +trigger_deployment() { + announce gh workflow -R cli/cli run deployment.yml --ref "$branch" -f tag_name="$tag_name" -f environment="$deploy_env" +} + +build_local() { + local goreleaser_config=".goreleaser.yml" + case "$platform" in + linux ) + sed '/#build:windows/,/^$/d; /#build:macos/,/^$/d' .goreleaser.yml >.goreleaser.generated.yml + goreleaser_config=".goreleaser.generated.yml" + ;; + macos ) + sed '/#build:windows/,/^$/d; /#build:linux/,/^$/d' .goreleaser.yml >.goreleaser.generated.yml + goreleaser_config=".goreleaser.generated.yml" + ;; + windows ) + sed '/#build:linux/,/^$/d; /#build:macos/,/^$/d' .goreleaser.yml >.goreleaser.generated.yml + goreleaser_config=".goreleaser.generated.yml" + ;; + esac + [ -z "$tag_name" ] || export GORELEASER_CURRENT_TAG="$tag_name" + announce goreleaser release -f "$goreleaser_config" --clean --skip-validate --skip-publish --release-notes="$(mktemp)" +} + +if [ -n "$is_local" ]; then + build_local +else + if [ -n "$do_push" ]; then + if ! git diff --quiet || ! git diff --cached --quiet; then + echo "refusing to continue due to uncomitted local changes" >&2 + exit 1 + fi + announce git push + fi + trigger_deployment + if [ "$deploy_env" = "production" ]; then + echo + echo "Go to Slack to manually approve this production deployment." + fi +fi diff --git a/script/scoop-gen b/script/scoop-gen deleted file mode 100755 index 2e4adf647..000000000 --- a/script/scoop-gen +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# Usage: cat checksums.txt | script/scoop-gen -set -e - -tagname="${1?}" -jsonfile="${2?}" - -jq_args=( - --arg version "${tagname#v}" - --arg urlprefix "https://github.com/cli/cli/releases/download/$tagname/" - $(cat | awk ' - /windows_386/ { - printf "--arg win32hash %s\n", $1 - printf "--arg win32file %s\n", $2 - } - /windows_amd64/ { - printf "--arg win64hash %s\n", $1 - printf "--arg win64file %s\n", $2 - } - ') -) - -jq ' - .version = $version | - .architecture."32bit".url = $urlprefix + $win32file | - .architecture."32bit".hash = $win32hash | - .architecture."64bit".url = $urlprefix + $win64file | - .architecture."64bit".hash = $win64hash -' "${jq_args[@]}" --indent 4 "$jsonfile" > "$jsonfile"~ - -mv "$jsonfile"{~,} diff --git a/script/sign b/script/sign new file mode 100755 index 000000000..1630a06b5 --- /dev/null +++ b/script/sign @@ -0,0 +1,65 @@ +#!/bin/bash +# usage: script/sign +# +# Signs macOS binaries using codesign, notarizes macOS zip archives using notarytool, and signs +# Windows EXE and MSI files using osslsigncode. +# +set -e + +sign_windows() { + if [ -z "$CERT_FILE" ]; then + echo "skipping Windows code-signing; CERT_FILE not set" >&2 + return 0 + fi + + if [ ! -f "$CERT_FILE" ]; then + echo "error Windows code-signing; file '$CERT_FILE' not found" >&2 + return 1 + fi + + if [ -z "$CERT_PASSWORD" ]; then + echo "error Windows code-signing; no value for CERT_PASSWORD" >&2 + return 1 + fi + + osslsigncode sign -n "GitHub CLI" -t http://timestamp.digicert.com \ + -pkcs12 "$CERT_FILE" -readpass <(printf "%s" "$CERT_PASSWORD") -h sha256 \ + -in "$1" -out "$1"~ + + mv "$1"~ "$1" +} + +sign_macos() { + if [ -z "$APPLE_DEVELOPER_ID" ]; then + echo "skipping macOS code-signing; APPLE_DEVELOPER_ID not set" >&2 + return 0 + fi + + if [[ $1 == *.zip ]]; then + xcrun notarytool submit "$1" --apple-id "${APPLE_ID?}" --team-id "${APPLE_DEVELOPER_ID?}" --password "${APPLE_ID_PASSWORD?}" + else + codesign --timestamp --options=runtime -s "${APPLE_DEVELOPER_ID?}" -v "$1" + fi +} + +if [ $# -eq 0 ]; then + echo "usage: script/sign " >&2 + exit 1 +fi + +platform="$(uname -s)" + +for input_file; do + case "$input_file" in + *.exe | *.msi ) + sign_windows "$input_file" + ;; + * ) + if [ "$platform" = "Darwin" ]; then + sign_macos "$input_file" + else + printf "warning: don't know how to sign %s on %s\n" "$1", "$platform" >&2 + fi + ;; + esac +done \ No newline at end of file diff --git a/script/sign-windows-executable.sh b/script/sign-windows-executable.sh deleted file mode 100755 index d89e6dbe4..000000000 --- a/script/sign-windows-executable.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -set -e - -EXE="$1" - -if [ -z "$CERT_FILE" ]; then - echo "skipping Windows code-signing; CERT_FILE not set" >&2 - exit 0 -fi - -if [ ! -f "$CERT_FILE" ]; then - echo "error Windows code-signing; file '$CERT_FILE' not found" >&2 - exit 1 -fi - -if [ -z "$CERT_PASSWORD" ]; then - echo "error Windows code-signing; no value for CERT_PASSWORD" >&2 - exit 1 -fi - -osslsigncode sign -n "GitHub CLI" -t http://timestamp.digicert.com \ - -pkcs12 "$CERT_FILE" -readpass <(printf "%s" "$CERT_PASSWORD") -h sha256 \ - -in "$EXE" -out "$EXE"~ - -mv "$EXE"~ "$EXE" diff --git a/script/sign.bat b/script/sign.bat new file mode 100644 index 000000000..4fbe8a7af --- /dev/null +++ b/script/sign.bat @@ -0,0 +1,8 @@ +@echo off + +if "%CERT_FILE%" == "" ( + echo skipping Windows code-signing; CERT_FILE not set + exit /b +) + +.\script\signtool sign /d "GitHub CLI" /f "%CERT_FILE%" /p "%CERT_PASSWORD%" /fd sha256 /tr http://timestamp.digicert.com /v "%1" \ No newline at end of file diff --git a/script/sign.ps1 b/script/sign.ps1 deleted file mode 100644 index 336e0204c..000000000 --- a/script/sign.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -param ( - [string]$Certificate = $(throw "-Certificate is required."), - [string]$Executable = $(throw "-Executable is required.") -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -$ProgramName = "GitHub CLI" -$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition - -& $scriptPath\signtool.exe sign /d $ProgramName /f $Certificate /p $env:CERT_PASSWORD /fd sha256 /tr http://timestamp.digicert.com /v $Executable From 591fcafd9c11dc59472789757f26b9fa635908d8 Mon Sep 17 00:00:00 2001 From: Biswapriyo Nath Date: Wed, 26 Apr 2023 02:43:06 +0530 Subject: [PATCH 06/23] Make: Fix target name for Windows platform (#7370) This fixes the taget name by adding .exe extension for Windows platform. Otherwise, the following error is shown with `make bin/gh.exe' command. make: *** No rule to make target 'script/build', needed by 'bin/gh.exe'. Stop. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e6bd74207..7dac0d290 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,8 @@ endif ## The following tasks delegate to `script/build.go` so they can be run cross-platform. .PHONY: bin/gh$(EXE) -bin/gh$(EXE): script/build - @script/build $@ +bin/gh$(EXE): script/build$(EXE) + @script/build$(EXE) $@ script/build$(EXE): script/build.go ifeq ($(EXE),) From 83aaa76141cd15b707004c6795a1ffb581d94cc5 Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Tue, 9 May 2023 21:32:05 +0100 Subject: [PATCH 07/23] Include auto-merge information in gh pr status (#7386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRs that have auto-merge enabled are marked with a green "✓ Auto-merge enabled" label. --- pkg/cmd/pr/status/fixtures/prStatus.json | 21 +++++++++++++++++---- pkg/cmd/pr/status/http.go | 2 +- pkg/cmd/pr/status/status.go | 4 ++++ pkg/cmd/pr/status/status_test.go | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/pr/status/fixtures/prStatus.json b/pkg/cmd/pr/status/fixtures/prStatus.json index a226663a3..d5cfb265a 100644 --- a/pkg/cmd/pr/status/fixtures/prStatus.json +++ b/pkg/cmd/pr/status/fixtures/prStatus.json @@ -15,7 +15,8 @@ "headRepositoryOwner": { "login": "OWNER" }, - "isCrossRepository": false + "isCrossRepository": false, + "autoMergeRequest": null } } ] @@ -31,7 +32,8 @@ "state": "OPEN", "url": "https://github.com/cli/cli/pull/8", "headRefName": "strawberries", - "isDraft": false + "isDraft": false, + "autoMergeRequest": null } } ] @@ -46,7 +48,17 @@ "state": "OPEN", "url": "https://github.com/cli/cli/pull/9", "headRefName": "apples", - "isDraft": false + "isDraft": false, + "autoMergeRequest": { + "authorEmail": null, + "commitBody": null, + "commitHeadline": null, + "mergeMethod": "SQUASH", + "enabledAt": "2020-08-27T19:00:12Z", + "enabledBy": { + "login": "hubot" + } + } } }, { "node": { "number": 11, @@ -54,7 +66,8 @@ "state": "OPEN", "url": "https://github.com/cli/cli/pull/1", "headRefName": "figs", - "isDraft": true + "isDraft": true, + "autoMergeRequest": null } } ] diff --git a/pkg/cmd/pr/status/http.go b/pkg/cmd/pr/status/http.go index e0b2145e9..49bdb147c 100644 --- a/pkg/cmd/pr/status/http.go +++ b/pkg/cmd/pr/status/http.go @@ -191,7 +191,7 @@ func pullRequestFragment(hostname string, conflictStatus bool) (string, error) { fields := []string{ "number", "title", "state", "url", "isDraft", "isCrossRepository", "headRefName", "headRepositoryOwner", "mergeStateStatus", - "statusCheckRollup", "requiresStrictStatusChecks", + "statusCheckRollup", "requiresStrictStatusChecks", "autoMergeRequest", } if conflictStatus { diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index c2d4271d0..fa517bada 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -284,6 +284,10 @@ func printPrs(io *iostreams.IOStreams, totalCount int, prs ...api.PullRequest) { } } + if pr.AutoMergeRequest != nil { + fmt.Fprintf(w, " %s", cs.Green("✓ Auto-merge enabled")) + } + } else { fmt.Fprintf(w, " - %s", shared.StateTitleWithColor(cs, pr)) } diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index f94822e47..1b39ddb24 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -91,7 +91,7 @@ func TestPRStatus(t *testing.T) { expectedPrs := []*regexp.Regexp{ regexp.MustCompile(`#8.*\[strawberries\]`), - regexp.MustCompile(`#9.*\[apples\]`), + regexp.MustCompile(`#9.*\[apples\].*✓ Auto-merge enabled`), regexp.MustCompile(`#10.*\[blueberries\]`), regexp.MustCompile(`#11.*\[figs\]`), } From 0da0de4022cae34046f65b4a3003d1b6e9d68a4e Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Tue, 9 May 2023 21:39:52 +0100 Subject: [PATCH 08/23] Include auto-merge information in gh pr view (#7385) --- .../prViewPreviewWithAutoMergeEnabled.json | 55 +++++++++++++++++++ pkg/cmd/pr/view/view.go | 34 +++++++++++- pkg/cmd/pr/view/view_test.go | 33 +++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 pkg/cmd/pr/view/fixtures/prViewPreviewWithAutoMergeEnabled.json diff --git a/pkg/cmd/pr/view/fixtures/prViewPreviewWithAutoMergeEnabled.json b/pkg/cmd/pr/view/fixtures/prViewPreviewWithAutoMergeEnabled.json new file mode 100644 index 000000000..000dc440e --- /dev/null +++ b/pkg/cmd/pr/view/fixtures/prViewPreviewWithAutoMergeEnabled.json @@ -0,0 +1,55 @@ +{ + "data": { + "repository": { + "pullRequest": { + "number": 12, + "title": "Blueberries are from a fork", + "state": "OPEN", + "body": "**blueberries taste good**", + "url": "https://github.com/OWNER/REPO/pull/12", + "author": { + "login": "nobody" + }, + "autoMergeRequest": { + "authorEmail": null, + "commitBody": null, + "commitHeadline": null, + "mergeMethod": "SQUASH", + "enabledAt": "2020-08-27T19:00:12Z", + "enabledBy": { + "login": "hubot" + } + }, + "additions": 100, + "deletions": 10, + "reviewRequests": { + "nodes": [], + "totalcount": 0 + }, + "assignees": { + "nodes": [], + "totalcount": 0 + }, + "labels": { + "nodes": [], + "totalcount": 0 + }, + "projectcards": { + "nodes": [], + "totalcount": 0 + }, + "milestone": {}, + "commits": { + "totalCount": 12 + }, + "baseRefName": "master", + "headRefName": "blueberries", + "headRepositoryOwner": { + "login": "hubot" + }, + "isCrossRepository": true, + "isDraft": false + } + } + } +} diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go index 10300a23c..71bda0649 100644 --- a/pkg/cmd/pr/view/view.go +++ b/pkg/cmd/pr/view/view.go @@ -77,7 +77,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman } var defaultFields = []string{ - "url", "number", "title", "state", "body", "author", + "url", "number", "title", "state", "body", "author", "autoMergeRequest", "isDraft", "maintainerCanModify", "mergeable", "additions", "deletions", "commitsCount", "baseRefName", "headRefName", "headRepositoryOwner", "headRepository", "isCrossRepository", "reviewRequests", "reviews", "assignees", "labels", "projectCards", "milestone", @@ -157,6 +157,15 @@ func printRawPrPreview(io *iostreams.IOStreams, pr *api.PullRequest) error { fmt.Fprintf(out, "url:\t%s\n", pr.URL) fmt.Fprintf(out, "additions:\t%s\n", cs.Green(strconv.Itoa(pr.Additions))) fmt.Fprintf(out, "deletions:\t%s\n", cs.Red(strconv.Itoa(pr.Deletions))) + var autoMerge string + if pr.AutoMergeRequest == nil { + autoMerge = "disabled" + } else { + autoMerge = fmt.Sprintf("enabled\t%s\t%s", + pr.AutoMergeRequest.EnabledBy.Login, + strings.ToLower(pr.AutoMergeRequest.MergeMethod)) + } + fmt.Fprintf(out, "auto-merge:\t%s\n", autoMerge) fmt.Fprintln(out, "--") fmt.Fprintln(out, pr.Body) @@ -223,6 +232,29 @@ func printHumanPrPreview(opts *ViewOptions, pr *api.PullRequest) error { fmt.Fprintln(out, pr.Milestone.Title) } + // Auto-Merge status + autoMerge := pr.AutoMergeRequest + if autoMerge != nil { + var mergeMethod string + switch autoMerge.MergeMethod { + case "MERGE": + mergeMethod = "a merge commit" + case "REBASE": + mergeMethod = "rebase and merge" + case "SQUASH": + mergeMethod = "squash and merge" + default: + mergeMethod = fmt.Sprintf("an unknown merge method (%s)", autoMerge.MergeMethod) + } + fmt.Fprintf(out, + "%s %s by %s, using %s\n", + cs.Bold("Auto-merge:"), + cs.Green("enabled"), + autoMerge.EnabledBy.Login, + mergeMethod, + ) + } + // Body var md string var err error diff --git a/pkg/cmd/pr/view/view_test.go b/pkg/cmd/pr/view/view_test.go index 166289050..91b77cac8 100644 --- a/pkg/cmd/pr/view/view_test.go +++ b/pkg/cmd/pr/view/view_test.go @@ -314,6 +314,25 @@ func TestPRView_Preview_nontty(t *testing.T) { `\*\*blueberries taste good\*\*`, }, }, + "PR with auto-merge enabled": { + branch: "master", + args: "12", + fixtures: map[string]string{ + "PullRequestByNumber": "./fixtures/prViewPreviewWithAutoMergeEnabled.json", + }, + expectedOutputs: []string{ + `title:\tBlueberries are from a fork\n`, + `state:\tOPEN\n`, + `author:\tnobody\n`, + `labels:\t\n`, + `assignees:\t\n`, + `projects:\t\n`, + `milestone:\t\n`, + `additions:\t100\n`, + `deletions:\t10\n`, + `auto-merge:\tenabled\thubot\tsquash\n`, + }, + }, } for name, tc := range tests { @@ -504,6 +523,20 @@ func TestPRView_Preview(t *testing.T) { `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, }, }, + "PR with auto-merge enabled": { + branch: "master", + args: "12", + fixtures: map[string]string{ + "PullRequestByNumber": "./fixtures/prViewPreviewWithAutoMergeEnabled.json", + }, + expectedOutputs: []string{ + `Blueberries are from a fork #12\n`, + `Open.*nobody wants to merge 12 commits into master from blueberries . about X years ago`, + `Auto-merge:.*enabled.* by hubot, using squash and merge`, + `blueberries taste good`, + `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, + }, + }, } for name, tc := range tests { From 8d0f211fdb03ba8e63f33484c0c7f0ee0de3547c Mon Sep 17 00:00:00 2001 From: Kousik Mitra Date: Wed, 10 May 2023 07:26:54 +0530 Subject: [PATCH 09/23] Print message after codespace deletion (#7353) --- pkg/cmd/codespace/delete.go | 14 ++++++++++++-- pkg/cmd/codespace/delete_test.go | 25 ++++++++++++++----------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/codespace/delete.go b/pkg/cmd/codespace/delete.go index 5a7028dc1..e54bf09fb 100644 --- a/pkg/cmd/codespace/delete.go +++ b/pkg/cmd/codespace/delete.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "sync/atomic" "time" "github.com/AlecAivazis/survey/v2" @@ -176,7 +177,8 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) { progressLabel = "Deleting codespaces" } - return a.RunWithProgress(progressLabel, func() error { + var deletedCodespaces uint32 + err = a.RunWithProgress(progressLabel, func() error { var g errgroup.Group for _, c := range codespacesToDelete { codespaceName := c.Name @@ -185,15 +187,23 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) { a.errLogger.Printf("error deleting codespace %q: %v\n", codespaceName, err) return err } + atomic.AddUint32(&deletedCodespaces, 1) return nil }) } if err := g.Wait(); err != nil { - return errors.New("some codespaces failed to delete") + return fmt.Errorf("%d codespace(s) failed to delete", len(codespacesToDelete)-int(deletedCodespaces)) } return nil }) + + if a.io.IsStdoutTTY() && deletedCodespaces > 0 { + successMsg := fmt.Sprintf("%d codespace(s) deleted successfully\n", deletedCodespaces) + fmt.Fprint(a.io.ErrOut, successMsg) + } + + return err } func confirmDeletion(p prompter, apiCodespace *api.Codespace, isInteractive bool) (bool, error) { diff --git a/pkg/cmd/codespace/delete_test.go b/pkg/cmd/codespace/delete_test.go index 0a5a76956..4541af802 100644 --- a/pkg/cmd/codespace/delete_test.go +++ b/pkg/cmd/codespace/delete_test.go @@ -26,7 +26,7 @@ func TestDelete(t *testing.T) { codespaces []*api.Codespace confirms map[string]bool deleteErr error - wantErr bool + wantErr string wantDeleted []string wantStdout string wantStderr string @@ -42,7 +42,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"hubot-robawt-abc"}, - wantStdout: "", + wantStderr: "1 codespace(s) deleted successfully\n", }, { name: "by repo", @@ -70,7 +70,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"monalisa-spoonknife-123", "monalisa-spoonknife-c4f3"}, - wantStdout: "", + wantStderr: "2 codespace(s) deleted successfully\n", }, { name: "unused", @@ -93,7 +93,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"}, - wantStdout: "", + wantStderr: "2 codespace(s) deleted successfully\n", }, { name: "deletion failed", @@ -109,12 +109,13 @@ func TestDelete(t *testing.T) { }, }, deleteErr: errors.New("aborted by test"), - wantErr: true, + wantErr: "2 codespace(s) failed to delete", wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-123"}, wantStderr: heredoc.Doc(` error deleting codespace "hubot-robawt-abc": aborted by test error deleting codespace "monalisa-spoonknife-123": aborted by test `), + wantStdout: "", }, { name: "with confirm", @@ -149,7 +150,7 @@ func TestDelete(t *testing.T) { "Codespace hubot-robawt-abc has unsaved changes. OK to delete?": true, }, wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"}, - wantStdout: "", + wantStderr: "2 codespace(s) deleted successfully\n", }, { name: "deletion for org codespace by admin succeeds", @@ -174,7 +175,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"monalisa-spoonknife-123"}, - wantStdout: "", + wantStderr: "1 codespace(s) deleted successfully\n", }, { name: "deletion for org codespace by admin fails for codespace not found", @@ -200,7 +201,8 @@ func TestDelete(t *testing.T) { }, wantDeleted: []string{}, wantStdout: "", - wantErr: true, + wantErr: "error fetching codespace information: " + + "codespace not found for user johnDoe with name monalisa-spoonknife-123", }, { name: "deletion for org codespace succeeds without username", @@ -215,7 +217,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"monalisa-spoonknife-123"}, - wantStdout: "", + wantStderr: "1 codespace(s) deleted successfully\n", }, { name: "by repo owner", @@ -253,6 +255,7 @@ func TestDelete(t *testing.T) { }, }, wantDeleted: []string{"octocat-spoonknife-123", "octocat-spoonknife-c4f3"}, + wantStderr: "2 codespace(s) deleted successfully\n", wantStdout: "", }, } @@ -306,8 +309,8 @@ func TestDelete(t *testing.T) { ios.SetStdoutTTY(true) app := NewApp(ios, nil, apiMock, nil, nil) err := app.Delete(context.Background(), opts) - if (err != nil) != tt.wantErr { - t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr) + if (err != nil) && tt.wantErr != err.Error() { + t.Errorf("delete() error = %v, wantErr = %v", err, tt.wantErr) } for _, listArgs := range apiMock.ListCodespacesCalls() { if listArgs.Opts.OrgName != "" && listArgs.Opts.UserName == "" { From fbb9d71b02003ce14d3f6a5315ecd59c5e709586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 10 May 2023 12:14:07 +0200 Subject: [PATCH 10/23] Fix release guard for deployment workflow Co-authored-by: Sam Coe --- .github/workflows/deployment.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 8d1cb2169..c2dad891e 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -293,6 +293,7 @@ jobs: cp -a upload/* site/packages/ - name: Create the release env: + # In non-production environments, the assets will not have been signed DO_PUBLISH: ${{ inputs.environment == 'production' }} TAG_NAME: ${{ inputs.tag_name }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -313,9 +314,9 @@ jobs: else release_args+=( --discussion-category "General" ) fi - action="echo" - [ "$DO_PUBLISH" = "false" ] || action="command" - script/label-assets dist/gh_* | xargs $action gh release create "${release_args[@]}" -- + guard="echo" + [ "$DO_PUBLISH" = "false" ] || guard="" + script/label-assets dist/gh_* | xargs $guard gh release create "${release_args[@]}" -- - name: Publish site env: DO_PUBLISH: ${{ inputs.environment == 'production' && !contains(inputs.tag_name, '-') }} From aa2adab7fac34837e4034244fc8cd24f0dccdee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 10 May 2023 12:38:06 +0200 Subject: [PATCH 11/23] Fix label assets --- script/label-assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/label-assets b/script/label-assets index 8c06c4b91..322e35998 100755 --- a/script/label-assets +++ b/script/label-assets @@ -16,5 +16,5 @@ for asset; do *.deb ) label="${label} deb" ;; *.rpm ) label="${label} RPM" ;; esac - printf "%s#%s\n" "$asset" "$label" + printf '"%s#%s"\n' "$asset" "$label" done \ No newline at end of file From 07ed1e4e8a2adc4ee46b0108cd2d464498689842 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 11 May 2023 14:59:12 +0200 Subject: [PATCH 12/23] Introduce helpTopics type and reduce duplication across commands (#7414) --- pkg/cmd/root/help.go | 4 +- pkg/cmd/root/help_reference.go | 6 +- pkg/cmd/root/help_topic.go | 65 ++++++++++++-------- pkg/cmd/root/help_topic_test.go | 103 +++++++++++++++++--------------- pkg/cmd/root/root.go | 26 +++++--- 5 files changed, 118 insertions(+), 86 deletions(-) diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index e35486f48..9da57d6e9 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -139,8 +139,8 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) { if c := findCommand(command, "actions"); c != nil { helpTopics = append(helpTopics, rpad(c.Name()+":", namePadding)+c.Short) } - for topic, params := range HelpTopics { - helpTopics = append(helpTopics, rpad(topic+":", namePadding)+params["short"]) + for _, helpTopic := range HelpTopics { + helpTopics = append(helpTopics, rpad(helpTopic.name+":", namePadding)+helpTopic.short) } sort.Strings(helpTopics) helpEntries = append(helpEntries, helpEntry{"HELP TOPICS", strings.Join(helpTopics, "\n")}) diff --git a/pkg/cmd/root/help_reference.go b/pkg/cmd/root/help_reference.go index 86132c985..76e9db24e 100644 --- a/pkg/cmd/root/help_reference.go +++ b/pkg/cmd/root/help_reference.go @@ -11,7 +11,9 @@ import ( "github.com/spf13/cobra" ) -func referenceHelpFn(io *iostreams.IOStreams) func(*cobra.Command, []string) { +// longPager provides a pager over a commands Long message. +// It is currently only used for the reference command +func longPager(io *iostreams.IOStreams) func(*cobra.Command, []string) { return func(cmd *cobra.Command, args []string) { wrapWidth := 0 if io.IsStdoutTTY() { @@ -38,7 +40,7 @@ func referenceHelpFn(io *iostreams.IOStreams) func(*cobra.Command, []string) { } } -func referenceLong(cmd *cobra.Command) string { +func stringifyReference(cmd *cobra.Command) string { buf := bytes.NewBufferString("# gh reference\n\n") for _, c := range cmd.Commands() { if c.Hidden { diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 4df7d9e1a..18a7f4801 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -10,10 +10,18 @@ import ( "github.com/spf13/cobra" ) -var HelpTopics = map[string]map[string]string{ - "mintty": { - "short": "Information about using gh with MinTTY", - "long": heredoc.Doc(` +type helpTopic struct { + name string + short string + long string + example string +} + +var HelpTopics = []helpTopic{ + { + name: "mintty", + short: "Information about using gh with MinTTY", + long: heredoc.Doc(` MinTTY is the terminal emulator that comes by default with Git for Windows. It has known issues with gh's ability to prompt a user for input. @@ -30,9 +38,10 @@ var HelpTopics = map[string]map[string]string{ NOTE: this can lead to some UI bugs. `), }, - "environment": { - "short": "Environment variables that can be used with gh", - "long": heredoc.Doc(` + { + name: "environment", + short: "Environment variables that can be used with gh", + long: heredoc.Doc(` GH_TOKEN, GITHUB_TOKEN (in order of precedence): an authentication token for github.com API requests. Setting this avoids being prompted to authenticate and takes precedence over previously stored credentials. @@ -91,12 +100,14 @@ var HelpTopics = map[string]map[string]string{ its own path such as in the cygwin terminal. `), }, - "reference": { - "short": "A comprehensive reference of all gh commands", + { + name: "reference", + short: "A comprehensive reference of all gh commands", }, - "formatting": { - "short": "Formatting options for JSON data exported from gh", - "long": heredoc.Docf(` + { + name: "formatting", + short: "Formatting options for JSON data exported from gh", + long: heredoc.Docf(` By default, the result of %[1]sgh%[1]s commands are output in line-based plain text format. Some commands support passing the %[1]s--json%[1]s flag, which converts the output to JSON format. Once in JSON, the output can be further formatted according to a required formatting string by @@ -132,7 +143,7 @@ var HelpTopics = map[string]map[string]string{ To learn more about Go templates, see: . `, "`"), - "example": heredoc.Doc(` + example: heredoc.Doc(` # default output format $ gh pr list Showing 23 of 23 open pull requests in cli/cli @@ -242,9 +253,10 @@ var HelpTopics = map[string]map[string]string{ mislav COMMENTED This is going along great! Thanks for working on this ❤️ `), }, - "exit-codes": { - "short": "Exit codes used by gh", - "long": heredoc.Doc(` + { + name: "exit-codes", + short: "Exit codes used by gh", + long: heredoc.Doc(` gh follows normal conventions regarding exit codes. - If a command completes successfully, the exit code will be 0 @@ -262,30 +274,31 @@ var HelpTopics = map[string]map[string]string{ }, } -func NewHelpTopic(ios *iostreams.IOStreams, topic string) *cobra.Command { +func NewCmdHelpTopic(ios *iostreams.IOStreams, ht helpTopic) *cobra.Command { cmd := &cobra.Command{ - Use: topic, - Short: HelpTopics[topic]["short"], - Long: HelpTopics[topic]["long"], - Example: HelpTopics[topic]["example"], + Use: ht.name, + Short: ht.short, + Long: ht.long, + Example: ht.example, Hidden: true, Annotations: map[string]string{ "markdown:generate": "true", - "markdown:basename": "gh_help_" + topic, + "markdown:basename": "gh_help_" + ht.name, }, } - cmd.SetHelpFunc(func(c *cobra.Command, args []string) { - helpTopicHelpFunc(ios.Out, c, args) - }) cmd.SetUsageFunc(func(c *cobra.Command) error { return helpTopicUsageFunc(ios.ErrOut, c) }) + cmd.SetHelpFunc(func(c *cobra.Command, _ []string) { + helpTopicHelpFunc(ios.Out, c) + }) + return cmd } -func helpTopicHelpFunc(w io.Writer, command *cobra.Command, args []string) { +func helpTopicHelpFunc(w io.Writer, command *cobra.Command) { fmt.Fprint(w, command.Long) if command.Example != "" { fmt.Fprintf(w, "\n\nEXAMPLES\n") diff --git a/pkg/cmd/root/help_topic_test.go b/pkg/cmd/root/help_topic_test.go index 5f6825912..baa4f7f56 100644 --- a/pkg/cmd/root/help_topic_test.go +++ b/pkg/cmd/root/help_topic_test.go @@ -4,76 +4,85 @@ import ( "testing" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestNewHelpTopic(t *testing.T) { +func TestCmdHelpTopic(t *testing.T) { + t.Parallel() + tests := []struct { - name string - topic string - args []string - flags []string - wantsErr bool + name string + topic helpTopic + args []string + flags []string + errorAssertion require.ErrorAssertionFunc + expectedAnnotations map[string]string }{ { - name: "valid topic", - topic: "environment", - args: []string{}, - flags: []string{}, - wantsErr: false, + name: "when there are no args or flags, prints the long message to stdout", + topic: helpTopic{name: "test-topic", long: "test-topic-long"}, + args: []string{}, + flags: []string{}, + errorAssertion: require.NoError, }, { - name: "invalid topic", - topic: "invalid", - args: []string{}, - flags: []string{}, - wantsErr: false, + name: "when an arg is provided, it is ignored and help is printed", + topic: helpTopic{name: "test-topic"}, + args: []string{"anything"}, + flags: []string{}, + errorAssertion: require.NoError, }, { - name: "more than zero args", - topic: "environment", - args: []string{"invalid"}, - flags: []string{}, - wantsErr: false, + name: "when a flag is provided, returns an error", + topic: helpTopic{name: "test-topic"}, + args: []string{}, + flags: []string{"--anything"}, + errorAssertion: require.Error, }, { - name: "more than zero flags", - topic: "environment", - args: []string{}, - flags: []string{"--invalid"}, - wantsErr: true, + name: "when there is an example, include it in the stdout", + topic: helpTopic{name: "test-topic", example: "test-topic-example"}, + args: []string{}, + flags: []string{}, + errorAssertion: require.NoError, }, { - name: "help arg", - topic: "environment", - args: []string{"help"}, - flags: []string{}, - wantsErr: false, - }, - { - name: "help flag", - topic: "environment", - args: []string{}, - flags: []string{"--help"}, - wantsErr: false, + name: "sets important markdown annotations on the command", + topic: helpTopic{name: "test-topic"}, + args: []string{}, + flags: []string{}, + errorAssertion: require.NoError, + expectedAnnotations: map[string]string{ + "markdown:generate": "true", + "markdown:basename": "gh_help_test-topic", + }, }, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { - ios, _, _, stderr := iostreams.Test() + t.Parallel() - cmd := NewHelpTopic(ios, tt.topic) + ios, _, stdout, _ := iostreams.Test() + + cmd := NewCmdHelpTopic(ios, tt.topic) cmd.SetArgs(append(tt.args, tt.flags...)) - cmd.SetOut(stderr) - cmd.SetErr(stderr) _, err := cmd.ExecuteC() - if tt.wantsErr { - assert.Error(t, err) - return + tt.errorAssertion(t, err) + + if tt.topic.long != "" { + require.Contains(t, stdout.String(), tt.topic.long) + } + + if tt.topic.example != "" { + require.Contains(t, stdout.String(), tt.topic.example) + } + + if len(tt.expectedAnnotations) > 0 { + require.Equal(t, cmd.Annotations, tt.expectedAnnotations) } - assert.NoError(t, err) }) } } diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 78b436fe1..8d18a4db1 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -125,18 +125,26 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmd.AddCommand(labelCmd.NewCmdLabel(&repoResolvingCmdFactory)) // Help topics - cmd.AddCommand(NewHelpTopic(f.IOStreams, "environment")) - cmd.AddCommand(NewHelpTopic(f.IOStreams, "formatting")) - cmd.AddCommand(NewHelpTopic(f.IOStreams, "mintty")) - cmd.AddCommand(NewHelpTopic(f.IOStreams, "exit-codes")) - referenceCmd := NewHelpTopic(f.IOStreams, "reference") - referenceCmd.SetHelpFunc(referenceHelpFn(f.IOStreams)) - cmd.AddCommand(referenceCmd) + var referenceCmd *cobra.Command + for _, ht := range HelpTopics { + helpTopicCmd := NewCmdHelpTopic(f.IOStreams, ht) + cmd.AddCommand(helpTopicCmd) + + // See bottom of the function for why we explicitly care about the reference cmd + if ht.name == "reference" { + referenceCmd = helpTopicCmd + } + } cmdutil.DisableAuthCheck(cmd) - // this needs to appear last: - referenceCmd.Long = referenceLong(cmd) + // The reference command produces paged output that displays information on every other command. + // Therefore, we explicitly set the Long text and HelpFunc here after all other commands are registered. + // We experimented with producing the paged output dynamically when the HelpFunc is called but since + // doc generation makes use of the Long text, it is simpler to just be explicit here that this command + // is special. + referenceCmd.Long = stringifyReference(cmd) + referenceCmd.SetHelpFunc(longPager(f.IOStreams)) return cmd } From bd749b67f69cedc6fd975ba949a7dab5eedc947a Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Mon, 15 May 2023 01:08:01 +0200 Subject: [PATCH 13/23] Allow `gh repo set-default --view` without repo argument (#7441) --- pkg/cmd/repo/setdefault/setdefault.go | 8 ++++---- pkg/cmd/repo/setdefault/setdefault_test.go | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/repo/setdefault/setdefault.go b/pkg/cmd/repo/setdefault/setdefault.go index 2cf8b19b9..d5a03baf1 100644 --- a/pkg/cmd/repo/setdefault/setdefault.go +++ b/pkg/cmd/repo/setdefault/setdefault.go @@ -86,7 +86,7 @@ func NewCmdSetDefault(f *cmdutil.Factory, runF func(*SetDefaultOptions) error) * } } - if !opts.IO.CanPrompt() && opts.Repo == nil { + if !opts.ViewMode && !opts.IO.CanPrompt() && opts.Repo == nil { return cmdutil.FlagErrorf("repository required when not running interactively") } @@ -119,10 +119,10 @@ func setDefaultRun(opts *SetDefaultOptions) error { currentDefaultRepo, _ := remotes.ResolvedRemote() if opts.ViewMode { - if currentDefaultRepo == nil { - fmt.Fprintln(opts.IO.Out, "no default repository has been set; use `gh repo set-default` to select one") - } else { + if currentDefaultRepo != nil { fmt.Fprintln(opts.IO.Out, displayRemoteRepoName(currentDefaultRepo)) + } else if opts.IO.IsStdoutTTY() { + fmt.Fprintln(opts.IO.Out, "no default repository has been set; use `gh repo set-default` to select one") } return nil } diff --git a/pkg/cmd/repo/setdefault/setdefault_test.go b/pkg/cmd/repo/setdefault/setdefault_test.go index a1c1f44ac..58871f75b 100644 --- a/pkg/cmd/repo/setdefault/setdefault_test.go +++ b/pkg/cmd/repo/setdefault/setdefault_test.go @@ -166,7 +166,8 @@ func TestDefaultRun(t *testing.T) { wantStdout: "no default repository has been set\n", }, { - name: "view mode no current default", + name: "tty view mode no current default", + tty: true, opts: SetDefaultOptions{ViewMode: true}, remotes: []*context.Remote{ { @@ -176,6 +177,16 @@ func TestDefaultRun(t *testing.T) { }, wantStdout: "no default repository has been set; use `gh repo set-default` to select one\n", }, + { + name: "view mode no current default", + opts: SetDefaultOptions{ViewMode: true}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + }, + }, { name: "view mode with base resolved current default", opts: SetDefaultOptions{ViewMode: true}, From ae58b7714c420c2160f9dbcd03ca10f45ab03ac2 Mon Sep 17 00:00:00 2001 From: ffalor <35144141+ffalor@users.noreply.github.com> Date: Sun, 14 May 2023 22:38:49 -0500 Subject: [PATCH 14/23] respect GH_REPO env variable in `pr create` --- pkg/cmd/pr/create/create.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 4fc61f362..93b61d53d 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "os" "regexp" "strings" "time" @@ -129,6 +130,12 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co opts.TitleProvided = cmd.Flags().Changed("title") opts.RepoOverride, _ = cmd.Flags().GetString("repo") + // Workaround: Due to the way this command is implemented, we need to manually check GH_REPO. + // Commands should use the standard BaseRepoOverride functionality to handle this behavior instead. + if opts.RepoOverride == "" { + opts.RepoOverride = os.Getenv("GH_REPO") + } + noMaintainerEdit, _ := cmd.Flags().GetBool("no-maintainer-edit") opts.MaintainerCanModify = !noMaintainerEdit From 0d110ea8bf45fbc865ccb15fd17b95398947cd18 Mon Sep 17 00:00:00 2001 From: Rick Kilgore Date: Mon, 15 May 2023 02:47:53 -0700 Subject: [PATCH 15/23] survey: fix color contrast of default values in prompts (#7354) --- cmd/gh/main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/gh/main.go b/cmd/gh/main.go index eb0ad0672..f231d7d2c 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -80,9 +80,6 @@ func mainRun() exitCode { surveyCore.TemplateFuncsWithColor["color"] = func(style string) string { switch style { case "white": - if cmdFactory.IOStreams.ColorSupport256() { - return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242) - } return ansi.ColorCode("default") default: return ansi.ColorCode(style) From ae3e879480a099304c5ad607ff4916d840f1feae Mon Sep 17 00:00:00 2001 From: Tony F Date: Mon, 15 May 2023 10:49:45 -0500 Subject: [PATCH 16/23] Mention setting GH_ENTERPRISE_TOKEN When GH_HOST is set --- pkg/cmd/root/help_topic.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 18a7f4801..7db694668 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -50,7 +50,8 @@ var HelpTopics = []helpTopic{ token for API requests to GitHub Enterprise. When setting this, also set GH_HOST. GH_HOST: specify the GitHub hostname for commands that would otherwise assume the - "github.com" host when not in a context of an existing repository. + "github.com" host when not in a context of an existing repository. When setting this, + also set GH_ENTERPRISE_TOKEN. GH_REPO: specify the GitHub repository in the "[HOST/]OWNER/REPO" format for commands that otherwise operate on a local repository. From f77a3dcacbf265e1b4ce5dafa860552d504a0888 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 16 May 2023 09:44:00 +1200 Subject: [PATCH 17/23] Do not fall back to legacy template if template selector returns nil (#7444) --- pkg/cmd/pr/create/create.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 93b61d53d..83421d88b 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -325,8 +325,6 @@ func createRun(opts *CreateOptions) (err error) { if template != nil { templateContent = string(template.Body()) - } else { - templateContent = string(tpl.LegacyBody()) } } From cd8547b227774e34df0101ce92da9702a29075f2 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 16 May 2023 19:35:10 +0200 Subject: [PATCH 18/23] Add some further test coverage around PR ChecksStatus --- api/pull_request_test.go | 301 +++++++++++++++++++++++++++++++++++---- 1 file changed, 277 insertions(+), 24 deletions(-) diff --git a/api/pull_request_test.go b/api/pull_request_test.go index 2e4fa73b1..67ea20cb5 100644 --- a/api/pull_request_test.go +++ b/api/pull_request_test.go @@ -2,42 +2,295 @@ package api import ( "encoding/json" + "fmt" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestPullRequest_ChecksStatus(t *testing.T) { - pr := PullRequest{} +func TestChecksStatus_NoCheckRunsOrStatusContexts(t *testing.T) { + t.Parallel() + payload := ` + { "statusCheckRollup": { "nodes": [] } } + ` + var pr PullRequest + require.NoError(t, json.Unmarshal([]byte(payload), &pr)) + + expectedChecksStatus := PullRequestChecksStatus{ + Pending: 0, + Failing: 0, + Passing: 0, + Total: 0, + } + require.Equal(t, expectedChecksStatus, pr.ChecksStatus()) +} + +func TestChecksStatus_SummarisingCheckRuns(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + payload string + expectedChecksStatus PullRequestChecksStatus + }{ + { + name: "QUEUED is treated as Pending", + payload: singleCheckRunWithStatus("QUEUED"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "IN_PROGRESS is treated as Pending", + payload: singleCheckRunWithStatus("IN_PROGRESS"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "WAITING is treated as Pending", + payload: singleCheckRunWithStatus("WAITING"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "PENDING is treated as Pending", + payload: singleCheckRunWithStatus("PENDING"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "REQUESTED is treated as Pending", + payload: singleCheckRunWithStatus("REQUESTED"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "COMPLETED / STARTUP_FAILURE is treated as Pending", + payload: singleCompletedCheckRunWithConclusion("STARTUP_FAILURE"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "COMPLETED / STALE is treated as Pending", + payload: singleCompletedCheckRunWithConclusion("STALE"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "COMPLETED / SUCCESS is treated as Passing", + payload: singleCompletedCheckRunWithConclusion("SUCCESS"), + expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1}, + }, + { + name: "COMPLETED / NEUTRAL is treated as Passing", + payload: singleCompletedCheckRunWithConclusion("NEUTRAL"), + expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1}, + }, + { + name: "COMPLETED / SKIPPED is treated as Passing", + payload: singleCompletedCheckRunWithConclusion("SKIPPED"), + expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1}, + }, + { + name: "COMPLETED / ACTION_REQUIRED is treated as Failing", + payload: singleCompletedCheckRunWithConclusion("ACTION_REQUIRED"), + expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1}, + }, + { + name: "COMPLETED / TIMED_OUT is treated as Failing", + payload: singleCompletedCheckRunWithConclusion("TIMED_OUT"), + expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1}, + }, + { + name: "COMPLETED / CANCELLED is treated as Failing", + payload: singleCompletedCheckRunWithConclusion("CANCELLED"), + expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1}, + }, + { + name: "COMPLETED / CANCELLED is treated as Failing", + payload: singleCompletedCheckRunWithConclusion("CANCELLED"), + expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1}, + }, + { + name: "COMPLETED / FAILURE is treated as Failing", + payload: singleCompletedCheckRunWithConclusion("FAILURE"), + expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1}, + }, + { + name: "Unrecognized Status are treated as Pending", + payload: singleCheckRunWithStatus("AnUnrecognizedStatusJustForThisTest"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "Unrecognized Conclusions are treated as Pending", + payload: singleCompletedCheckRunWithConclusion("AnUnrecognizedConclusionJustForThisTest"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var pr PullRequest + require.NoError(t, json.Unmarshal([]byte(tt.payload), &pr)) + + require.Equal(t, tt.expectedChecksStatus, pr.ChecksStatus()) + }) + } +} + +func TestChecksStatus_SummarisingStatusContexts(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + payload string + expectedChecksStatus PullRequestChecksStatus + }{ + { + name: "EXPECTED is treated as Pending", + payload: singleStatusContextWithState("EXPECTED"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "PENDING is treated as Pending", + payload: singleStatusContextWithState("PENDING"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + { + name: "SUCCESS is treated as Passing", + payload: singleStatusContextWithState("SUCCESS"), + expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1}, + }, + { + name: "ERROR is treated as Failing", + payload: singleStatusContextWithState("ERROR"), + expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1}, + }, + { + name: "FAILURE is treated as Failing", + payload: singleStatusContextWithState("FAILURE"), + expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1}, + }, + { + name: "Unrecognized States are treated as Pending", + payload: singleStatusContextWithState("AnUnrecognizedStateJustForThisTest"), + expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var pr PullRequest + require.NoError(t, json.Unmarshal([]byte(tt.payload), &pr)) + + require.Equal(t, tt.expectedChecksStatus, pr.ChecksStatus()) + }) + } +} + +func TestChecksStatus_SummarisingCheckRunsAndStatusContexts(t *testing.T) { + t.Parallel() + + // This might look a bit intimidating, but we're just inserting three nodes + // into the rollup, two completed check run nodes and one status context node. + payload := fmt.Sprintf(` { "statusCheckRollup": { "nodes": [{ "commit": { "statusCheckRollup": { "contexts": { "nodes": [ - { "state": "SUCCESS" }, - { "state": "PENDING" }, - { "state": "FAILURE" }, - { "status": "IN_PROGRESS", - "conclusion": null }, - { "status": "COMPLETED", - "conclusion": "SUCCESS" }, - { "status": "COMPLETED", - "conclusion": "FAILURE" }, - { "status": "COMPLETED", - "conclusion": "ACTION_REQUIRED" }, - { "status": "COMPLETED", - "conclusion": "STALE" } + %s, + %s, + %s ] } } } }] } } - ` - err := json.Unmarshal([]byte(payload), &pr) - assert.NoError(t, err) + `, + completedCheckRunNode("SUCCESS"), + statusContextNode("PENDING"), + completedCheckRunNode("FAILURE"), + ) - checks := pr.ChecksStatus() - assert.Equal(t, 8, checks.Total) - assert.Equal(t, 3, checks.Pending) - assert.Equal(t, 3, checks.Failing) - assert.Equal(t, 2, checks.Passing) + var pr PullRequest + require.NoError(t, json.Unmarshal([]byte(payload), &pr)) + + expectedChecksStatus := PullRequestChecksStatus{ + Pending: 1, + Failing: 1, + Passing: 1, + Total: 3, + } + require.Equal(t, expectedChecksStatus, pr.ChecksStatus()) +} + +// Note that it would be incorrect to provide a status of COMPLETED here +// as the conclusion is always set to null. If you want a COMPLETED status, +// use `singleCompletedCheckRunWithConclusion`. +func singleCheckRunWithStatus(status string) string { + return fmt.Sprintf(` + { "statusCheckRollup": { "nodes": [{ "commit": { + "statusCheckRollup": { + "contexts": { + "nodes": [ + { + "__typename": "CheckRun", + "status": "%s", + "conclusion": null + } + ] + } + } + } }] } } + `, status) +} + +func singleCompletedCheckRunWithConclusion(conclusion string) string { + return fmt.Sprintf(` + { "statusCheckRollup": { "nodes": [{ "commit": { + "statusCheckRollup": { + "contexts": { + "nodes": [ + { + "__typename": "CheckRun", + "status": "COMPLETED", + "conclusion": "%s" + } + ] + } + } + } }] } } + `, conclusion) +} + +func singleStatusContextWithState(state string) string { + return fmt.Sprintf(` + { "statusCheckRollup": { "nodes": [{ "commit": { + "statusCheckRollup": { + "contexts": { + "nodes": [ + { + "__typename": "StatusContext", + "state": "%s" + } + ] + } + } + } }] } } + `, state) +} + +func completedCheckRunNode(conclusion string) string { + return fmt.Sprintf(` + { + "__typename": "CheckRun", + "status": "COMPLETED", + "conclusion": "%s" + }`, conclusion) +} + +func statusContextNode(state string) string { + return fmt.Sprintf(` + { + "__typename": "StatusContext", + "state": "%s" + }`, state) } From c4bb344dddc7b0cd805c28cdc5eb47404a8c2cb1 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 16 May 2023 19:41:59 +0200 Subject: [PATCH 19/23] Add some comments to PR ChecksStatus --- api/queries_pr.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index ae8f7c0c6..5ae5cbb8f 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -265,11 +265,17 @@ func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { if len(pr.StatusCheckRollup.Nodes) == 0 { return } + commit := pr.StatusCheckRollup.Nodes[0].Commit for _, c := range commit.StatusCheckRollup.Contexts.Nodes { - state := c.State // StatusContext + // Nodes are a discriminated union of CheckRun or StatusContext, + // but we can use the State field to disambiguate, rather than using the TypeName. + // First we try to get the State, as if we have a StatusContext + state := c.State + // But if the State is empty, then we assume we actually have a CheckRun if state == "" { - // CheckRun + // If the Status of a CheckRun is COMPLETED, then we want to look more closely + // at the Conclusion, as that contains the relevant information. if c.Status == "COMPLETED" { state = c.Conclusion } else { @@ -281,7 +287,9 @@ func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { summary.Passing++ case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED": summary.Failing++ - default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE" + // We treat anything that isn't Passing or Failing as Pending, which included any future unknown states + // we might get back from the API. + default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE", "STARTUP_FAILURE" summary.Pending++ } summary.Total++ From bfb5e8f1d6bef0f161271a26578fba7baf22b05f Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 16 May 2023 19:43:07 +0200 Subject: [PATCH 20/23] Avoid using named return in PR ChecksStatus See: https://dave.cheney.net/practical-go/presentations/gophercon-israel.html#_avoid_named_return_values --- api/queries_pr.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 5ae5cbb8f..6f121caab 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -261,9 +261,11 @@ type PullRequestChecksStatus struct { Total int } -func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { +func (pr *PullRequest) ChecksStatus() PullRequestChecksStatus { + var summary PullRequestChecksStatus + if len(pr.StatusCheckRollup.Nodes) == 0 { - return + return summary } commit := pr.StatusCheckRollup.Nodes[0].Commit @@ -295,7 +297,7 @@ func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { summary.Total++ } - return + return summary } func (pr *PullRequest) DisplayableReviews() PullRequestReviews { From 83d6dc58780bccb479296b433ac3482f8ae765a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 17 May 2023 14:43:31 +0200 Subject: [PATCH 21/23] Fix windows crash by bumping wincred --- go.mod | 10 +++++----- go.sum | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 3730d469d..e112f2e22 100644 --- a/go.mod +++ b/go.mod @@ -34,8 +34,8 @@ require ( github.com/sourcegraph/jsonrpc2 v0.1.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.7.5 - github.com/zalando/go-keyring v0.2.2 + github.com/stretchr/testify v1.8.1 + github.com/zalando/go-keyring v0.2.3 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/sync v0.1.0 golang.org/x/term v0.6.0 @@ -51,7 +51,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/cli/browser v1.1.0 // indirect github.com/cli/shurcooL-graphql v0.0.3 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/fatih/color v1.7.0 // indirect @@ -73,13 +73,13 @@ require ( github.com/rivo/uniseg v0.4.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect - github.com/stretchr/objx v0.4.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect github.com/yuin/goldmark v1.4.13 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.8.0 // indirect google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 8808b1ec9..49bdc6ae9 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -251,14 +251,16 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= -github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -271,8 +273,8 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= -github.com/zalando/go-keyring v0.2.2 h1:f0xmpYiSrHtSNAVgwip93Cg8tuF45HJM6rHq/A5RI/4= -github.com/zalando/go-keyring v0.2.2/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -393,7 +395,6 @@ golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -403,8 +404,9 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From 7e2bac89bda62a31a20f28ece74edade5b961c2c Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 17 May 2023 14:50:28 +0200 Subject: [PATCH 22/23] Remove old, unused detector fields (#7458) --- .../featuredetection/feature_detection.go | 25 +++------------- .../feature_detection_test.go | 29 ++++--------------- 2 files changed, 9 insertions(+), 45 deletions(-) diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index 6c81ac546..7cd9fa2ff 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -22,30 +22,20 @@ var allIssueFeatures = IssueFeatures{ } type PullRequestFeatures struct { - ReviewDecision bool - StatusCheckRollup bool - BranchProtectionRule bool - MergeQueue bool + MergeQueue bool } var allPullRequestFeatures = PullRequestFeatures{ - ReviewDecision: true, - StatusCheckRollup: true, - BranchProtectionRule: true, - MergeQueue: true, + MergeQueue: true, } type RepositoryFeatures struct { - IssueTemplateMutation bool - IssueTemplateQuery bool PullRequestTemplateQuery bool VisibilityField bool AutoMerge bool } var allRepositoryFeatures = RepositoryFeatures{ - IssueTemplateMutation: true, - IssueTemplateQuery: true, PullRequestTemplateQuery: true, VisibilityField: true, AutoMerge: true, @@ -103,11 +93,7 @@ func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) { // return allPullRequestFeatures, nil // } - features := PullRequestFeatures{ - ReviewDecision: true, - StatusCheckRollup: true, - BranchProtectionRule: true, - } + features := PullRequestFeatures{} var featureDetection struct { PullRequest struct { @@ -137,10 +123,7 @@ func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) { return allRepositoryFeatures, nil } - features := RepositoryFeatures{ - IssueTemplateQuery: true, - IssueTemplateMutation: true, - } + features := RepositoryFeatures{} var featureDetection struct { Repository struct { diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go index 2f2fb5b05..c4944d0de 100644 --- a/internal/featuredetection/feature_detection_test.go +++ b/internal/featuredetection/feature_detection_test.go @@ -93,10 +93,7 @@ func TestPullRequestFeatures(t *testing.T) { `), }, wantFeatures: PullRequestFeatures{ - ReviewDecision: true, - StatusCheckRollup: true, - BranchProtectionRule: true, - MergeQueue: true, + MergeQueue: true, }, wantErr: false, }, @@ -110,10 +107,7 @@ func TestPullRequestFeatures(t *testing.T) { `), }, wantFeatures: PullRequestFeatures{ - ReviewDecision: true, - StatusCheckRollup: true, - BranchProtectionRule: true, - MergeQueue: false, + MergeQueue: false, }, wantErr: false, }, @@ -129,10 +123,7 @@ func TestPullRequestFeatures(t *testing.T) { `), }, wantFeatures: PullRequestFeatures{ - ReviewDecision: true, - StatusCheckRollup: true, - BranchProtectionRule: true, - MergeQueue: true, + MergeQueue: true, }, wantErr: false, }, @@ -169,8 +160,6 @@ func TestRepositoryFeatures(t *testing.T) { name: "github.com", hostname: "github.com", wantFeatures: RepositoryFeatures{ - IssueTemplateMutation: true, - IssueTemplateQuery: true, PullRequestTemplateQuery: true, VisibilityField: true, AutoMerge: true, @@ -184,8 +173,6 @@ func TestRepositoryFeatures(t *testing.T) { `query Repository_fields\b`: `{"data": {}}`, }, wantFeatures: RepositoryFeatures{ - IssueTemplateMutation: true, - IssueTemplateQuery: true, PullRequestTemplateQuery: false, }, wantErr: false, @@ -201,8 +188,6 @@ func TestRepositoryFeatures(t *testing.T) { `), }, wantFeatures: RepositoryFeatures{ - IssueTemplateMutation: true, - IssueTemplateQuery: true, PullRequestTemplateQuery: true, }, wantErr: false, @@ -218,9 +203,7 @@ func TestRepositoryFeatures(t *testing.T) { `), }, wantFeatures: RepositoryFeatures{ - IssueTemplateMutation: true, - IssueTemplateQuery: true, - VisibilityField: true, + VisibilityField: true, }, wantErr: false, }, @@ -235,9 +218,7 @@ func TestRepositoryFeatures(t *testing.T) { `), }, wantFeatures: RepositoryFeatures{ - IssueTemplateMutation: true, - IssueTemplateQuery: true, - AutoMerge: true, + AutoMerge: true, }, wantErr: false, }, From 4a766599966e936d9c587035d7924e7546ed6907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 May 2023 14:58:44 +0000 Subject: [PATCH 23/23] build(deps): bump github.com/stretchr/testify from 1.8.1 to 1.8.2 Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.1 to 1.8.2. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.1...v1.8.2) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e112f2e22..33f10c733 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/sourcegraph/jsonrpc2 v0.1.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/zalando/go-keyring v0.2.3 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index 49bdc6ae9..331bef06c 100644 --- a/go.sum +++ b/go.sum @@ -259,8 +259,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=