Skip to content

Automatically determine the semantic version based on merge history.

License

Notifications You must be signed in to change notification settings

AlexAtkinson/github-action-gitops-autover

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

Introduction

version license language repo size

Language/content agnostic method of automatically determining the semantic version for a product based on branch merge history with MINIMAL discipline dependencies.

This is accomplished by counting the merges of branches matching the naming scheme into the [main|master] branch. Folks familiar with Scrum/SAFe or GitFlow/fooFlow strategies will recognize this scheme.

Burning Questions

  • Yes - This can indeed be implemented in repos that previously used different version increment methods.
  • Yes - Jira will recognize the issue tag anywhere in the branch name -- it does not have to be a prefix for the integration to function.
  • Yes - This aligns with and extends on guidance from Atlassian on branch naming schemes.

Convenience link to this action on the marketplace

Recent Changes

  • 0.3.1: Update the checkout action version to v4.
  • 0.3.0: Bring back the unshallowing, which ensures the full git log is available.
    • TODO: Adjust scripts to use git log --remotes to avoid unshallowing large repos.
  • 0.2.9: Fix 'ops' increments; add user friendly error outputs.

Usage

Setup

Repository Setup
1. Disable squash merging in the repository settings.
?. If you have a previously established version for the repo, ensure it's tagged like: 'MAJOR.MINOR.PATCH'.
For example, if you have a tag like 'v1.2.3', you will want to add a tag like '1.2.3' to that commit as well, which might look like:
git checkout v1.2.3
git tag 1.2.3
git push --tags
Workflow Setup
2. Ensure your action executes a checkout step prior to using this action.
3. Use the outputs from this action as desired. For example, you might use it to update the version of an npm package:
npm version ${{ steps.gitops-autover.outputs.new-version }}
4. Tag the repo with the new version at some point in the workflow.
Team Setup
5. Ensure the iteration team adheres to the branch naming scheme defined below. Here's an example workflow. Bonus points for integrating branch management into your issue tracking system.
git checkout -b fix/not-a-feature
git commit --allow-empty -m "This was a bug and not a feature after all..."
git push --set-upstream origin fix/not-a-feature
# THEN: Click the link to create a PR & merge it

Outputs

new-version: [string]
The newly detected version.
previous-version: [string]
The previous version.

Example GH Action Workflows

Below is a valid workflow utilizing this action. If you wanted to extend it to do something like update a 'package.json' version, for example, you would simply create a step that runs: npm version $NEW_VERSION.

name: gitops-autover

on:
  push:
    branches:
    - main

jobs:
  use-action:
    name: Verify GitOps AutoVer Action
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Run GitOps Automatic Versioning Action
      id: gitops-autover
      uses: AlexAtkinson/github-action-gitops-autover@0.3.0
    - name: Verify Outputs
      run: |
        NEW_VERSION=${{ steps.gitops-autover.outputs.new-version }}
        echo "new-version: $NEW_VERSION"
        PREVIOUS_VERSION=${{ steps.gitops-autover.outputs.previous-version }}
        echo "previous-version: $PREVIOUS_VERSION"

This results in outputs like:

major:

Version 0.2.2 > 1.0.0

patch:

Version 1.0.0 > 1.0.103

minor:

Version 1.0.103 > 1.1.0

Q: How did you execute 103 merges?
A: You can use the bump scripts in the scripts directory of this
repo, like: './scripts/bumpPatch.sh 42'. (Does not work with branch protection enabled)

Additionally, this repo uses its own action for versioning, so feel free to investigate that workflow for another example.

A More Complete Example

name: gitops-autover-example
on:
  push:
    branches:
      - main
