Skip to content

Commit a00d927

Browse files
committed
Add release download, upload files on create, upload retrying
1 parent 4e05db9 commit a00d927

File tree

14 files changed

+727
-309
lines changed

14 files changed

+727
-309
lines changed

api/client.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,9 @@ type HTTPError struct {
190190
}
191191

192192
func (err HTTPError) Error() string {
193-
if err.Message != "" {
194-
if msgs := strings.SplitN(err.Message, "\n", 2); len(msgs) > 1 {
195-
return fmt.Sprintf("HTTP %d: %s (%s)\n%s", err.StatusCode, msgs[0], err.RequestURL, msgs[1])
196-
}
193+
if msgs := strings.SplitN(err.Message, "\n", 2); len(msgs) > 1 {
194+
return fmt.Sprintf("HTTP %d: %s (%s)\n%s", err.StatusCode, msgs[0], err.RequestURL, msgs[1])
195+
} else if err.Message != "" {
197196
return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL)
198197
}
199198
return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL)

api/client_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,19 @@ func TestRESTGetDelete(t *testing.T) {
7676
}
7777

7878
func TestRESTError(t *testing.T) {
79-
http := &httpmock.Registry{}
80-
client := NewClient(ReplaceTripper(http))
81-
82-
http.StubResponse(422, bytes.NewBufferString(`{"message": "OH NO"}`))
79+
fakehttp := &httpmock.Registry{}
80+
client := NewClient(ReplaceTripper(fakehttp))
81+
82+
fakehttp.Register(httpmock.MatchAny, func(req *http.Request) (*http.Response, error) {
83+
return &http.Response{
84+
Request: req,
85+
StatusCode: 422,
86+
Body: ioutil.NopCloser(bytes.NewBufferString(`{"message": "OH NO"}`)),
87+
Header: map[string][]string{
88+
"Content-Type": {"application/json; charset=utf-8"},
89+
},
90+
}, nil
91+
})
8392

8493
var httpErr HTTPError
8594
err := client.REST("github.com", "DELETE", "repos/branch", nil, nil)

pkg/cmd/release/create/create.go

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
package list
1+
package create
22

33
import (
4-
"bytes"
5-
"encoding/json"
64
"fmt"
75
"io/ioutil"
86
"net/http"
7+
"strings"
98

10-
"github.com/cli/cli/api"
11-
"github.com/cli/cli/internal/ghinstance"
129
"github.com/cli/cli/internal/ghrepo"
10+
"github.com/cli/cli/pkg/cmd/release/shared"
1311
"github.com/cli/cli/pkg/cmdutil"
1412
"github.com/cli/cli/pkg/iostreams"
1513
"github.com/spf13/cobra"
@@ -20,7 +18,18 @@ type CreateOptions struct {
2018
IO *iostreams.IOStreams
2119
BaseRepo func() (ghrepo.Interface, error)
2220

23-
TagName string
21+
TagName string
22+
Target string
23+
Name string
24+
Body string
25+
BodyProvided bool
26+
Draft bool
27+
Prerelease bool
28+
29+
Assets []*shared.AssetForUpload
30+
31+
// maximum number of simultaneous uploads
32+
Concurrency int
2433
}
2534

2635
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
@@ -29,23 +38,55 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
2938
HttpClient: f.HttpClient,
3039
}
3140

41+
var notesFile string
42+
3243
cmd := &cobra.Command{
33-
Use: "create <tag>",
44+
Use: "create <tag> [<files>...]",
3445
Short: "Create a new release",
35-
Args: cobra.ExactArgs(1),
46+
Args: cobra.MinimumNArgs(1),
3647
RunE: func(cmd *cobra.Command, args []string) error {
3748
// support `-R, --repo` override
3849
opts.BaseRepo = f.BaseRepo
3950

4051
opts.TagName = args[0]
4152

53+
var err error
54+
opts.Assets, err = shared.AssetsFromArgs(args[1:])
55+
if err != nil {
56+
return err
57+
}
58+
59+
opts.Concurrency = 5
60+
61+
opts.BodyProvided = cmd.Flags().Changed("notes")
62+
if notesFile != "" {
63+
var b []byte
64+
if notesFile == "-" {
65+
b, err = ioutil.ReadAll(opts.IO.In)
66+
} else {
67+
b, err = ioutil.ReadFile(notesFile)
68+
}
69+
if err != nil {
70+
return err
71+
}
72+
opts.Body = string(b)
73+
opts.BodyProvided = true
74+
}
75+
4276
if runF != nil {
4377
return runF(opts)
4478
}
4579
return createRun(opts)
4680
},
4781
}
4882

83+
cmd.Flags().BoolVarP(&opts.Draft, "draft", "d", false, "Save the release as a draft instead of publishing it")
84+
cmd.Flags().BoolVarP(&opts.Prerelease, "prerelease", "p", false, "Mark the release as a prerelease")
85+
cmd.Flags().StringVar(&opts.Target, "target", "", "Target `branch` or commit SHA (default: main branch)")
86+
cmd.Flags().StringVarP(&opts.Name, "title", "t", "", "Release title")
87+
cmd.Flags().StringVarP(&opts.Body, "notes", "n", "", "Release notes")
88+
cmd.Flags().StringVarP(&notesFile, "notes-file", "F", "", "Read release notes from `file`")
89+
4990
return cmd
5091
}
5192

