Repository: cli/cli

Issue:
`gh release download` errors confusingly when release is a draft
### Describe the bug

Running `gh release download` targeting a draft release errors in a confusing manner:

```
➜ gh release download v0.0.2 --archive=zip
Get "": unsupported protocol scheme ""
```

### Affected version

Please run `gh version` and paste the output below.

### Steps to reproduce the behavior

Steps to reproduce the behavior:

1. Create a draft release
1. Set it to use a tag that will be created when the release is published
1. Save it but don't publish it
1. Use the `gh release download -R org/repo <tag-from-step-2> --archive=zip`
1. Watch the GitHub CLI throw the Get "": unsupported protocol scheme "" error

This occurs because we attempt to use the `zipball_url`, which is unmarshaled into an empty string here: https://github.com/cli/cli/blob/408e21ebdddf9cd14289e49135389a6e5125eff4/pkg/cmd/release/download/download.go#L168

```
> GET /repos/williammartin-test-org/test-repo/releases/212464261 HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Type: application/json; charset=utf-8
> Time-Zone: Europe/Amsterdam
> User-Agent: GitHub CLI 2.68.0

...

{
  "url": "https://api.github.com/repos/williammartin-test-org/test-repo/releases/212464261",
...
  "tarball_url": null,
  "zipball_url": null,
  "body": ""
}
```

Hints from issue comments:
@Mrchiejustic: - [ ] 

> /Duplicate of #10729 

Solution patch:
diff --git a/pkg/cmd/release/download/download.go b/pkg/cmd/release/download/download.go
index cdf6135b6..b90721412 100644
--- a/pkg/cmd/release/download/download.go
+++ b/pkg/cmd/release/download/download.go
@@ -165,10 +165,24 @@ func downloadRun(opts *DownloadOptions) error {
 	var toDownload []shared.ReleaseAsset
 	isArchive := false
 	if opts.ArchiveType != "" {
-		var archiveURL = release.ZipballURL
+		var archiveURL string
 		if opts.ArchiveType == "tar.gz" {
 			archiveURL = release.TarballURL
+		} else {
+			archiveURL = release.ZipballURL
+		}
+
+		if archiveURL == "" {
+			errMessage := fmt.Sprintf(
+				"release %q with tag %q, does not have a %q archive asset.",
+				release.Name, release.TagName, opts.ArchiveType,
+			)
+			if release.IsDraft {
+				errMessage += " Most likely, this is because it is a draft."
+			}
+			return errors.New(errMessage)
 		}
+
 		// create pseudo-Asset with no name and pointing to ZipBallURL or TarBallURL
 		toDownload = append(toDownload, shared.ReleaseAsset{APIURL: archiveURL})
 		isArchive = true



Test patch:
diff --git a/pkg/cmd/release/download/download_test.go b/pkg/cmd/release/download/download_test.go
index 78709dd57..9337c9b65 100644
--- a/pkg/cmd/release/download/download_test.go
+++ b/pkg/cmd/release/download/download_test.go
@@ -183,6 +183,7 @@ func Test_downloadRun(t *testing.T) {
 		name       string
 		isTTY      bool
 		opts       DownloadOptions
+		httpStubs  func(*httpmock.Registry)
 		wantErr    string
 		wantStdout string
 		wantStderr string
@@ -196,6 +197,24 @@ func Test_downloadRun(t *testing.T) {
 				Destination: ".",
 				Concurrency: 2,
 			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						"url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						"url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						"url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
+					"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
+				}`)
+
+				reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
+				reg.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`))
+				reg.Register(httpmock.REST("GET", "assets/5678"), httpmock.StringResponse(`5678`))
+			},
 			wantStdout: ``,
 			wantStderr: ``,
 			wantFiles: []string{
@@ -213,6 +232,23 @@ func Test_downloadRun(t *testing.T) {
 				Destination:  "tmp/assets",
 				Concurrency:  2,
 			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						"url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						"url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						"url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
+					"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
+				}`)
+
+				reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
+				reg.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`))
+			},
 			wantStdout: ``,
 			wantStderr: ``,
 			wantFiles: []string{
@@ -229,6 +265,20 @@ func Test_downloadRun(t *testing.T) {
 				Destination:  ".",
 				Concurrency:  2,
 			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						"url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						"url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						"url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
+					"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
+				}`)
+			},
 			wantStdout: ``,
 			wantStderr: ``,
 			wantErr:    "no assets match the file pattern",
@@ -242,6 +292,30 @@ func Test_downloadRun(t *testing.T) {
 				Destination: "tmp/packages",
 				Concurrency: 2,
 			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						"url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						"url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						"url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
+					"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
+				}`)
+
+				reg.Register(
+					httpmock.REST(
+						"GET",
+						"repos/OWNER/REPO/zipball/v1.2.3",
+					),
+					httpmock.WithHeader(
+						httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=zipball.zip",
+					),
+				)
+			},
 			wantStdout: ``,
 			wantStderr: ``,
 			wantFiles: []string{
@@ -257,6 +331,30 @@ func Test_downloadRun(t *testing.T) {
 				Destination: "tmp/packages",
 				Concurrency: 2,
 			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						"url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						"url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						"url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
+					"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
+				}`)
+
+				reg.Register(
+					httpmock.REST(
+						"GET",
+						"repos/OWNER/REPO/tarball/v1.2.3",
+					),
+					httpmock.WithHeader(
+						httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz",
+					),
+				)
+			},
 			wantStdout: ``,
 			wantStderr: ``,
 			wantFiles: []string{
@@ -273,6 +371,30 @@ func Test_downloadRun(t *testing.T) {
 				Concurrency: 2,
 				ArchiveType: "tar.gz",
 			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						"url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						"url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						"url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
+					"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
+				}`)
+
+				reg.Register(
+					httpmock.REST(
+						"GET",
+						"repos/OWNER/REPO/tarball/v1.2.3",
+					),
+					httpmock.WithHeader(
+						httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz",
+					),
+				)
+			},
 			wantStdout: ``,
 			wantStderr: ``,
 			wantFiles: []string{
@@ -289,6 +411,22 @@ func Test_downloadRun(t *testing.T) {
 				Concurrency:  2,
 				FilePatterns: []string{"*windows-32bit.zip"},
 			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						"url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						"url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						"url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
+					"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
+				}`)
+
+				reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
+			},
 			wantStdout: ``,
 			wantStderr: ``,
 			wantFiles: []string{
@@ -305,9 +443,85 @@ func Test_downloadRun(t *testing.T) {
 				Concurrency:  2,
 				FilePatterns: []string{"*windows-32bit.zip"},
 			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						"url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						"url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						"url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
+					"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
+				}`)
+
+				reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
+			},
 			wantStdout: `1234`,
 			wantStderr: ``,
 		},