jobs:
  init:
    name: Initialize
    runs-on: ubuntu-latest
    outputs:
      REPOSITORY: ${{ steps.init.outputs.REPOSITORY }}
      PRODUCT: ${{ steps.init.outputs.PRODUCT_NAME }}
      PRODUCT_NAME_LOWER: ${{ steps.init.outputs.PRODUCT_NAME_LOWER }}
      NEW_VERSION: ${{ steps.gitops-autover.outputs.new-version }}
      PREVIOUS_VERSION: ${{ steps.gitops-autover.outputs.previous-version }}
    steps:
    - name: Checkout Source
      uses: actions/checkout@v3
      with:
        lfs: true
        fetch-depth: 0
    - name: Initialize
      id: init
      run: |
        # Detect repo name.
        REPOSITORY=${PWD##*/}
        echo "REPOSITORY=$REPOSITORY" >> $GITHUB_OUTPUT
        # Autodetect product name. Eg:
        #   A repo named cool-corp-awesome-docker will result in
        #   the image being pushed as: awesome-docker:x.x.x
        [[ $(echo -n "$(cut -d- -f3- <<< ${REPOSITORY})" | wc -c) -gt 0 ]] && PRODUCT_NAME=$(cut -d- -f3- <<< ${REPOSITORY})
        [[ $(echo -n "$(cut -d- -f3- <<< ${REPOSITORY})" | wc -c) -eq 0 ]] && PRODUCT_NAME=default
        echo "PRODUCT_NAME=$PRODUCT_NAME" >> $GITHUB_OUTPUT
        PRODUCT_NAME_LOWER=${PRODUCT_NAME,,}
        echo "PRODUCT_NAME_LOWER=$PRODUCT_NAME_LOWER" >> $GITHUB_OUTPUT
    - name: GitOps Automatic Versioning
      id: gitops-autover
      uses: AlexAtkinson/github-action-gitops-autover@0.3.0
  build:
    name: "Build"
    runs-on: ubuntu-latest
    needs: [init]
    steps:
      - name: "Build"
        run: |
          echo "SUCCESSFUL BUILD" > "${{ needs.init.outputs.PRODUCT_NAME_LOWER }}.${{ needs.init.outputs.NEW_VERSION }}.txt"
          # Then bolt on extras such as slack notify or github release actions as needed.

Discipline Dependency

Branch Naming Scheme

This action depends only on the following branch naming scheme being observed.

Branch Name Increment Description
feature/.* Minor Product features.
enhancement/.* Minor Product enhancements.
fix/.* Patch Product fixes
bugfix/.* Patch You should use fix.
hotfix/.* Patch Are you from the past?
ops/.* Patch Enables ops changes to trigger builds.

For example, the name of the branch for a new awesome feature named Awesome Feature, might be: 'feature/awesome_feature'.

Appropriate Use Cases

This action is most suitable for git projects with the following operational design:

  • Each merge into main|master is intended to produce an artifact, following the "everything is potentially releasabe" approach.

This action is not suitable for projects requiring:

  • pre-release, beta, etc., type fields. Such projects should depend upon their own language native tooling.
  • specific version numbers to be planned and orchestrated ahead of time (usually marketing efforts).
    • Exception: Major releases. These can be actioned on demand as outlined below.
  • rebase merges. Reminder: this action depends on merge commit messages.
    • Exception: Patterns like: main < (merge-commit) < staging-branch < (rebase) work-branches
      • As long as main|master gets a merge commit message, everyone is happy.

Version Format

Versions are returned only in the following format: 'MAJOR.MINOR.PATCH'.

Major Increments

MAJOR version increments depend upon manual intervention to trigger as it is not practical to automatically detect either major refactoring or accepted/planned breaking changes to a product. Human input informs the tool of such MAJOR increment qualifying scenarios.

This increment can be accomplished in one of the following ways:

  1. Push a commit message containing: '+semver: [major|breaking]'. For example:

     git commit --allow-empty -m "+semver: major"
     git push
    
  2. Push the MAJOR tag manually. That this is the less desirable option as it will require the merge of a qualifying branch to iterate the version number, making the first possible version that could be produced 'n.0.1'. Assuming successful build and testing.

     git tag 1.0.0
     git push --tags
    

Version Increment Logic

For those interested, here's some pseudo code:

lastMajor = Extract from previous git tag (why option 1 is recommended)
lastMinor = Extract from previous git tag
lastPatch = Extract from previous git tag
IF no previous git tag; THEN
    MAJOR = 0
    MINOR = 0
    PATCH = 0
IF major increment indicator; THEN
    MAJOR = lastMajor + 1
    MINOR = 0
    PATCH = 0
ELSEIF merged feature/.* or enhancement/.* branches; THEN
    MAJOR = lastMajor
    MINOR = lastMinor + count of merged branches
    PATCH = 0
ELSEIF merged bugfix/.* or hotfix/.* branches; THEN
    MAJOR = lastMajor
    MINOR = lastMinor
    PATCH = lastPatch + count of merged branches

[Known|Non]-Issues

  • If there are no merges of branches conforming to the above naming scheme, this action will fail with the following output:

      ERROR: No feature, enhancement, fix, bugfix, hotfix, or ops branches detected!
    
    • When encountering this scenario, and a build is desired, you can simply create a branch with the appropriate naming convention and an empty commit, then merge it. Or use the bump scripts in the 'scripts/' directory of the repo for this action.
  • Merged branches not conforming to the above naming scheme will simply be ignored.

    • HINT: This can be useful when you don't want to increment the version.
      • Align this with build 'on:push:branches:' workflow configuration to avoid unnecessary builds.
  • The version output does not have a 'v' prefix.

    • If you would like a 'v' prefix to your versions, add it to your logic when using the output from this action. For example:

      echo "The new version is v${{ steps.detect-version.outputs.new-version }}"
                               ^ here
      
  • If both 'main' and 'master' branches exist remotely: FAIL

    • This will not be changed.
  • Squash merging must be disabled. This is required to populate the git log with the commit messages issued from the git provider.

Future Enhancements

PRs are welcome.

  • input(s): iteration-branches (map) - inform MINOR and PATCH incrementing branch name patterns.
  • input(s): mono-mode (bool) - version subdirs discretely
  • CAN'T DO: DONE: unshallow from last version tag to latest commit to... Seems a limitation of (git at first glance). See the Checkout From Tag action.