@@ -61,47 +102,45 @@ func createRun(opts *CreateOptions) error {
61102
}
62103

63104
params := map[string]interface{}{
64-
"tag_name": opts.TagName,
105+
"tag_name": opts.TagName,
106+
"draft": opts.Draft,
107+
"prerelease": opts.Prerelease,
108+
"name": opts.Name,
109+
"body": opts.Body,
65110
}
66-
67-
bodyBytes, err := json.Marshal(params)
68-
if err != nil {
69-
return err
111+
if opts.Target != "" {
112+
params["target_commitish"] = opts.Target
70113
}
71114

72-
path := fmt.Sprintf("repos/%s/%s/releases", baseRepo.RepoOwner(), baseRepo.RepoName())
73-
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
74-
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
75-
if err != nil {
76-
return err
115+
hasAssets := len(opts.Assets) > 0
116+
if hasAssets {
117+
params["draft"] = true
77118
}
78119

79-
req.Header.Set("Content-Type", "application/json; charset=utf-8")
80-
81-
resp, err := httpClient.Do(req)
82-
if err != nil {
83-
return err
84-
}
85-
defer resp.Body.Close()
86-
87-
success := resp.StatusCode >= 200 && resp.StatusCode < 300
88-
if !success {
89-
return api.HandleHTTPError(resp)
90-
}
91-
92-
b, err := ioutil.ReadAll(resp.Body)
120+
newRelease, err := createRelease(httpClient, baseRepo, params)
93121
if err != nil {
94122
return err
95123
}
96124

97-
var newRelease struct {
98-
HTMLURL string `json:"html_url"`
99-
AssetsURL string `json:"assets_url"`
100-
}
101-
102-
err = json.Unmarshal(b, &newRelease)
103-
if err != nil {
104-
return err
125+
if hasAssets {
126+
uploadURL := newRelease.UploadURL
127+
if idx := strings.IndexRune(uploadURL, '{'); idx > 0 {
128+
uploadURL = uploadURL[:idx]
129+
}
130+
131+
opts.IO.StartProgressIndicator()
132+
err = shared.ConcurrentUpload(httpClient, uploadURL, opts.Concurrency, opts.Assets)
133+
opts.IO.StopProgressIndicator()
134+
if err != nil {
135+
return err
136+
}
137+
138+
if !opts.Draft {
139+
err := publishRelease(httpClient, newRelease.URL)
140+
if err != nil {
141+
return err
142+
}
143+
}
105144
}
106145

107146
fmt.Fprintf(opts.IO.Out, "%s\n", newRelease.HTMLURL)

pkg/cmd/release/create/http.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package create
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"io/ioutil"
9+
"net/http"
10+
11+
"github.com/cli/cli/api"
12+
"github.com/cli/cli/internal/ghinstance"
13+
"github.com/cli/cli/internal/ghrepo"
14+
"github.com/cli/cli/pkg/cmd/release/shared"
15+
)
16+
17+
func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[string]interface{}) (*shared.Release, error) {
18+
bodyBytes, err := json.Marshal(params)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
path := fmt.Sprintf("repos/%s/%s/releases", repo.RepoOwner(), repo.RepoName())
24+
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
25+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
req.Header.Set("Content-Type", "application/json; charset=utf-8")
31+
32+
resp, err := httpClient.Do(req)
33+
if err != nil {
34+
return nil, err
35+
}
36+
defer resp.Body.Close()
37+
38+
success := resp.StatusCode >= 200 && resp.StatusCode < 300
39+
if !success {
40+
return nil, api.HandleHTTPError(resp)
41+
}
42+
43+
b, err := ioutil.ReadAll(resp.Body)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
var newRelease shared.Release
49+
err = json.Unmarshal(b, &newRelease)
50+
return &newRelease, err
51+
}
52+
53+
func publishRelease(httpClient *http.Client, releaseURL string) error {
54+
req, err := http.NewRequest("PATCH", releaseURL, bytes.NewBufferString(`{"draft":false}`))
55+
if err != nil {
56+
return err
57+
}
58+
59+
req.Header.Add("Content-Type", "application/json")
60+
61+
resp, err := httpClient.Do(req)
62+
if err != nil {
63+
return err
64+
}
65+
66+
defer resp.Body.Close()
67+
if resp.StatusCode > 299 {
68+
return api.HandleHTTPError(resp)
69+
}
70+
71+
_, err = io.Copy(ioutil.Discard, resp.Body)
72+
return err
73+
}

0 commit comments

Comments
 (0)