diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index 95e4963..a8758fa 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -65,7 +65,7 @@ jobs: - name: Check increment result shell: bash - run: '[[ "$(date +%Y.%m)" == "$(echo "${{ steps.version-increment.outputs.VERSION }}" | cut -d "." -f 1-2)" ]]' + run: '[[ "$(date +%Y.%-m)" == "$(echo "${{ steps.version-increment.outputs.VERSION }}" | cut -d "." -f 1-2)" ]]' test-action-yml: # integration testing needs: diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..8226afb --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +external-sources=true diff --git a/README.md b/README.md index 82988f2..36e23e1 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ use the API mode: ```yaml - name: Get next version - uses: reecetech/version-increment@2023.10.1 + uses: reecetech/version-increment@2024.4.1 id: version with: use_api: true @@ -70,6 +70,22 @@ If the current latest normal version is not the current year and month, then the year and month digits will be set to the current year and month, and the release digit will be reset to 1. +### Conventional Commits (semver with smarts) 💡 + +If you choose the conventional commits scheme, the action will parse the last commit message _(usually the merge commit)_ to determine +the increment type for a `semver` version. + +The following increment types by keyword are supported: + - patch: build, chore, ci, docs, fix, perf, refactor, revert, style, test + - minor: feat + - major: any of the above keywords followed by a '!' character, or 'BREAKING CHANGE:' in commit body + +If none of the keywords are detected, then the increment specified by the `increment` input will be used (defaults to patch). + +> [!TIP] +> You might like to _enforce_ conventional commits in the title of your pull requests to ensure that the merge commit has the correct +> information. Something like this action might be handy: https://github.com/marketplace/actions/conventional-commit-in-pull-requests + ### Default branch vs. any other branch 🎋 **Default branch** @@ -107,7 +123,7 @@ Examples: | name | description | required | default | | :--- | :--- | :--- | :--- | -| scheme | The versioning scheme in-use, either `semver` or `calver` | No | `semver` | +| scheme | The versioning scheme in-use, either `semver`, `calver` or `conventional_commits` | No | `semver` | | pep440 | Set to `true` for PEP440 compatibility of _pre-release_ versions by making use of the build metadata segment of semver, which maps to local version identifier in PEP440 | No | `false` | | increment | The digit to increment, either `major`, `minor` or `patch`, ignored if `scheme` == `calver` | No | `patch` | | release_branch | Specify a non-default branch to use for the release tag (the one without -pre) | No | | diff --git a/action.yml b/action.yml index b4a1e60..6fcbc22 100644 --- a/action.yml +++ b/action.yml @@ -8,7 +8,14 @@ branding: inputs: scheme: - description: 'Versioning scheme - semver, or, calver (defaults to semver)' + description: | + Versioning scheme - semver, calver or conventional_commits (defaults to semver). + + `conventional_commits` Will parse the last commit message (e.g. the merge commit) to + determine the increment type, and supports the following increment types by keyword: + - patch (build, chore, ci, docs, fix, perf, refactor, revert, style, test) + - minor (feat) + - major (any of the above keywords followed by a '!' character, or 'BREAKING CHANGE:' in commit body) required: false default: 'semver' pep440: @@ -19,6 +26,9 @@ inputs: description: | Field to increment - major, minor, or, patch (defaults to patch) + If using the `conventional_commits` scheme, this is the default increment if the parsing of the merge commit fails to + find conventional commits information + Not applicable to `calver` scheme required: false default: 'patch' @@ -36,14 +46,14 @@ outputs: description: 'Current normal version detected' value: ${{ steps.version-lookup.outputs.CURRENT_VERSION }} current-v-version: - description: 'Current normal version detected, prefixed with a `v` charatcter' + description: 'Current normal version detected, prefixed with a `v` character' value: ${{ steps.version-lookup.outputs.CURRENT_V_VERSION }} version: description: 'Incremented version calculated' value: ${{ steps.version-increment.outputs.VERSION }} v-version: description: 'Incremented version calculated, prefixed with a `v` charatcter' - value: ${{ steps.version-increment.outputs.V_VERSION }} + value: ${{ steps.version-increment.outputs.V_VERSION }} major-version: description: 'Major number of the incremented version' value: ${{ steps.version-increment.outputs.MAJOR_VERSION }} diff --git a/shared.sh b/shared.sh index e1c97e6..d9f529f 100644 --- a/shared.sh +++ b/shared.sh @@ -11,13 +11,21 @@ pcre_master_ver='^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9 pcre_allow_vprefix="^v{0,1}${pcre_master_ver:1}" pcre_old_calver='^(?P0|[1-9]\d*)-0{0,1}(?P0|[0-9]\d*)-R(?P0|[1-9]\d*)$' +##==---------------------------------------------------------------------------- +## Conventional commit regexes +## see: https://www.conventionalcommits.org/en/v1.0.0/ + +pcre_conventional_commit_patch='^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\([a-zA-Z0-9-]+\))?:\s.*' +pcre_conventional_commit_minor='^(feat)(\([a-zA-Z0-9-]+\))?:\s.*' +pcre_conventional_commit_breaking='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-zA-Z0-9-]+\))?!:.*|BREAKING CHANGE:' + ##==---------------------------------------------------------------------------- ## Input validation input_errors='false' scheme="${scheme:-semver}" -if [[ "${scheme}" != 'semver' && "${scheme}" != 'calver' ]] ; then - echo "🛑 Value of 'scheme' is not valid, choose from 'semver' or 'calver'" 1>&2 +if [[ "${scheme}" != 'semver' && "${scheme}" != 'calver' && "${scheme}" != 'conventional_commits' ]] ; then + echo "🛑 Value of 'scheme' is not valid, choose from 'semver', 'calver' or 'conventional_commits" 1>&2 input_errors='true' fi @@ -40,7 +48,6 @@ if [[ "${use_api}" == 'true' ]] ; then fi fi - ##==---------------------------------------------------------------------------- ## MacOS compatibility - for local testing diff --git a/tests/test_version-increment.bats b/tests/test_version-increment.bats index f2da4d7..bdb4834 100644 --- a/tests/test_version-increment.bats +++ b/tests/test_version-increment.bats @@ -272,3 +272,131 @@ function init_repo { [[ "$output" = *"PRE_RELEASE_LABEL=pre.${short_ref}"* ]] && [[ "$output" = *"VERSION=$(date +%Y.%-m.1)+pre.${short_ref}"* ]] } + +@test "increments the patch version after a fix commit (conventional commits)" { + init_repo + + export current_version=1.2.3 + export scheme="conventional_commits" + + echo "fix: something" > fix.txt + git add fix.txt + git commit -m "fix: something" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"VERSION=1.2.4"* ]] + [[ "$output" != *"No conventional commit found"* ]] +} + + +@test "increments the patch version after a scoped fix commit (conventional commits)" { + init_repo + + export current_version=1.2.3 + export scheme="conventional_commits" + + echo "fix: something" > fix.txt + git add fix.txt + git commit -m "fix(api): something" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"VERSION=1.2.4"* ]] + [[ "$output" != *"No conventional commit found"* ]] +} + +@test "increments the major version after a breaking fix commit (conventional commits)" { + init_repo + + export current_version=1.2.3 + export scheme="conventional_commits" + + echo "fix: breaking something" > fix.txt + git add fix.txt + git commit -m "fix!: something" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"VERSION=2.0.0"* ]] + [[ "$output" != *"No conventional commit found"* ]] +} + +@test "increments the minor version after a feat commit (conventional commits)" { + init_repo + + export current_version=1.2.3 + export scheme="conventional_commits" + + echo "new feature" > feat.txt + git add feat.txt + git commit -m "feat: something" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"VERSION=1.3.0"* ]] + [[ "$output" != *"No conventional commit found"* ]] +} + +@test "increments the major version after a breaking feat commit (conventional commits)" { + init_repo + + export current_version=1.2.3 + export scheme="conventional_commits" + + echo "breaking new feature" > feat.txt + git add feat.txt + git commit -m "feat!: something" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"VERSION=2.0.0"* ]] + [[ "$output" != *"No conventional commit found"* ]] +} + +@test "increments the major version after a breaking change in the commit body (conventional commits)" { + init_repo + + export current_version=1.2.3 + export scheme="conventional_commits" + + echo "breaking new fix" > feat.txt + git add feat.txt + git commit -m "Fix: something + BREAKING CHANGE: really important" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"VERSION=2.0.0"* ]] + [[ "$output" != *"No conventional commit found"* ]] +} + +@test "increments the patch version by default if no conventional commits found and enabled (conventional commits)" { + init_repo + + export current_version=1.2.3 + export scheme="conventional_commits" + + echo "some new change" > feat.txt + git add feat.txt + git commit -m "new change" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"VERSION=1.2.4"* ]] + [[ "$output" = *"No conventional commit found"* ]] +} diff --git a/version-increment.sh b/version-increment.sh index 7036524..e76c091 100755 --- a/version-increment.sh +++ b/version-increment.sh @@ -30,7 +30,7 @@ fi default_branch='main' # use release_branch if not empty if [[ -n "${release_branch:-}" ]] ; then - default_branch="${release_branch}" + default_branch="${release_branch}" elif [[ -z "${BATS_VERSION:-}" ]] ; then # if we're _not_ testing, then _actually_ check the origin if [[ "${use_api:-}" == 'true' ]] ; then @@ -48,6 +48,7 @@ elif [[ -z "${BATS_VERSION:-}" ]] ; then fi current_ref="${GITHUB_REF:-}" +git_commit_sha=${GITHUB_SHA:-} if [[ "${use_api:-}" == 'true' ]] ; then # because we cannot use `rev-parse` with the API, we'll take a punt that 9 characters is enough for uniqueness @@ -55,7 +56,32 @@ if [[ "${use_api:-}" == 'true' ]] ; then git_commit="$(echo "${GITHUB_SHA:0:9}" | sed 's/0*//')" # Also, trim leading zeros, because semver doesn't allow that in else # the 'pre-release version' part, but we can't use the + char git_commit="$(git rev-parse --short HEAD | sed 's/0*//')" # to make it 'build metadata' as that's not supported in K8s -fi # labels + git_commit_sha="$(git rev-parse --short HEAD)" # labels +fi + +##==---------------------------------------------------------------------------- +## Conventional commits +if [[ "${scheme}" == 'conventional_commits' ]] ; then + # Get message from given commit + commit_message=$(git log -1 --pretty=format:%B "${git_commit_sha}") + + # Check commit message header + found_match='false' + if [[ -n "$(echo "${commit_message}" | ${grep} -P "${pcre_conventional_commit_breaking}")" ]] ; then + increment='major' + found_match='true' + elif [[ -n "$(echo "${commit_message}" | ${grep} -P "${pcre_conventional_commit_minor}")" ]] ; then + increment='minor' + found_match='true' + elif [[ -n "$(echo "${commit_message}" | ${grep} -P "${pcre_conventional_commit_patch}")" ]] ; then + increment='patch' + found_match='true' + fi + + if [[ "${found_match}" == 'false' ]] ; then + echo "⚠️ No conventional commit found, defaulting to ${increment} increment" 1>&2 + fi +fi ##==---------------------------------------------------------------------------- ## Version increment diff --git a/version-lookup.sh b/version-lookup.sh index 130ca89..3957688 100755 --- a/version-lookup.sh +++ b/version-lookup.sh @@ -46,7 +46,7 @@ if [[ -z "${current_version:-}" ]] ; then echo "⚠️ No previous release version identified in git tags" # brand new repo! (probably) case "${scheme}" in - semver) + semver | conventional_commits) current_version="0.0.0" ;; calver)