diff --git a/pkg/cmd/release/delete-asset/delete_asset_test.go b/pkg/cmd/release/delete-asset/delete_asset_test.go index f28f493d0..e302ca3d1 100644 --- a/pkg/cmd/release/delete-asset/delete_asset_test.go +++ b/pkg/cmd/release/delete-asset/delete_asset_test.go @@ -157,6 +157,7 @@ func Test_deleteAssetRun(t *testing.T) { ios.SetStderrTTY(tt.isTTY) fakeHTTP := &httpmock.Registry{} + defer fakeHTTP.Verify(t) shared.StubFetchRelease(t, fakeHTTP, "OWNER", "REPO", tt.opts.TagName, `{ "tag_name": "v1.2.3", "draft": false, diff --git a/pkg/cmd/release/download/download_test.go b/pkg/cmd/release/download/download_test.go index c2260568a..782056314 100644 --- a/pkg/cmd/release/download/download_test.go +++ b/pkg/cmd/release/download/download_test.go @@ -516,7 +516,7 @@ func Test_downloadRun_cloberAndSkip(t *testing.T) { tt.opts.IO = ios reg := &httpmock.Registry{} - // defer reg.Verify(t) // FIXME: intermittetly fails due to StubFetchRelease internals + defer reg.Verify(t) shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{ "assets": [ { "name": "windows-64bit.zip", "size": 34, diff --git a/pkg/cmd/release/edit/edit.go b/pkg/cmd/release/edit/edit.go index cac9c0e48..0360911bf 100644 --- a/pkg/cmd/release/edit/edit.go +++ b/pkg/cmd/release/edit/edit.go @@ -26,6 +26,7 @@ type EditOptions struct { Draft *bool Prerelease *bool IsLatest *bool + VerifyTag bool } func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command { @@ -81,6 +82,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman cmd.Flags().StringVar(&opts.Target, "target", "", "Target `branch` or full commit SHA (default: main branch)") cmd.Flags().StringVar(&opts.TagName, "tag", "", "The name of the tag") cmd.Flags().StringVarP(¬esFile, "notes-file", "F", "", "Read release notes from `file` (use \"-\" to read from standard input)") + cmd.Flags().BoolVar(&opts.VerifyTag, "verify-tag", false, "Abort in case the git tag doesn't already exist in the remote repository") _ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "target") @@ -110,6 +112,17 @@ func editRun(tag string, opts *EditOptions) error { params["tag_name"] = release.TagName } + if opts.VerifyTag && opts.TagName != "" { + remoteTagPresent, err := remoteTagExists(httpClient, baseRepo, opts.TagName) + if err != nil { + return err + } + if !remoteTagPresent { + return fmt.Errorf("tag %s doesn't exist in the repo %s, aborting due to --verify-tag flag", + opts.TagName, ghrepo.FullName(baseRepo)) + } + } + editedRelease, err := editRelease(httpClient, baseRepo, release.DatabaseID, params) if err != nil { return err diff --git a/pkg/cmd/release/edit/edit_test.go b/pkg/cmd/release/edit/edit_test.go index 9c7085b11..180051d12 100644 --- a/pkg/cmd/release/edit/edit_test.go +++ b/pkg/cmd/release/edit/edit_test.go @@ -140,6 +140,15 @@ func Test_NewCmdEdit(t *testing.T) { Body: stringPtr("MY NOTES"), }, }, + { + name: "verify-tag", + args: "v1.2.0 --tag=v1.1.0 --verify-tag", + isTTY: false, + want: EditOptions{ + TagName: "v1.1.0", + VerifyTag: true, + }, + }, } for _, tt := range tests { @@ -189,6 +198,7 @@ func Test_NewCmdEdit(t *testing.T) { assert.Equal(t, tt.want.Draft, opts.Draft) assert.Equal(t, tt.want.Prerelease, opts.Prerelease) assert.Equal(t, tt.want.IsLatest, opts.IsLatest) + assert.Equal(t, tt.want.VerifyTag, opts.VerifyTag) }) } } @@ -406,6 +416,21 @@ func Test_editRun(t *testing.T) { wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", wantStderr: "", }, + { + name: "error when remote tag does not exist and verify-tag flag is set", + isTTY: true, + opts: EditOptions{ + TagName: "v1.2.4", + VerifyTag: true, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register(httpmock.GraphQL("RepositoryFindRef"), + httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": ""}}}}`)) + }, + wantErr: "tag v1.2.4 doesn't exist in the repo OWNER/REPO, aborting due to --verify-tag flag", + wantStdout: "", + wantStderr: "", + }, } for _, tt := range tests { @@ -416,7 +441,7 @@ func Test_editRun(t *testing.T) { ios.SetStderrTTY(tt.isTTY) fakeHTTP := &httpmock.Registry{} - // defer reg.Verify(t) // FIXME: intermittetly fails due to StubFetchRelease internals + defer fakeHTTP.Verify(t) shared.StubFetchRelease(t, fakeHTTP, "OWNER", "REPO", "v1.2.3", `{ "id": 12345, "tag_name": "v1.2.3" diff --git a/pkg/cmd/release/edit/http.go b/pkg/cmd/release/edit/http.go index 6016ad776..bf310da60 100644 --- a/pkg/cmd/release/edit/http.go +++ b/pkg/cmd/release/edit/http.go @@ -11,6 +11,7 @@ import ( "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/release/shared" + "github.com/shurcooL/githubv4" ) func editRelease(httpClient *http.Client, repo ghrepo.Interface, releaseID int64, params map[string]interface{}) (*shared.Release, error) { @@ -48,3 +49,22 @@ func editRelease(httpClient *http.Client, repo ghrepo.Interface, releaseID int64 err = json.Unmarshal(b, &newRelease) return &newRelease, err } + +func remoteTagExists(httpClient *http.Client, repo ghrepo.Interface, tagName string) (bool, error) { + gql := api.NewClientFromHTTP(httpClient) + qualifiedTagName := fmt.Sprintf("refs/tags/%s", tagName) + var query struct { + Repository struct { + Ref struct { + ID string + } `graphql:"ref(qualifiedName: $tagName)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "tagName": githubv4.String(qualifiedTagName), + } + err := gql.Query(repo.RepoHost(), "RepositoryFindRef", &query, variables) + return query.Repository.Ref.ID != "", err +} diff --git a/pkg/cmd/release/shared/fetch.go b/pkg/cmd/release/shared/fetch.go index c8182cc83..84a9a369b 100644 --- a/pkg/cmd/release/shared/fetch.go +++ b/pkg/cmd/release/shared/fetch.go @@ -9,6 +9,7 @@ import ( "net/http" "reflect" "strings" + "testing" "time" "github.com/cli/cli/v2/api" @@ -224,11 +225,7 @@ func fetchReleasePath(ctx context.Context, httpClient *http.Client, host string, return &release, nil } -type testingT interface { - Errorf(format string, args ...interface{}) -} - -func StubFetchRelease(t testingT, reg *httpmock.Registry, owner, repoName, tagName, responseBody string) { +func StubFetchRelease(t *testing.T, reg *httpmock.Registry, owner, repoName, tagName, responseBody string) { path := "repos/OWNER/REPO/releases/tags/v1.2.3" if tagName == "" { path = "repos/OWNER/REPO/releases/latest" @@ -239,10 +236,12 @@ func StubFetchRelease(t testingT, reg *httpmock.Registry, owner, repoName, tagNa if tagName != "" { reg.Register( httpmock.GraphQL(`query RepositoryReleaseByTag\b`), - httpmock.GraphQLQuery(`{ "data": { "repository": { "release": null }}}`, func(q string, vars map[string]interface{}) { - assert.Equal(t, owner, vars["owner"]) - assert.Equal(t, repoName, vars["name"]) - assert.Equal(t, tagName, vars["tagName"]) - })) + httpmock.GraphQLQuery(`{ "data": { "repository": { "release": null }}}`, + func(q string, vars map[string]interface{}) { + assert.Equal(t, owner, vars["owner"]) + assert.Equal(t, repoName, vars["name"]) + assert.Equal(t, tagName, vars["tagName"]) + }), + ) } } diff --git a/pkg/cmd/release/view/view_test.go b/pkg/cmd/release/view/view_test.go index 5d42ba373..c8b837923 100644 --- a/pkg/cmd/release/view/view_test.go +++ b/pkg/cmd/release/view/view_test.go @@ -215,6 +215,7 @@ func Test_viewRun(t *testing.T) { ios.SetStderrTTY(tt.isTTY) fakeHTTP := &httpmock.Registry{} + defer fakeHTTP.Verify(t) shared.StubFetchRelease(t, fakeHTTP, "OWNER", "REPO", tt.opts.TagName, fmt.Sprintf(`{ "tag_name": "v1.2.3", "draft": false,