name: Calculate version description: Validate Conventional Commits and calculate the build version inputs: commit-types: description: Comma-separated Conventional Commit types required: false default: feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert outputs: version: description: Version for dotnet build and pack value: ${{ steps.version.outputs.version }} base-version: description: Version before the current changes value: ${{ steps.version.outputs.base-version }} label: description: Build label, such as next or pr.42 value: ${{ steps.version.outputs.label }} metadata: description: Build metadata, such as sha.abc123 value: ${{ steps.version.outputs.metadata }} bump: description: Highest version increment found value: ${{ steps.version.outputs.bump }} runs: using: composite steps: - id: version shell: bash env: VERSION_EVENT_NAME: ${{ github.event_name }} VERSION_REF_NAME: ${{ github.ref_name }} VERSION_BASE_REF: ${{ github.base_ref }} VERSION_HEAD_SHA: ${{ github.event.pull_request.head.sha }} VERSION_PR_NUMBER: ${{ github.event.pull_request.number }} VERSION_COMMIT_TYPES: ${{ inputs.commit-types }} run: | set -euo pipefail error() { echo "::error::$*" >&2 exit 1 } command -v git >/dev/null || error "git is required" event_name="${VERSION_EVENT_NAME:-${GITHUB_EVENT_NAME:-}}" ref_name="${VERSION_REF_NAME:-${GITHUB_REF_NAME:-}}" base_ref="${VERSION_BASE_REF:-${GITHUB_BASE_REF:-}}" head_sha="${VERSION_HEAD_SHA:-${GITHUB_SHA:-HEAD}}" pr_number="${VERSION_PR_NUMBER:-${PR_NUMBER:-}}" commit_types="${VERSION_COMMIT_TYPES:-feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert}" type_pattern="${commit_types//,/|}" output_file="${GITHUB_OUTPUT:-}" write_output() { [[ -n "$output_file" ]] && printf '%s=%s\n' "$1" "$2" >>"$output_file" } valid_version() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] } tag_version() { valid_version "$1" || error "Tag '$1' is not a valid release version (expected MAJOR.MINOR.PATCH)" printf '%s' "$1" } latest_release_tag() { git tag --list '[0-9]*' --sort=-version:refname | head -n 1 } version_parts() { IFS=. read -r major minor patch <<<"$1" printf '%s %s %s' "$major" "$minor" "$patch" } bump_version() { local version=$1 bump=$2 major minor patch read -r major minor patch <<<"$(version_parts "$version")" case "$bump" in major) major=$((major + 1)); minor=0; patch=0 ;; minor) minor=$((minor + 1)); patch=0 ;; patch) patch=$((patch + 1)) ;; none) ;; *) error "Unknown version bump '$bump'" ;; esac printf '%s.%s.%s' "$major" "$minor" "$patch" } commit_bump() { local commit=$1 validate=${2:-1} subject body bump=none type subject=$(git log -1 --format=%s "$commit") body=$(git log -1 --format=%b "$commit") if ! [[ "$subject" =~ ^($type_pattern)(\([^()]+\))?(!)?:[[:space:]]+[^[:space:]].*$ ]]; then [[ "$validate" == 0 ]] && { printf none; return; } error "Commit $commit has an invalid Conventional Commit subject: $subject" fi type=${BASH_REMATCH[1]} if [[ "$subject" =~ ^[^:]+\!: ]] || [[ "$body" == *"BREAKING CHANGE:"* ]] || [[ "$body" == *"BREAKING-CHANGE:"* ]]; then bump=major elif [[ "$type" == feat ]]; then bump=minor elif [[ "$type" == fix ]]; then bump='patch' fi printf '%s' "$bump" } higher_bump() { case "$1:$2" in major:*) printf major ;; *:major) printf major ;; minor:minor|minor:patch|patch:minor) printf minor ;; patch:patch) printf patch ;; none:*) printf '%s' "$2" ;; *:none) printf '%s' "$1" ;; *) error "Cannot compare bumps '$1' and '$2'" ;; esac } commits_between() { git rev-list --reverse --no-merges "$1..$2" } calculate_bump() { local commits=$1 validate=${2:-1} commit bump=none current while IFS= read -r commit; do [[ -n "$commit" ]] || continue current=$(commit_bump "$commit" "$validate") bump=$(higher_bump "$bump" "$current") done <<<"$commits" printf '%s' "$bump" } tag=$(latest_release_tag || true) if [[ -n "$tag" ]]; then base_version=$(tag_version "$tag") tag_start=$tag else base_version=0.0.0 tag_start=$(git rev-list --max-parents=0 HEAD | tail -n 1) fi if [[ "$event_name" == pull_request || -n "$base_ref" ]]; then [[ -n "$pr_number" ]] || error "Pull request number is required" [[ -n "$base_ref" ]] || error "Pull request base ref is required" base_commit=$(git rev-parse "$base_ref" 2>/dev/null || git rev-parse "origin/$base_ref") head_commit=$(git rev-parse "$head_sha") pr_commits=$(commits_between "$base_commit" "$head_commit") pr_bump=$(calculate_bump "$pr_commits") base_bump=$(calculate_bump "$(commits_between "$tag_start" "$base_commit")" 0) bump=$(higher_bump "$base_bump" "$pr_bump") version=$(bump_version "$base_version" "$bump")-pr."$pr_number".sha."$head_commit" label="pr.$pr_number" metadata="sha.$head_commit" elif [[ "$event_name" == push && "$ref_name" == [0-9]* || "$ref_name" == [0-9]* ]]; then version=$(tag_version "$ref_name") bump=none label=release metadata=none else head_commit=$(git rev-parse "$head_sha") commits=$(commits_between "$tag_start" "$head_commit") bump=$(calculate_bump "$commits") version=$(bump_version "$base_version" "$bump")-next.sha."$head_commit" label=next metadata="sha.$head_commit" fi write_output version "$version" write_output base-version "$base_version" write_output label "$label" write_output metadata "$metadata" write_output bump "$bump" printf 'Calculated version: %s\n' "$version"