Repository: cli/cli
Pull request #8474 (2023-12-19 20:38:46)

Add `--json` export flag for release list
It was similarly solved in https://github.com/cli/cli/pull/3869 but for the `release list` command.

In addition, I added the same logic for empty results as explained in https://github.com/cli/cli/pull/6419.


```bash
$ go run cmd/gh/main.go \
  release list \
  --limit 1 \
  --json name,tagName \
  --jq '.[]' --repo cli/cli
{"name":"GitHub CLI 2.40.1","tagName":"v2.40.1"}
```

```bash
$ go run cmd/gh/main.go \
  release list --help
List releases in a repository

For more information about output formatting flags, see `gh help formatting`.

USAGE
  gh release list [flags]

FLAGS
      --exclude-drafts         Exclude draft releases
      --exclude-pre-releases   Exclude pre-releases
  -q, --jq expression          Filter JSON output using a jq expression
      --json fields            Output JSON with the specified fields
  -L, --limit int              Maximum number of items to fetch (default 30)
  -t, --template string        Format JSON output using a Go template; see "gh help formatting"
```

Closes https://github.com/cli/cli/issues/4572

andyfeller: @samcoe @williammartin : Are these changes necessary because we have different `Release` types floating around?

https://github.com/cli/cli/blob/3e1268fe828f1147681852019d34848058d7c44a/pkg/cmd/release/shared/fetch.go#L23-L124

https://github.com/cli/cli/blob/3e1268fe828f1147681852019d34848058d7c44a/pkg/cmd/release/list/http.go#L14-L32

I haven't dug into the history behind the _"shared"_ version especially given it doesn't seem like the only implementation.  I worry having these separate code paths makes it difficult to maintain and ensure a consistent experience.

Patch:
diff --git a/pkg/cmd/release/list/http.go b/pkg/cmd/release/list/http.go
index 72ddcacf6..6ef5f0328 100644
--- a/pkg/cmd/release/list/http.go
+++ b/pkg/cmd/release/list/http.go
@@ -6,9 +6,20 @@ import (
 
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/shurcooL/githubv4"
 )
 
+var releaseFields = []string{
+	"name",
+	"tagName",
+	"isDraft",
+	"isLatest",
+	"isPrerelease",
+	"createdAt",
+	"publishedAt",
+}
+
 type Release struct {
 	Name         string
 	TagName      string
@@ -19,6 +30,10 @@ type Release struct {
 	PublishedAt  time.Time
 }
 
+func (r *Release) ExportData(fields []string) map[string]interface{} {
+	return cmdutil.StructExportData(r, fields)
+}
+
 func fetchReleases(httpClient *http.Client, repo ghrepo.Interface, limit int, excludeDrafts bool, excludePreReleases bool) ([]Release, error) {
 	type responseData struct {
 		Repository struct {
diff --git a/pkg/cmd/release/list/list.go b/pkg/cmd/release/list/list.go
index e6efc6062..8d88e95a7 100644
--- a/pkg/cmd/release/list/list.go
+++ b/pkg/cmd/release/list/list.go
@@ -18,6 +18,8 @@ type ListOptions struct {
 	IO         *iostreams.IOStreams
 	BaseRepo   func() (ghrepo.Interface, error)
 
+	Exporter cmdutil.Exporter
+
 	LimitResults       int
 	ExcludeDrafts      bool
 	ExcludePreReleases bool
@@ -48,6 +50,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
 	cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of items to fetch")
 	cmd.Flags().BoolVar(&opts.ExcludeDrafts, "exclude-drafts", false, "Exclude draft releases")
 	cmd.Flags().BoolVar(&opts.ExcludePreReleases, "exclude-pre-releases", false, "Exclude pre-releases")
+	cmdutil.AddJSONFlags(cmd, &opts.Exporter, releaseFields)
 
 	return cmd
 }
@@ -68,7 +71,7 @@ func listRun(opts *ListOptions) error {
 		return err
 	}
 
-	if len(releases) == 0 {
+	if len(releases) == 0 && opts.Exporter == nil {
 		return cmdutil.NewNoResultsError("no releases found")
 	}
 
@@ -78,6 +81,10 @@ func listRun(opts *ListOptions) error {
 		fmt.Fprintf(opts.IO.ErrOut, "failed to start pager: %v\n", err)
 	}
 
+	if opts.Exporter != nil {
+		return opts.Exporter.Write(opts.IO, releases)
+	}
+
 	table := tableprinter.New(opts.IO, tableprinter.WithHeader("Title", "Type", "Tag name", "Published"))
 	iofmt := opts.IO.ColorScheme()
 	for _, rel := range releases {


Test patch:
diff --git a/pkg/cmd/release/list/list_test.go b/pkg/cmd/release/list/list_test.go
index df883ead2..aafccd11e 100644
--- a/pkg/cmd/release/list/list_test.go
+++ b/pkg/cmd/release/list/list_test.go
@@ -214,3 +214,24 @@ func Test_listRun(t *testing.T) {
 		})
 	}
 }
+
+func TestExportReleases(t *testing.T) {
+	ios, _, stdout, _ := iostreams.Test()
+	createdAt, _ := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z")
+	publishedAt, _ := time.Parse(time.RFC3339, "2024-02-01T00:00:00Z")
+	rs := []Release{{
+		Name:         "v1",
+		TagName:      "tag",
+		IsDraft:      true,
+		IsLatest:     false,
+		IsPrerelease: true,
+		CreatedAt:    createdAt,
+		PublishedAt:  publishedAt,
+	}}
+	exporter := cmdutil.NewJSONExporter()
+	exporter.SetFields(releaseFields)
+	require.NoError(t, exporter.Write(ios, rs))
+	require.JSONEq(t,
+		`[{"createdAt":"2024-01-01T00:00:00Z","isDraft":true,"isLatest":false,"isPrerelease":true,"name":"v1","publishedAt":"2024-02-01T00:00:00Z","tagName":"tag"}]`,
+		stdout.String())
+}