release create: clean up leftover draft release on upload/publish failure

This commit is contained in:
Mislav Marohnić 2022-12-14 18:00:30 +01:00
parent 36ffbe18de
commit cbeed671dd
No known key found for this signature in database
3 changed files with 136 additions and 3 deletions

View file

@ -426,6 +426,7 @@ func createRun(opts *CreateOptions) error {
}
hasAssets := len(opts.Assets) > 0
draftWhileUploading := false
if hasAssets && !opts.Draft {
// Check for an existing release
@ -437,6 +438,7 @@ func createRun(opts *CreateOptions) error {
}
}
// Save the release initially as draft and publish it after all assets have finished uploading
draftWhileUploading = true
params["draft"] = true
}
@ -445,6 +447,16 @@ func createRun(opts *CreateOptions) error {
return err
}
cleanupDraftRelease := func(err error) error {
if !draftWhileUploading {
return err
}
if cleanupErr := deleteRelease(httpClient, newRelease); cleanupErr != nil {
return fmt.Errorf("%w\ncleaning up draft failed: %v", err, cleanupErr)
}
return err
}
if hasAssets {
uploadURL := newRelease.UploadURL
if idx := strings.IndexRune(uploadURL, '{'); idx > 0 {
@ -455,13 +467,13 @@ func createRun(opts *CreateOptions) error {
err = shared.ConcurrentUpload(httpClient, uploadURL, opts.Concurrency, opts.Assets)
opts.IO.StopProgressIndicator()
if err != nil {
return err
return cleanupDraftRelease(err)
}
if !opts.Draft {
if draftWhileUploading {
rel, err := publishRelease(httpClient, newRelease.APIURL, opts.DiscussionCategory)
if err != nil {
return err
return cleanupDraftRelease(err)
}
newRelease = rel
}

View file

@ -728,6 +728,102 @@ func Test_createRun(t *testing.T) {
wantStderr: ``,
wantErr: `a release with the same tag name already exists: v1.2.3`,
},
{
name: "clean up draft after uploading files fails",
isTTY: false,
opts: CreateOptions{
TagName: "v1.2.3",
Name: "",
Body: "",
BodyProvided: true,
Draft: false,
Target: "",
Assets: []*shared.AssetForUpload{
{
Name: "ball.tgz",
Open: func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
},
},
},
Concurrency: 1,
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`))
reg.Register(httpmock.REST("POST", "assets/upload"), httpmock.StatusStringResponse(500, `{}`))
reg.Register(httpmock.REST("DELETE", "releases/123"), httpmock.StatusStringResponse(204, ``))
},
wantStdout: ``,
wantStderr: ``,
wantErr: `HTTP 500 (https://api.github.com/assets/upload?label=&name=ball.tgz)`,
},
{
name: "clean up draft after publishing fails",
isTTY: false,
opts: CreateOptions{
TagName: "v1.2.3",
Name: "",
Body: "",
BodyProvided: true,
Draft: false,
Target: "",
Assets: []*shared.AssetForUpload{
{
Name: "ball.tgz",
Open: func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
},
},
},
Concurrency: 1,
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`))
reg.Register(httpmock.REST("POST", "assets/upload"), httpmock.StatusStringResponse(201, `{}`))
reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.StatusStringResponse(500, `{}`))
reg.Register(httpmock.REST("DELETE", "releases/123"), httpmock.StatusStringResponse(204, ``))
},
wantStdout: ``,
wantStderr: ``,
wantErr: `HTTP 500 (https://api.github.com/releases/123)`,
},
{
name: "upload files but release already exists",
isTTY: true,
opts: CreateOptions{
TagName: "v1.2.3",
Name: "",
Body: "",
BodyProvided: true,
Draft: false,
Target: "",
Assets: []*shared.AssetForUpload{
{
Name: "ball.tgz",
Open: func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
},
},
},
Concurrency: 1,
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(200, ``))
},
wantStdout: ``,
wantStderr: ``,
wantErr: `a release with the same tag name already exists: v1.2.3`,
},
{
name: "upload files and create discussion",
isTTY: true,

View file

@ -225,3 +225,28 @@ func publishRelease(httpClient *http.Client, releaseURL string, discussionCatego
err = json.Unmarshal(b, &release)
return &release, err
}
func deleteRelease(httpClient *http.Client, release *shared.Release) error {
req, err := http.NewRequest("DELETE", release.APIURL, nil)
if err != nil {
return err
}
resp, err := httpClient.Do(req)
if err != nil {
return err
}
if resp.Body != nil {
defer resp.Body.Close()
}
success := resp.StatusCode >= 200 && resp.StatusCode < 300
if !success {
return api.HandleHTTPError(resp)
}
if resp.StatusCode != 204 {
_, _ = io.Copy(io.Discard, resp.Body)
}
return nil
}