+		{
+			name:  "draft release with null tarball_url and zipball_url",
+			isTTY: true,
+			opts: DownloadOptions{
+				TagName:     "v1.2.3",
+				ArchiveType: "tar.gz",
+				Destination: "tmp/packages",
+				Concurrency: 2,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"tag_name": "v1.2.3",
+					"name": "patch-36",
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						  "url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						  "url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						  "url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": null,
+					"zipball_url": null,
+					"draft": true
+				}`)
+			},
+			wantStdout: ``,
+			wantStderr: ``,
+			wantErr:    "release \"patch-36\" with tag \"v1.2.3\", does not have a \"tar.gz\" archive asset. Most likely, this is because it is a draft.",
+		},
+		{
+			name:  "non-draft release with null tarball_url and zipball_url",
+			isTTY: true,
+			opts: DownloadOptions{
+				TagName:     "v1.2.3",
+				ArchiveType: "tar.gz",
+				Destination: "tmp/packages",
+				Concurrency: 2,
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
+					"tag_name": "v1.2.3",
+					"name": "patch-36",
+					"assets": [
+						{ "name": "windows-32bit.zip", "size": 12,
+						  "url": "https://api.github.com/assets/1234" },
+						{ "name": "windows-64bit.zip", "size": 34,
+						  "url": "https://api.github.com/assets/3456" },
+						{ "name": "linux.tgz", "size": 56,
+						  "url": "https://api.github.com/assets/5678" }
+					],
+					"tarball_url": null,
+					"zipball_url": null,
+					"draft": false
+				}`)
+			},
+			wantStdout: ``,
+			wantStderr: ``,
+			wantErr:    "release \"patch-36\" with tag \"v1.2.3\", does not have a \"tar.gz\" archive asset.",
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -324,41 +538,11 @@ func Test_downloadRun(t *testing.T) {
 			ios.SetStderrTTY(tt.isTTY)
 
 			fakeHTTP := &httpmock.Registry{}
-			shared.StubFetchRelease(t, fakeHTTP, "OWNER", "REPO", tt.opts.TagName, `{
-				"assets": [
-					{ "name": "windows-32bit.zip", "size": 12,
-					  "url": "https://api.github.com/assets/1234" },
-					{ "name": "windows-64bit.zip", "size": 34,
-					  "url": "https://api.github.com/assets/3456" },
-					{ "name": "linux.tgz", "size": 56,
-					  "url": "https://api.github.com/assets/5678" }
-				],
-				"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
-				"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
-			}`)
-			fakeHTTP.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
-			fakeHTTP.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`))
-			fakeHTTP.Register(httpmock.REST("GET", "assets/5678"), httpmock.StringResponse(`5678`))
-
-			fakeHTTP.Register(
-				httpmock.REST(
-					"GET",
-					"repos/OWNER/REPO/tarball/v1.2.3",
-				),
-				httpmock.WithHeader(
-					httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz",
-				),
-			)
-
-			fakeHTTP.Register(
-				httpmock.REST(
-					"GET",
-					"repos/OWNER/REPO/zipball/v1.2.3",
-				),
-				httpmock.WithHeader(
-					httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=zipball.zip",
-				),
-			)
+			defer fakeHTTP.Verify(t)
+
+			if tt.httpStubs != nil {
+				tt.httpStubs(fakeHTTP)
+			}
 
 			tt.opts.IO = ios
 			tt.opts.HttpClient = func() (*http.Client, error) {