diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index ee9ad3429..e667f65ef 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -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 } diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index aec10911a..eef4e05b5 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -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, diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index 2a0b65468..f970d46f1 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -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 +}