diff --git a/cmd/uplift/bump.go b/cmd/uplift/bump.go index 11442b5a..3cf936e2 100644 --- a/cmd/uplift/bump.go +++ b/cmd/uplift/bump.go @@ -45,9 +45,28 @@ import ( ) const ( - bumpDesc = `Bumps the semantic version within files in your git repository. The -version bump is based on the conventional commit message from the last commit. -Uplift can bump the version in any file using regex pattern matching` + bumpLongDesc = `Calculates the next semantic version based on the conventional commits since the +last release (or identifiable tag) and bumps (or patches) a configurable set of +files with said version. JSON Path or Regex Pattern matching is supported when +scanning files for an existing semantic version. Uplift automatically handles +the staging and pushing of modified files to the git remote, but this behavior +can be disabled, to manage this action manually. + +Configuring a bump requires an Uplift configuration file to exist within the +root of your project: + +https://upliftci.dev/bumping-files/` + + bumpExamples = ` +# Bump (patch) all configured files with the next calculated semantic version +uplift bump + +# Append a prerelease suffix to the next calculated semantic version +uplift bump --prerelease beta.1 + +# Bump (patch) all configured files but do not stage or push any changes +# back to the git remote +uplift bump --no-stage` ) type bumpOptions struct { @@ -68,10 +87,11 @@ func newBumpCmd(gopts *globalOptions, out io.Writer) *bumpCommand { } cmd := &cobra.Command{ - Use: "bump", - Short: "Bump the semantic version within files", - Long: bumpDesc, - Args: cobra.NoArgs, + Use: "bump", + Short: "Bump the semantic version within files", + Long: bumpLongDesc, + Example: bumpExamples, + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return bumpFiles(bmpCmd.Opts, out) }, diff --git a/cmd/uplift/changelog.go b/cmd/uplift/changelog.go index 8511a4da..e25f2d93 100644 --- a/cmd/uplift/changelog.go +++ b/cmd/uplift/changelog.go @@ -45,11 +45,41 @@ import ( ) const ( - chlogDesc = `Create or update an existing changelog with an entry for -the latest semantic release. For a first release, all commits -between the latest tag and trunk will be written to the -changelog. Subsequent entries will contain only commits between -release tags` + changelogLongDesc = `Scans the git log for the latest semantic release and generates a changelog +entry. If this is a first release, all commits between the last release (or +identifiable tag) and the repository trunk will be written to the changelog. +Any subsequent entry within the changelog will only contain commits between +the latest set of tags. Basic customization is supported. Optionally commits +can be explicitly included or excluded from the entry and sorted in ascending +or descending order. Uplift automatically handles the staging and pushing of +changes to the CHANGELOG.md file to the git remote, but this behavior can be +disabled, to manage this action manually. + +Uplift bases its changelog format on the Keep a Changelog specification: + +https://keepachangelog.com/en/1.0.0/` + + changelogExamples = ` +# Generate the next changelog entry for the latest semantic release +uplift changelog + +# Generate a changelog for the entire history of the repository +uplift changelog --all + +# Generate the next changelog entry and write it to stdout +uplift changelog --diff-only + +# Generate the next changelog entry by exclude any conventional commits +# with the ci, chore or test prefixes +uplift changelog --exclude "^ci,^chore,^test" + +# Generate the next changelog entry with commits that only include the +# following scope +uplift changelog --include "^.*\(scope\)" + +# Generate the next changelog entry but do not stage or push any changes +# back to the git remote +uplift changelog --no-stage` ) type changelogOptions struct { @@ -74,10 +104,11 @@ func newChangelogCmd(gopts *globalOptions, out io.Writer) *changelogCommand { } cmd := &cobra.Command{ - Use: "changelog", - Short: "Create or update a changelog with the latest semantic release", - Long: chlogDesc, - Args: cobra.NoArgs, + Use: "changelog", + Short: "Create or update a changelog with the latest semantic release", + Long: changelogLongDesc, + Example: changelogExamples, + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Always lowercase sort chglogCmd.Opts.Sort = strings.ToLower(chglogCmd.Opts.Sort) diff --git a/cmd/uplift/release.go b/cmd/uplift/release.go index cba41db5..694fc351 100644 --- a/cmd/uplift/release.go +++ b/cmd/uplift/release.go @@ -54,9 +54,34 @@ import ( ) const ( - releaseDesc = `Release the next semantic version of your git repository. A release -will automatically bump any files and tag the associated commit with -the required semantic version` + releaseLongDesc = `Release the next semantic version of your git repository. A release consists of +a three-stage process. First, all configured files will be bumped (patched) using +the next semantic version. Second, a changelog entry containing all commits for +the latest semantic release will be created. Finally, Uplift will tag the +repository. Uplift automatically handles the staging and pushing of modified +files and the tagging of the repository with two separate git pushes. But this +behavior can be disabled to manage these actions manually. + +Parts of this release process can be disabled if needed. + +https://upliftci.dev/first-release/` + + releaseExamples = ` +# Release the next semantic version +uplift release + +# Release the next semantic version without bumping any files +uplift release --skip-bumps + +# Release the next semantic version without generating a changelog +uplift release --skip-changelog + +# Append a prerelease suffix to the next calculated semantic version +uplift release --prerelease beta.1 + +# Ensure any "v" prefix is stripped from the next calculated semantic +# version to explicitly adhere to the SemVer specification +uplift release --no-prefix` ) type releaseOptions struct { @@ -67,6 +92,7 @@ type releaseOptions struct { SkipBumps bool NoPrefix bool Exclude []string + Include []string Sort string *globalOptions } @@ -84,10 +110,11 @@ func newReleaseCmd(gopts *globalOptions, out io.Writer) *releaseCommand { } cmd := &cobra.Command{ - Use: "release", - Short: "Release the next semantic version of a repository", - Long: releaseDesc, - Args: cobra.NoArgs, + Use: "release", + Short: "Release the next semantic version of a repository", + Long: releaseLongDesc, + Example: releaseExamples, + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Just check if uplift would trigger a release if relCmd.Opts.Check { @@ -105,7 +132,8 @@ func newReleaseCmd(gopts *globalOptions, out io.Writer) *releaseCommand { f.BoolVar(&relCmd.Opts.SkipChangelog, "skip-changelog", false, "skips the creation or amendment of a changelog") f.BoolVar(&relCmd.Opts.SkipBumps, "skip-bumps", false, "skips the bumping of any files") f.BoolVar(&relCmd.Opts.NoPrefix, "no-prefix", false, "strip the default 'v' prefix from the next calculated semantic version") - f.StringSliceVar(&relCmd.Opts.Exclude, "exclude", []string{}, "a list of conventional commit prefixes to exclude from the changelog") + f.StringSliceVar(&relCmd.Opts.Exclude, "exclude", []string{}, "a list of regexes for excluding conventional commits from the changelog") + f.StringSliceVar(&relCmd.Opts.Include, "include", []string{}, "a list of regexes to cherry-pick conventional commits for the changelog") f.StringVar(&relCmd.Opts.Sort, "sort", "", "the sort order of commits within each changelog entry") relCmd.Cmd = cmd @@ -171,8 +199,8 @@ func setupReleaseContext(opts releaseOptions, out io.Writer) (*context.Context, ctx.Changelog.PreTag = true // Merge config and command line arguments together - ctx.Changelog.Exclude = opts.Exclude - ctx.Changelog.Exclude = append(ctx.Changelog.Exclude, ctx.Config.Changelog.Exclude...) + ctx.Changelog.Include = append(opts.Include, ctx.Config.Changelog.Include...) + ctx.Changelog.Exclude = append(opts.Exclude, ctx.Config.Changelog.Exclude...) // By default ensure the ci(uplift): commits are excluded also ctx.Changelog.Exclude = append(ctx.Changelog.Exclude, "ci(uplift):") diff --git a/cmd/uplift/release_test.go b/cmd/uplift/release_test.go index c31ed000..9360c6a4 100644 --- a/cmd/uplift/release_test.go +++ b/cmd/uplift/release_test.go @@ -197,7 +197,7 @@ func TestRelease_WithExclude(t *testing.T) { untaggedRepo(t, "feat: a new feat", "fix: a new fix", "ci: a ci task", "docs: some new docs") relCmd := newReleaseCmd(noChangesPushed(), os.Stdout) - relCmd.Cmd.SetArgs([]string{"--exclude", "ci,docs"}) + relCmd.Cmd.SetArgs([]string{"--exclude", "^ci,^docs"}) err := relCmd.Cmd.Execute() require.NoError(t, err) @@ -210,3 +210,21 @@ func TestRelease_WithExclude(t *testing.T) { assert.NotContains(t, cl, "ci: a ci task") assert.NotContains(t, cl, "docs: some new docs") } + +func TestRelease_WithInclude(t *testing.T) { + untaggedRepo(t, "feat: a new feat", "fix: a new fix", "ci: a ci task", "docs: some new docs") + + relCmd := newReleaseCmd(noChangesPushed(), os.Stdout) + relCmd.Cmd.SetArgs([]string{"--include", "^feat"}) + + err := relCmd.Cmd.Execute() + require.NoError(t, err) + + assert.True(t, changelogExists(t)) + + cl := readChangelog(t) + assert.Contains(t, cl, "feat: a new feat") + assert.NotContains(t, cl, "fix: a new fix") + assert.NotContains(t, cl, "ci: a ci task") + assert.NotContains(t, cl, "docs: some new docs") +} diff --git a/cmd/uplift/tag.go b/cmd/uplift/tag.go index 0c47c5b3..b5ecff46 100644 --- a/cmd/uplift/tag.go +++ b/cmd/uplift/tag.go @@ -45,23 +45,45 @@ import ( ) const ( - tagDesc = `Tags a git repository with the next semantic version. The tag -is based on the conventional commit message from the last commit.` + tagLongDesc = `Generates a new git tag by scanning the git log of a repository for any +conventional commits since the last release (or identifiable tag). When +examining the git log, Uplift will always calculate the next semantic version +based on the most significant detected increment. Uplift automatically handles +the creation and pushing of a new git tag to the remote, but this behavior can +be disabled, to manage this action manually. - examples = ` # Tag the repository with the next calculated semantic version - uplift tag +Conventional Commits is a set of rules for creating an explicit commit history, +which makes building automation tools like Uplift much easier. Uplift adheres to +v1.0.0 of the specification: - # Identify the next semantic version and write to stdout. - # Repository is not tagged - uplift tag --next --silent +https://www.conventionalcommits.org/en/v1.0.0` - # Identify the current semantic version and write to stdout. - # Repository is not tagged - uplift tag --current + tagExamples = ` +# Tag the repository with the next calculated semantic version +uplift tag - # Identify the next and current semantic versions and write to stdout. - # Repository is not tagged - uplift tag --current --next --silent` +# Identify the next semantic version and write to stdout. Repository is +# not tagged +uplift tag --next --silent + +# Identify the current semantic version and write to stdout. Repository +# is not tagged +uplift tag --current + +# Identify the current and next semantic versions and write both to stdout. +# Repository is not tagged +uplift tag --current --next --silent + +# Ensure the calculated version explicitly aheres to the SemVer specification +# by stripping the "v" prefix from the generated tag +uplift tag --no-prefix + +# Append a prerelease suffix to the next calculated semantic version +uplift tag --prerelease beta.1 + +# Tag the repository with the next calculated semantic version, but do not +# push the tag to the remote +uplift tag --no-push` ) var ( @@ -112,9 +134,9 @@ func newTagCmd(gopts *globalOptions, out io.Writer) *tagCommand { cmd := &cobra.Command{ Use: "tag", - Short: "Tag a git repository with the next semantic version", - Long: tagDesc, - Example: examples, + Short: "Tag a git repository with the next calculated semantic version", + Long: tagLongDesc, + Example: tagExamples, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // If only the current tag is to be printed, skip running a pipeline