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.32.0.0)
Minor bumps for new features (1.2.31.3.0)
Patch bumps for bug fixes and docs (1.2.31.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 }}"

...