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
[email protected]
[email protected]+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"
Loading

@long1eu
Copy link

long1eu commented Jun 3, 2025

how can you use this?

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