Skip to content

Instantly share code, notes, and snippets.

@arabold
Created March 10, 2024 13:12
Show Gist options
  • Save arabold/8ba71bb341841f421ca26d15bcf94968 to your computer and use it in GitHub Desktop.
Save arabold/8ba71bb341841f421ca26d15bcf94968 to your computer and use it in GitHub Desktop.
GitHub Action: Increment Version
name: 🔄 Increment Version
description: Increment the repository version number
inputs:
namespace:
description: "Use to create a named sub-version. This value will be prepended to tags created for this version."
required: false
channel:
description: "Denote the channel or pre-release version, i.e. alpha, beta, rc, etc."
required: false
metadata:
description: "Metadata for the current version. This value will be ignored when looking for the latest version."
required: false
part:
description: "The part of the version to increment (major, minor, patch or none)."
required: true
default: ${{ github.ref == 'refs/heads/main' && 'minor' || 'patch' }}
change-path:
description: >-
Path to check for changes. If any changes are detected in the path the 'changed'
output will true. Enter multiple paths separated by spaces.
required: false
create-tag:
description: "Create a tag for the new version"
required: false
default: "false"
outputs:
namespace:
description: "The namespace"
value: ${{ steps.versioning.outputs.namespace }}
version:
description: "The version number in Semantic Versioning format without namespace or metadata"
value: ${{ steps.versioning.outputs.version }}
version-tag:
description: "The version tag"
value: ${{ steps.versioning.outputs.version-tag }}
major:
description: "Current major number"
value: ${{ steps.versioning.outputs.major }}
minor:
description: "Current minor number"
value: ${{ steps.versioning.outputs.minor }}
patch:
description: "Current patch number"
value: ${{ steps.versioning.outputs.patch }}
channel:
description: "The channel or pre-release version"
value: ${{ steps.versioning.outputs.channel }}
metadata:
description: "metadata for the current version"
value: ${{ steps.versioning.outputs.metadata }}
changed:
description: >-
Indicates whether there was a change since the last version if
change-path was specified. If no change-path was specified
this value will always be true since the entire repo is considered.
value: ${{ steps.versioning.outputs.changed }}
runs:
using: "composite"
steps:
- name: 🔄 Get Latest Version Tag
id: versioning
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
// Define the command to list and sort tags
const listTagsCommand = "git tag --list --sort=-version:refname";
// Execute the command to get the sorted tags
const tagsOutput = execSync(listTagsCommand).toString();
// Define the regex pattern for Semantic Versioning
const tagRegEx = /^(?:([a-zA-Z0-9][a-zA-Z0-9-_.]*)\@)?v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
// Filter the tags based on the regex
let versionTags = tagsOutput.split('\n').filter(tag => tagRegEx.test(tag));
// Log the filtered tags
console.log("Version tags:");
console.log(versionTags);
const namespace = "${{ inputs.namespace }}";
const channel = "${{ inputs.channel }}";
const metadata = "${{ inputs.metadata }}";
let latestTag;
let major = 0;
let minor = 0;
let patch = 0;
// Iterate the tags until we find the latest version that matches our namespace and channel
for (let i = 0; i < versionTags.length; i++) {
const tag = versionTags[i];
const match = tag.match(tagRegEx);
const tagNamespace = match[1] ?? "";
const tagChannel = match[5] ?? "";
if (tagNamespace === namespace && tagChannel === channel) {
// Found a matching tag
latestTag = tag;
major = parseInt(match[2]);
minor = parseInt(match[3]);
patch = parseInt(match[4]);
break;
}
}
if (!latestTag) {
console.log("No version tag found.");
}
// Pick the first tag
const part = "${{ inputs.part }}";
let changed;
if (["major", "minor", "patch"].includes(part)) {
// Test if anything has changed since the last version
const changePaths = `${{ inputs.change-path }}`.replaceAll(/[\s\r\n]+/gm, " ").trim();
changed = true;
if (changePaths && latestTag) {
const diffCommand = `git diff --name-only "${latestTag}" "HEAD" -- ${changePaths}`;
const diffOutput = execSync(diffCommand).toString();
changed = diffOutput.trim() !== "";
if (changed) {
console.log("Source changes detected:");
console.log(diffOutput);
} else {
console.log("No source changes detected.");
}
}
// Increment the version (if needed)
if (changed) {
console.log("Incrementing version.");
if (part === "major") {
major = major + 1;
minor = 0;
patch = 0;
} else if (part === "minor") {
minor = minor + 1;
patch = 0;
} else if (part === "patch") {
patch = patch + 1;
}
}
else {
console.log("No changes detected. Skipping version increment.");
}
}
else {
console.log("Skipping version increment.");
changed = false;
}
// Build the new version string
const version = `${major}.${minor}.${patch}` + (channel ? `-${channel}` : "");
const versionTag = (namespace ? `${namespace}@` : "") + `v${version}` + (metadata ? `+${metadata}` : "");
console.log(`Namespace: ${namespace}`);
console.log(`Version: ${version}`);
console.log(`Version Tag: ${versionTag}`);
console.log(`Major: ${major}`);
console.log(`Minor: ${minor}`);
console.log(`Patch: ${patch}`);
console.log(`Channel: ${channel}`);
console.log(`Metadata: ${metadata}`);
core.setOutput("namespace", namespace);
core.setOutput("version", version);
core.setOutput("version-tag", versionTag);
core.setOutput("major", major);
core.setOutput("minor", minor);
core.setOutput("patch", patch);
core.setOutput("channel", channel);
core.setOutput("metadata", metadata);
core.setOutput("changed", changed);
return versionTag;
# Create a tag for the new version
- name: 🏷️ Creating Tag
if: ${{ inputs.create-tag == 'true' && steps.versioning.outputs.changed == 'true' }}
run: |
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
git config --global user.name "${{ github.actor }}"
git tag -a ${{ steps.versioning.outputs.version-tag }} -m "Version ${{ steps.versioning.outputs.version }} on ${{ github.ref_name }}"
git push origin ${{ steps.versioning.outputs.version-tag }}
shell: bash
@arabold
Copy link
Author

arabold commented Mar 10, 2024

Simple, reusable GitHub Action to automatically increment an application version and tag the git repository accordingly.

Git tag syntax:

[{namespace}@]v{major}.{minor}.{patch}[-{channel}][+{metadata}]
Part Description Example Value
namespace Manage multiple separate versions in the same repository app
major Major version must be incremented if any backward incompatible changes are introduced 1
minor Minor version must be incremented if new, backward compatible functionality is introduced 0
patch Patch version must be incremented if only backward compatible bug fixes are introduced 0
channel Distribution channel or pre-release name alpha
metadata Build metadata is ignored when determining version precedence 20240310

Valid examples are:

v1.0.0
v1.0.0-alpha
app@v1.0.0
app@v1.0.0+dev
v1.0.0+20240310

This action also facilitates a branching scheme as the one below:

gitGraph:
  checkout main
  commit tag: "v1.0.0"
  commit tag: "v1.1.0"
  commit tag: "v1.2.0"

  checkout main
  branch release/qa
  commit tag: "v1.2.1+qa"

  checkout main
  branch release/demo
  commit tag: "v1.2.1+demo"

  checkout main
  commit tag: "v1.3.0"

  checkout release/qa
  commit tag: "v1.2.2+qa"
  commit tag: "v1.2.3+qa"

  checkout release/demo
  commit tag: "v1.2.2+demo"

  checkout release/qa
  merge main
  commit tag: "v1.3.1+qa"

  checkout main
  commit tag: "v1.4.0"
  commit tag: "v1.5.0"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment