From 41a040c60ed9359deb1daa64bd0504a4c242b0ad Mon Sep 17 00:00:00 2001 From: Phil Jay Date: Wed, 20 Oct 2021 11:18:23 +1100 Subject: [PATCH] Add initial codebase --- .github/workflows/test-and-release.yml | 64 +++++++++++ .gitignore | 72 ++++++++++++ README.md | 103 +++++++++++++++++ action.yml | 42 +++++++ shared.sh | 21 ++++ tests/helper_print-info.bash | 9 ++ tests/test_version_increment.bats | 147 +++++++++++++++++++++++++ tests/test_version_lookup.bats | 115 +++++++++++++++++++ version-increment.sh | 93 ++++++++++++++++ version-lookup.sh | 66 +++++++++++ 10 files changed, 732 insertions(+) create mode 100644 .github/workflows/test-and-release.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 action.yml create mode 100644 shared.sh create mode 100644 tests/helper_print-info.bash create mode 100644 tests/test_version_increment.bats create mode 100644 tests/test_version_lookup.bats create mode 100755 version-increment.sh create mode 100755 version-lookup.sh diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml new file mode 100644 index 0000000..de4b40b --- /dev/null +++ b/.github/workflows/test-and-release.yml @@ -0,0 +1,64 @@ +--- +name: Test and Release + +on: + push: + branches: + - '**' + tags-ignore: + - '**' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@94e0aab03ca135d11a35e5bfc14e6746dc56e7e9 + with: + check_together: 'yes' + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup bats + uses: mig4/setup-bats@af9a00deb21b5d795cabfeaa8d9060410377686d + with: + bats-version: 1.2.1 + + - name: Test + run: bats tests/*.bats + + release: + needs: + - lint + - test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Lookup version + id: version-lookup + run: ./version-lookup.sh + + - name: Increment version + id: version-increment + run: ./version-increment.sh + env: + current_version: ${{ steps.version-lookup.outputs.current-version }} + INPUT_SCHEME: calver + + - name: Release version + uses: marvinpinto/action-automatic-releases@919008cf3f741b179569b7a6fb4d8860689ab7f0 + if: ${{ github.ref == 'refs/heads/main' }} + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + draft: false + prerelease: false + automatic_release_tag: "${{ steps.version-increment.outputs.version }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4acf457 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +.tmp_testing/ + + +##-- Vim ignores + +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + + +##-- MacOS ignores + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +##-- Windows ignores + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..91337ce --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# ➕ Version Increment + +## 📄 Use + +### âŒ¨ī¸ Example + +```yaml + - name: Get next version + uses: reecetech/version-increment + id: version + with: + scheme: semver + increment: patch + + - name: Build image + uses: docker/build-push-action@v2 + with: + push: false + tags: "example/application:${{ steps.version.outputs.version }}" + context: . +``` + +### 🔖 semver +This action will detect the current latest _normal_ semantic version (semver) from the tags in +a git repository. It will increment the version as directed (by default: +1 to +the patch digit). Both the current latest and the incremented version are +reported back as outputs. + +Normal semantic versions are made up of a major, minor and patch digit. Normal +versions do not include pre-release versions, or versions with build meta-data. + +e.g. `1.2.7` + +See: https://semver.org/spec/v2.0.0.html + +### 📅 calver (semver compliant) + +Optionally, this action can provide semver compliant calendar versions (calver). +In this calver scheme, the semver major, minor and patch digits map to year, +month and release digits. + +Note: to be semver compliant, digits must not have leading zeros. + +e.g. `2021.6.2` + +| semver | calver | example | note | +| :--- | :--- | :--- | :--- | +| major | year | `2021` | +| minor | month | `6` | +| patch | release | `2` | The *n*th release for the month | + +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. + +### 🎋 Default branch vs. any other branch + +**Default branch** + +The action will return a _normal_ version if it is detected that the current commit is on the default branch (usually `main`). + +Examples: +* `1.2.7` +* `2021.6.2` + +**Any other branch** + +The action will return a _pre-release_ version if any other branch is detected (e.g. `new-feature`, `bugfix/foo`, etc). The _pre-release_ portion of the version number will be the literal string `pre.` followed by the git commit ID short reference SHA (trimmed of any leading zeros). + +Examples: +* `1.2.7-pre.41218aa78` +* `2021.6.2-pre.32fd19841` + +### đŸ“Ĩ Inputs + +| name | description | required | default | +| :--- | :--- | :--- | :--- | +| scheme | The versioning scheme in-use, either `semver` or `calver` | No | `semver` | +| increment | The digit to increment, either `major`, `minor` or `patch`, ignored if `scheme` == `calver` | No | `patch` | + +### 📤 Outputs + +| name | description | +| :--- | :--- | +| current_version | The current latest version detected from the git repositories tags | +| version | The incremented version number (e.g. the next version) | + +## 💕 Contributing + +Please raise a pull request, but note the testing tools below + +### bats + +BATS is used to test the logic of the shell scripts. + +See: https://github.com/bats-core/bats-core + +### shellcheck + +Shellcheck is used to lint our shell scripts. + +Please use [local ignores](https://stackoverflow.com/a/52659039) if you'd like to skip any particular checks. + +See: https://github.com/koalaman/shellcheck diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..72f58b1 --- /dev/null +++ b/action.yml @@ -0,0 +1,42 @@ +--- +name: 'Version Increment' +description: | + Inspects the git tags to determine the current normal version, and returns the + next version number. + + A normal version will be returned if the branch is the default branch + (usually `main`), otherwise a pre-release version will be returned. + +inputs: + scheme: + description: 'Versioning scheme - semver, or, calver (defaults to semver)' + required: false + default: 'semver' + increment: + description: | + Field to increment - major, minor, or, patch (defaults to patch) + + Not applicable to `calver` scheme + required: false + default: 'patch' + +outputs: + current_version: + description: 'Current normal version detected' + value: ${{ steps.version-lookup.outputs.current-version }} + version: + description: 'Incremented version calculated' + value: ${{ steps.version-increment.outputs.version }} + +runs: + using: "composite" + steps: + - id: version-lookup + run: ${{ github.action_path }}/version-lookup.sh + shell: bash + + - id: version-increment + run: ${{ github.action_path }}/version-increment.sh + shell: bash + with: + current_version: ${{ steps.version-lookup.outputs.current-version }} diff --git a/shared.sh b/shared.sh new file mode 100644 index 0000000..c41fb9c --- /dev/null +++ b/shared.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# shellcheck disable=SC2034 +set -euo pipefail + +# see: https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +pcre_semver='^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' +pcre_master_ver='^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)$' +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*)$' + +##==---------------------------------------------------------------------------- +## MacOS compatibility - for local testing + +export grep="grep" +if [[ "$(uname)" == "Darwin" ]] ; then + export grep="ggrep" + if ! grep --version 1>/dev/null ; then + echo "🛑 GNU grep not installed, try brew install coreutils" 1>&2 + exit 9 + fi +fi diff --git a/tests/helper_print-info.bash b/tests/helper_print-info.bash new file mode 100644 index 0000000..a62a7b3 --- /dev/null +++ b/tests/helper_print-info.bash @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# vim: set ft=sh sw=4 : + +# shellcheck disable=SC2154 + +function print_run_info() { + echo "status: ${status}" + echo "output: ${output}" +} diff --git a/tests/test_version_increment.bats b/tests/test_version_increment.bats new file mode 100644 index 0000000..2051fa4 --- /dev/null +++ b/tests/test_version_increment.bats @@ -0,0 +1,147 @@ +#!/usr/bin/env bats +# vim: set ft=sh sw=4 : + +load helper_print-info + +export repo=".tmp_testing/repo" + +function init_repo { + rm -rf "${repo}" && + mkdir -p "${repo}" && + cd "${repo}" && + git init && + git checkout -b main && + touch README.md && + git add README.md && + git config user.email test@example.com && + git config user.name Tester && + git commit -m "README" && + export GITHUB_REF="refs/heads/main" +} + +@test "fails if no current_version given" { + init_repo + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 5 ] && + [[ "$output" = *"Environment variable 'current_version' is unset or empty"* ]] +} + +@test "fails if invalid current_version given" { + init_repo + + export current_version=1.3.5-prerelease + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 6 ] && + [[ "$output" = *"Environment variable 'current_version' is not a valid normal version"* ]] +} + +@test "fails if invalid scheme given" { + init_repo + + export current_version=1.2.3 + export INPUT_SCHEME="foover" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 8 ] && + [[ "$output" = *"Value of 'scheme' is not valid"* ]] +} + +@test "fails if invalid increment given" { + init_repo + + export current_version=1.2.3 + export INPUT_INCREMENT="critical" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 7 ] && + [[ "$output" = *"Value of 'increment' is not valid, choose from 'major', 'minor', or 'patch'"* ]] +} + +@test "increments the patch digit correctly (semver)" { + init_repo + + export current_version=1.2.3 + export INPUT_INCREMENT="patch" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=version::1.2.4"* ]] +} + +@test "increments the minor digit correctly (semver)" { + init_repo + + export current_version=1.2.3 + export INPUT_INCREMENT="minor" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=version::1.3.0"* ]] +} + +@test "increments the major digit correctly (semver)" { + init_repo + + export current_version=1.2.3 + export INPUT_INCREMENT="major" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=version::2.0.0"* ]] +} + +@test "increments to a new month (calver)" { + init_repo + + export current_version=2020.6.4 + export INPUT_SCHEME="calver" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=version::$(date +%Y.%-m.1)"* ]] +} + +@test "increments the patch digit within a month (calver)" { + init_repo + + export current_version="$(date +%Y.%-m.123)" + export INPUT_SCHEME="calver" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=version::$(date +%Y.%-m.124)"* ]] +} + +@test "appends prerelease information if on a branch" { + init_repo + + export current_version=1.2.3 + export GITHUB_REF="refs/heads/super-awesome-feature" + export short_ref="$(git rev-parse --short HEAD | sed 's/0*//')" + + run ../../version-increment.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=version::1.2.4-pre.${short_ref}"* ]] +} diff --git a/tests/test_version_lookup.bats b/tests/test_version_lookup.bats new file mode 100644 index 0000000..622bc68 --- /dev/null +++ b/tests/test_version_lookup.bats @@ -0,0 +1,115 @@ +#!/usr/bin/env bats +# vim: set ft=sh sw=4 : + +load helper_print-info + +export repo=".tmp_testing/repo" + +function init_repo { + rm -rf "${repo}" && + mkdir -p "${repo}" && + cd "${repo}" && + git init && + touch README.md && + git add README.md && + git config user.email test@example.com && + git config user.name Tester && + git commit -m "README" +} + +@test "fails if invalid scheme given" { + init_repo + + export INPUT_SCHEME="foover" + + run ../../version-lookup.sh + + print_run_info + [ "$status" -eq 8 ] && + [[ "$output" = *"Value of 'scheme' is not valid"* ]] +} + +@test "finds the current normal version" { + init_repo + + git tag 0.0.1 + git tag 0.1.1 + git tag 0.1.2 + + run ../../version-lookup.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=current-version::0.1.2"* ]] +} + +@test "finds the current normal version even if there's a newer pre-release version" { + init_repo + + git tag 1.2.300 + git tag 1.2.301-dev.234 + + run ../../version-lookup.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=current-version::1.2.300"* ]] +} + +@test "returns 0.0.0 if no normal version detected" { + init_repo + + run ../../version-lookup.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=current-version::0.0.0"* ]] +} + +@test "returns 0.0.0 if no normal version detected even if there's a pre-release version" { + init_repo + + git tag 0.0.1-dev.999 + + run ../../version-lookup.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=current-version::0.0.0"* ]] +} + +@test "returns a calver if no normal version detected and calver scheme specified" { + init_repo + + export INPUT_SCHEME="calver" + + run ../../version-lookup.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=current-version::$(date '+%Y.%-m.0')"* ]] +} + +@test "converts from older calver scheme automatically" { + init_repo + + git tag 2020-09-R2 + + run ../../version-lookup.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=current-version::2020.9.2"* ]] +} + +@test "strips v from the version" { + init_repo + + git tag v3.4.5 + + run ../../version-lookup.sh + + print_run_info + [ "$status" -eq 0 ] && + [[ "$output" = *"::set-output name=current-version::3.4.5"* ]] +} diff --git a/version-increment.sh b/version-increment.sh new file mode 100755 index 0000000..39309c6 --- /dev/null +++ b/version-increment.sh @@ -0,0 +1,93 @@ +#!/bin/bash +set -euo pipefail + +# https://stackoverflow.com/questions/59895/get-the-source-directory-of-a-bash-script-from-within-the-script-itself +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck source=shared.sh +source "${script_dir}/shared.sh" + +if [[ -z "${current_version:-}" ]] ; then + echo "🛑 Environment variable 'current_version' is unset or empty" 1>&2 + exit 5 +fi + +if [[ -z "$(echo "${current_version}" | ${grep} -P "${pcre_master_ver}")" ]] ; then + echo "🛑 Environment variable 'current_version' is not a valid normal version (M.m.p)" 1>&2 + exit 6 +fi + +increment="${INPUT_INCREMENT:-patch}" +if [[ "${increment}" != 'patch' && "${increment}" != 'minor' && "${increment}" != 'major' ]] ; then + echo "🛑 Value of 'increment' is not valid, choose from 'major', 'minor', or 'patch'" 1>&2 + exit 7 +fi + +scheme="${INPUT_SCHEME:-semver}" +if [[ "${scheme}" != 'semver' && "${scheme}" != 'calver' ]] ; then + echo "🛑 Value of 'scheme' is not valid, choose from 'semver' or 'calver'" 1>&2 + exit 8 +fi + +##==---------------------------------------------------------------------------- +## Git info - branch names, commit short ref + +default_branch='main' +# if we're _not_ testing, then _actually_ check the origin +if [[ -z "${BATS_VERSION:-}" ]] ; then + default_branch="$(git remote show origin | ${grep} 'HEAD branch' | cut -d ' ' -f 5)" +fi +current_ref="${GITHUB_REF:-}" +git_commit="$(git rev-parse --short HEAD | sed 's/0*//')" # trim leading zeros, because semver doesn't allow that in + # the 'pre-release version' part, but we can't use the + char + # to make it 'build metadata' as that's not supported in K8s + # labels + +##==---------------------------------------------------------------------------- +## Version increment + +# increment the month if needed +if [[ "${scheme}" == "calver" ]] ; then + month="$(date '+%Y.%-m.')" + release="${current_version//$month/}" + if [[ "${release}" == "${current_version}" ]] ; then + current_version="$(date '+%Y.%-m.0')" + fi +fi + +# increment the patch digit +IFS=" " read -r -a version_array <<< "${current_version//./ }" +if [[ "${increment}" == 'patch' || "${scheme}" == 'calver' ]] ; then + (( ++version_array[2] )) +elif [[ "${increment}" == 'minor' ]] ; then + (( ++version_array[1] )) + version_array[2]='0' +elif [[ "${increment}" == 'major' ]] ; then + (( ++version_array[0] )) + version_array[1]='0' + version_array[2]='0' +fi + +new_version="${version_array[0]}.${version_array[1]}.${version_array[2]}" + +# check we haven't accidentally forgotten to set scheme to calver +# TODO: provide an override "I know my version numbers are > 2020, but it's semver!" option +if [[ "${version_array[0]}" -gt 2020 && "${scheme}" != "calver" ]] ; then + echo "🛑 The major version number is greater than 2020, but the scheme is not set to 'calver'" 1>&2 + exit 11 +fi + +# add pre-release info to version if not the default branch +if [[ "${current_ref}" != "refs/heads/${default_branch}" ]] ; then + new_version="${new_version}-pre.${git_commit}" +fi + +if [[ -z "$(echo "${new_version}" | ${grep} -P "${pcre_semver}")" ]] ; then + echo "🛑 Version incrementing has failed to produce a semver compliant version" 1>&2 + echo "â„šī¸ See: https://semver.org/spec/v2.0.0.html" 1>&2 + echo "â„šī¸ Failed version string: '${new_version}'" 1>&2 + exit 12 +fi + +echo "â„šī¸ The new version is ${new_version}" + +echo "::set-output name=version::${new_version}" diff --git a/version-lookup.sh b/version-lookup.sh new file mode 100755 index 0000000..ac09baf --- /dev/null +++ b/version-lookup.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -euo pipefail + +# https://stackoverflow.com/questions/59895/get-the-source-directory-of-a-bash-script-from-within-the-script-itself +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck source=shared.sh +source "${script_dir}/shared.sh" + +scheme="${INPUT_SCHEME:-semver}" +if [[ "${scheme}" != 'semver' && "${scheme}" != 'calver' ]] ; then + echo "🛑 Value of 'scheme' is not valid, choose from 'semver' or 'calver'" 1>&2 + exit 8 +fi + +##==---------------------------------------------------------------------------- +## MacOS compatibility - for local testing + +export grep="grep" +if [[ "$(uname)" == "Darwin" ]] ; then + export grep="ggrep" + if ! grep --version 1>/dev/null ; then + echo "🛑 GNU grep not installed, try brew install coreutils" 1>&2 + exit 9 + fi +fi + +##==---------------------------------------------------------------------------- +## Get tags from GitHub repo + +# Skip if testing, otherwise pull tags +if [[ -z "${BATS_VERSION:-}" ]] ; then + git fetch --quiet origin 'refs/tags/*:refs/tags/*' +fi + +##==---------------------------------------------------------------------------- +## Version parsing + +# detect current version - removing "v" from start of tag if it exists +current_version="$(git tag -l | { ${grep} -P "${pcre_allow_vprefix}" || true; } | sed 's/^v//g' | sort -V --reverse | head -n1)" + +# support transition from an old reecetech calver style (yyyy-mm-Rr, where R is the literal `R`, and r is the nth release for the month) +if [[ -z "${current_version:-}" ]] ; then + current_version="$(git tag -l | { ${grep} -P "${pcre_old_calver}" || true; } | sort -V --reverse | head -n1)" + if [[ -n "${current_version:-}" ]] ; then + # convert - to . and drop leading zeros & the R + current_version="$(echo "${current_version}" | sed -r 's/^([0-9]+)-0{0,1}([0-9]+)-R0{0,1}([0-9]+)$/\1.\2.\3/')" + fi +fi + +# handle no version detected - start versioning! +if [[ -z "${current_version:-}" ]] ; then + echo "âš ī¸ No previous release version identified in git tags" + # brand new repo! (probably) + case "${scheme}" in + semver) + current_version="0.0.0" + ;; + calver) + current_version="$(date '+%Y.%-m.0')" + ;; + esac +fi + +echo "â„šī¸ The current normal version is ${current_version}" + +echo "::set-output name=current-version::${current_version}"