[gh release create] Fail when there are no new commits since the most recent release
This commit is contained in:
parent
55579582e2
commit
49fd7a5756
4 changed files with 172 additions and 4 deletions
|
|
@ -60,6 +60,7 @@ type CreateOptions struct {
|
|||
NotesStartTag string
|
||||
VerifyTag bool
|
||||
NotesFromTag bool
|
||||
OnlyNew bool
|
||||
}
|
||||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
|
||||
|
|
@ -194,6 +195,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
cmdutil.NilBoolFlag(cmd, &opts.IsLatest, "latest", "", "Mark this release as \"Latest\" (default [automatic based on date and version]). --latest=false to explicitly NOT set as latest")
|
||||
cmd.Flags().BoolVarP(&opts.VerifyTag, "verify-tag", "", false, "Abort in case the git tag doesn't already exist in the remote repository")
|
||||
cmd.Flags().BoolVarP(&opts.NotesFromTag, "notes-from-tag", "", false, "Automatically generate notes from annotated tag")
|
||||
cmd.Flags().BoolVar(&opts.OnlyNew, "only-new", false, "Create release only if there are new commits since the last release")
|
||||
|
||||
_ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "target")
|
||||
|
||||
|
|
@ -211,6 +213,16 @@ func createRun(opts *CreateOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if opts.OnlyNew {
|
||||
isNew, err := isNewRelease(httpClient, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isNew {
|
||||
return fmt.Errorf("no new commits since the last release")
|
||||
}
|
||||
}
|
||||
|
||||
var existingTag bool
|
||||
if opts.TagName == "" {
|
||||
tags, err := getTags(httpClient, baseRepo, 5)
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ func Test_NewCmdCreate(t *testing.T) {
|
|||
Concurrency: 5,
|
||||
Assets: []*shared.AssetForUpload(nil),
|
||||
VerifyTag: false,
|
||||
OnlyNew: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -87,6 +88,7 @@ func Test_NewCmdCreate(t *testing.T) {
|
|||
Concurrency: 5,
|
||||
Assets: []*shared.AssetForUpload(nil),
|
||||
VerifyTag: false,
|
||||
OnlyNew: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -347,6 +349,19 @@ func Test_NewCmdCreate(t *testing.T) {
|
|||
isTTY: false,
|
||||
wantErr: "using `--notes-from-tag` with `--generate-notes` or `--notes-start-tag` is not supported",
|
||||
},
|
||||
{
|
||||
name: "with --only-new",
|
||||
args: "v1.2.3 --only-new",
|
||||
isTTY: false,
|
||||
want: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
BodyProvided: false,
|
||||
Concurrency: 5,
|
||||
Assets: []*shared.AssetForUpload(nil),
|
||||
NotesFromTag: false,
|
||||
OnlyNew: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -402,6 +417,7 @@ func Test_NewCmdCreate(t *testing.T) {
|
|||
assert.Equal(t, tt.want.IsLatest, opts.IsLatest)
|
||||
assert.Equal(t, tt.want.VerifyTag, opts.VerifyTag)
|
||||
assert.Equal(t, tt.want.NotesFromTag, opts.NotesFromTag)
|
||||
assert.Equal(t, tt.want.OnlyNew, opts.OnlyNew)
|
||||
|
||||
require.Equal(t, len(tt.want.Assets), len(opts.Assets))
|
||||
for i := range tt.want.Assets {
|
||||
|
|
@ -460,6 +476,100 @@ func Test_createRun(t *testing.T) {
|
|||
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "create a release if there are new commits and the last release does not exist",
|
||||
isTTY: true,
|
||||
opts: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
Name: "The Big 1.2",
|
||||
Body: "* Fixed bugs",
|
||||
BodyProvided: true,
|
||||
Target: "",
|
||||
OnlyNew: true,
|
||||
},
|
||||
runStubs: defaultRunStubs,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(404, `{
|
||||
"message": "Not Found",
|
||||
"documentation_url": "https://docs.github.com/rest/releases/releases#get-the-latest-release",
|
||||
"status": "404"
|
||||
}`))
|
||||
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(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"
|
||||
}`, func(params map[string]interface{}) {
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"tag_name": "v1.2.3",
|
||||
"name": "The Big 1.2",
|
||||
"body": "* Fixed bugs",
|
||||
"draft": false,
|
||||
"prerelease": false,
|
||||
}, params)
|
||||
}))
|
||||
},
|
||||
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "create a release if there are new commits and the last release exists",
|
||||
isTTY: true,
|
||||
opts: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
Name: "The Big 1.2",
|
||||
Body: "* Fixed bugs",
|
||||
BodyProvided: true,
|
||||
Target: "",
|
||||
OnlyNew: true,
|
||||
},
|
||||
runStubs: defaultRunStubs,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(200, `{
|
||||
"tag_name": "v1.2.2"
|
||||
}`))
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/compare/v1.2.2...HEAD"), httpmock.StatusStringResponse(200, `{
|
||||
"status": "ahead"
|
||||
}`))
|
||||
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(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"
|
||||
}`, func(params map[string]interface{}) {
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"tag_name": "v1.2.3",
|
||||
"name": "The Big 1.2",
|
||||
"body": "* Fixed bugs",
|
||||
"draft": false,
|
||||
"prerelease": false,
|
||||
}, params)
|
||||
}))
|
||||
},
|
||||
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "create a release if there are no new commits but the last release exists",
|
||||
isTTY: true,
|
||||
opts: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
Name: "The Big 1.2",
|
||||
Body: "* Fixed bugs",
|
||||
BodyProvided: true,
|
||||
Target: "",
|
||||
OnlyNew: true,
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(200, `{
|
||||
"tag_name": "v1.2.2"
|
||||
}`))
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/compare/v1.2.2...HEAD"), httpmock.StatusStringResponse(200, `{
|
||||
"status": "identical"
|
||||
}`))
|
||||
},
|
||||
wantErr: "no new commits since the last release",
|
||||
wantStdout: "",
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "with discussion category",
|
||||
isTTY: true,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package create
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -299,3 +300,48 @@ func tokenHasWorkflowScope(resp *http.Response) bool {
|
|||
|
||||
return slices.Contains(strings.Split(scopes, ","), "workflow")
|
||||
}
|
||||
|
||||
// isNewRelease checks if there are new commits since the latest release.
|
||||
func isNewRelease(httpClient *http.Client, repo ghrepo.Interface) (bool, error) {
|
||||
ctx := context.Background()
|
||||
release, err := shared.FetchLatestRelease(ctx, httpClient, repo)
|
||||
if err != nil {
|
||||
if errors.Is(err, shared.ErrReleaseNotFound) {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
tagName := release.TagName
|
||||
path := fmt.Sprintf("repos/%s/%s/compare/%s...HEAD?per_page=1", repo.RepoOwner(), repo.RepoName(), tagName)
|
||||
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return false, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
type comparisonStatus struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var cmpStatus comparisonStatus
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cmpStatus); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
isNew := cmpStatus.Status == "ahead"
|
||||
return isNew, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ func (rel *Release) ExportData(fields []string) map[string]interface{} {
|
|||
return data
|
||||
}
|
||||
|
||||
var errNotFound = errors.New("release not found")
|
||||
var ErrReleaseNotFound = errors.New("release not found")
|
||||
|
||||
type fetchResult struct {
|
||||
release *Release
|
||||
|
|
@ -150,7 +150,7 @@ func FetchRelease(ctx context.Context, httpClient *http.Client, repo ghrepo.Inte
|
|||
}()
|
||||
|
||||
res := <-results
|
||||
if errors.Is(res.error, errNotFound) {
|
||||
if errors.Is(res.error, ErrReleaseNotFound) {
|
||||
res = <-results
|
||||
cancel() // satisfy the linter even though no goroutines are running anymore
|
||||
} else {
|
||||
|
|
@ -190,7 +190,7 @@ func fetchDraftRelease(ctx context.Context, httpClient *http.Client, repo ghrepo
|
|||
}
|
||||
|
||||
if query.Repository.Release == nil || !query.Repository.Release.IsDraft {
|
||||
return nil, errNotFound
|
||||
return nil, ErrReleaseNotFound
|
||||
}
|
||||
|
||||
// Then, use REST to get information about the draft release. In theory, we could have fetched
|
||||
|
|
@ -213,7 +213,7 @@ func fetchReleasePath(ctx context.Context, httpClient *http.Client, host string,
|
|||
|
||||
if resp.StatusCode == 404 {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
return nil, errNotFound
|
||||
return nil, ErrReleaseNotFound
|
||||
} else if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue