Gitea - action (semantic versioning)
In modern software development, consistent and meaningful version management is crucial for maintaining clear project evolution and enabling smooth CI/CD pipelines. This article explores a flexible Gitea Action that automatically determines semantic version numbers based on different strategies, eliminating manual version management overhead.
Overview
This Gitea Action implements intelligent semantic versioning that can automatically determine the next version number based on:
Commit message prefixes ([feat] Add new feature
,fix: Bug correction
)
Branch naming conventions (feat/new-feature
, fix/critical-bug
)
Pull request labels (feature
,bug
, docs
)
graph TD;
A[Start] --> B_subgraph;
subgraph B_subgraph [Bump version?]
direction TB
B1(Check commit type) --> B2[Check env.map mapping] --> B3{Does commit bump version?};
end
B_subgraph -- Yes --> C[Get current version];
C --> E[Bump version];
B_subgraph -- No --> D[No new version];
E --> F[Return new version number];
Key Features
🎯 Multiple Detection Strategies
The action supports three different approaches to determine version increments:
Commit-based: Analyzes the latest commit message for semantic prefixes
Branch-based: Extracts semantic information from branch names
Label-based: Uses pull request labels to determine version bump type
🧮 Automatic Version Calculation
Major bumps for breaking changes (1.2.3 → 2.0.0)
Minor bumps for new features (1.2.3 → 1.3.0)
Patch bumps for bug fixes and docs (1.2.3 → 1.2.4)
⚡ Smart Fallback System
Gracefully handles missing tags (starts from v0.0.0)
Skips version bumps when no semantic indicators are found
Provides clear error messages for debugging
🔧 Highly Configurable
The action accepts a JSON mapping that defines which keywords correspond to which version increments:
...
- name: 🧮 Determine next version
id: version-preparation
outputs:
release_needed: ${{ steps.version-preparation.outputs.RELEASE_NEEDED }}
version: ${{ steps.version-preparation.outputs.VERSION }}
release_id: ${{ steps.version-preparation.outputs.RELEASE_ID }}
env:
# Available options: commit, branch, label
# The commit schema requires the message to start with the pattern '[<type of changes>] Example commit message'.
# The branch schema requires a naming pattern where the branch name is prefixed with '<type of changes>/Example_branch_name'.
# The label will be read from the pull request's labels.
type: "label"
# 'map' is the variable name
# The '|' indicates that the following indented block is a multi-line string
map: |
{
"major": ["breaking"],
"minor": ["feature"],
"patch": ["fix", "bug", "docs"]
}
run: |
function getLastVersion() {
# Find the latest tag in the vX.Y.Z format
local lastTag=$(git tag -l "v*.*.*" -l "*.*.*" --sort=-creatordate | head -n 1)
if [ -z "$lastTag" ]; then
lastTag="0.0.0"
fi
# Remove 'v' prefix for version calculation
echo "${lastTag#v}"
}
function getTypeFromCommitPrefix() {
local regex='^\[(\w+)\]|^(\w+):'
local commitMsg=$(git log -1 --format=%s)
local prefix=""
# Try [type] format first
if [[ "$commitMsg" =~ ^\[([^]]+)\] ]]; then
prefix="${BASH_REMATCH[1]}"
# Try type: format
elif [[ "$commitMsg" =~ ^([^:]+): ]]; then
prefix="${BASH_REMATCH[1]}"
fi
if [[ -z "$prefix" ]]; then
echo "❌ Error: Could not find a valid prefix in the commit message: $commitMsg" >&2
exit 1
fi
echo "$prefix"
}
function getTypeFromBranchPrefix(){
local regex='(?<=^)\w+(?=\/)'
local branchName=$(git branch --show-current)
local prefix=$(echo "$branchName" | grep -oP "$regex")
if [[ -z "$prefix" ]]; then
echo "❌ Error: Could not find a valid prefix in the branch name: $branchName" >&2
exit 1
fi
echo "$prefix"
}
function getTypeFromLabels() {
local labels='${{ tojson(gitea.event.pull_request.labels) }}'
local label=$(echo "$labels" | jq -r 'first(.[] | .name) // "none"')
if [[ -z "$label" ]]; then
echo "❌ Error: Could not find any labels in the pull request." >&2
exit 1
fi
echo "$label"
}
function nextVersion() {
local type="$1" # MAJOR, MINOR, PATCH
local version="$2"
local major minor patch
# Parse version (remove v prefix if present)
version="${version#v}"
IFS='.' read -r major minor patch <<< "$version"
# Convert type to lowercase for comparison
type=$(echo "$type" | tr '[:upper:]' '[:lower:]')
case "$type" in
"major") major=$((major + 1)); minor=0; patch=0 ;;
"minor") minor=$((minor + 1)); patch=0 ;;
"patch") patch=$((patch + 1)) ;;
*) echo "❌ Error: Invalid version type: $type" >&2; exit 1 ;;
esac
echo "$major.$minor.$patch"
}
function getNextStep() {
local typeOfChanges="$1"
local map="$map"
# Find which version bump type corresponds to the change type
local incrementType=$(echo "$map" | jq -r --arg value "$typeOfChanges" '
to_entries[] |
select(.value[] == $value) |
.key
')
if [[ -z "$incrementType" || "$incrementType" == "null" ]]; then
incrementType="none"
fi
echo "$incrementType"
}
function main() {
echo "⚙️ Preparing of new version"
if [[ -z "$map" || -z "$type" ]]; then
echo "❌ Error: parameters map and type are required!" >&2
exit 1
fi
# Check version increment type, or skip if none is needed.
case "$type" in
"commit")
echo "✉️ Commit semantic versioning."
type=$(getTypeFromCommitPrefix)
;;
"branch")
echo "🌿 Branch semantic versioning."
type=$(getTypeFromBranchPrefix)
;;
"label")
echo "🏷️ Label semantic versioning."
type=$(getTypeFromLabels)
;;
*)
echo "❌ Error: Invalid type value: $type!"
exit 1
;;
esac
local version=""
local incrementType=$(getNextStep "$type")
if [[ -z "$incrementType" ]]; then
echo "❌ Error: Could not determine increment type for: $type" >&2
exit 1
fi
if [[ "$incrementType" == "none" ]]; then
echo "⏭️ Skipping version bump - no matching type found for: $type"
echo "VERSION=" >> $GITHUB_OUTPUT
echo "RELEASE_ID=" >> $GITHUB_OUTPUT
else
echo "🧮 Calculate new next version (type: $incrementType)"
local lastVersion=$(getLastVersion)
version=$(nextVersion "$incrementType" "$lastVersion")
echo "🎉 New version: v$version (was: v$lastVersion)"
echo "RELEASE_NEEDED=true" >> $GITHUB_OUTPUT
echo "VERSION=v$version" >> $GITHUB_OUTPUT
echo "RELEASE_ID=$version" >> $GITHUB_OUTPUT
fi
}
main
# - name: Build release
# if: ${{ steps.version-preparation.outputs.RELEASE == 'true' }}
# run: |
# echo "Building version: ${{ steps.version-preparation.outputs.VERSION }}"
# echo "Release ID: ${{ steps.version-preparation.outputs.RELEASE_ID }}"
...