Add gh release create [<tag>] --notes-from-tag

Generates release notes from an annotated git tag noninteractively.
Fixes #6718
This commit is contained in:
Keith Bailey 2023-08-17 15:40:52 -04:00
parent f777bec798
commit 5ccfbe9d7e
No known key found for this signature in database
GPG key ID: 40E9499AB8E6279D
2 changed files with 60 additions and 2 deletions

View file

@ -59,6 +59,7 @@ type CreateOptions struct {
GenerateNotes bool
NotesStartTag string
VerifyTag bool
NotesFromTag bool
}
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
@ -92,6 +93,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
To create a release from an annotated git tag, first create one locally with
git, push the tag to GitHub, then run this command.
You may also use %[1]s--notes-from-tag%[1]s to create it noninteractively.
When using automatically generated release notes, a release title will also be automatically
generated unless a title was explicitly passed. Additional release notes can be prepended to
@ -146,9 +148,13 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
return cmdutil.FlagErrorf("tag required when not running interactively")
}
if opts.NotesFromTag && (opts.GenerateNotes || opts.NotesStartTag != "") {
return cmdutil.FlagErrorf("using `--notes-from-tag` with `--generate-notes` or `notes-start-tag` is not supported")
}
opts.Concurrency = 5
opts.BodyProvided = cmd.Flags().Changed("notes") || opts.GenerateNotes
opts.BodyProvided = cmd.Flags().Changed("notes") || opts.GenerateNotes || opts.NotesFromTag
if notesFile != "" {
b, err := cmdutil.ReadFile(notesFile, opts.IO.In)
if err != nil {
@ -176,6 +182,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
cmd.Flags().StringVar(&opts.NotesStartTag, "notes-start-tag", "", "Tag to use as the starting point for generating release notes")
cmdutil.NilBoolFlag(cmd, &opts.IsLatest, "latest", "", "Mark this release as \"Latest\" (default: automatic based on date and version)")
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 populate the release notes with the message in the tag specified by `gh release create [<tag>]`")
_ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "target")
@ -241,6 +248,7 @@ func createRun(opts *CreateOptions) error {
var tagDescription string
if opts.RepoOverride == "" {
tagDescription, _ = gitTagInfo(opts.GitClient, opts.TagName)
remoteExists, err := remoteTagExists(httpClient, baseRepo, opts.TagName)
// If there is a local tag with the same name as specified
// the user may not want to create a new tag on the remote
// as the local one might be annotated or signed.
@ -250,7 +258,6 @@ func createRun(opts *CreateOptions) error {
// If a remote tag with the same name as specified exists already
// then a new tag will not be created so ignore local tag status.
if tagDescription != "" && !existingTag && opts.Target == "" && !opts.VerifyTag {
remoteExists, err := remoteTagExists(httpClient, baseRepo, opts.TagName)
if err != nil {
return err
}
@ -259,6 +266,13 @@ func createRun(opts *CreateOptions) error {
opts.TagName, ghrepo.FullName(baseRepo))
}
}
if opts.NotesFromTag {
if !remoteExists {
return fmt.Errorf("tag %s doesn't exist in the repo %s, cannot populate release notes with annotated git tag message using the `--notes-from-tag` flag",
opts.TagName, ghrepo.FullName(baseRepo))
}
}
}
if !opts.BodyProvided && opts.IO.CanPrompt() {
@ -427,6 +441,13 @@ func createRun(opts *CreateOptions) error {
params["generate_release_notes"] = true
}
}
if opts.NotesFromTag {
if opts.Body == "" {
params["body"] = tagDescription
} else {
params["body"] = fmt.Sprintf("%s\n%s", opts.Body, tagDescription)
}
}
hasAssets := len(opts.Assets) > 0
draftWhileUploading := false

View file

@ -321,6 +321,43 @@ func Test_NewCmdCreate(t *testing.T) {
VerifyTag: true,
},
},
{
name: "tag and --notes-from-tag",
args: "v1.2.3 --notes-from-tag",
isTTY: false,
want: CreateOptions{
TagName: "v1.2.3",
Target: "",
Name: "",
Body: "",
BodyProvided: true,
Draft: false,
Prerelease: false,
RepoOverride: "",
Concurrency: 5,
Assets: []*shared.AssetForUpload(nil),
GenerateNotes: false,
NotesFromTag: true,
},
},
{
name: "--notes-from-tag only ",
args: "--notes-from-tag",
isTTY: false,
wantErr: "tag required when not running interactively",
},
{
name: "tag and --notes-from-tag and --generate-notes",
args: "v1.2.3 --notes-from-tag --generate-notes",
isTTY: false,
wantErr: "using `--notes-from-tag` with `--generate-notes` or `notes-start-tag` is not supported",
},
{
name: "tag and --notes-from-tag and --notes-start-tag",
args: "v1.2.3 --notes-from-tag --notes-start-tag v1.2.3",
isTTY: false,
wantErr: "using `--notes-from-tag` with `--generate-notes` or `notes-start-tag` is not supported",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {