Version Derivation

Derive semantically correct versions from Git repository state and commit messages.

libraryDependencies += "africa.shuwari" %%% "version-resolution" % "0.9.0"

Available on JVM and Scala Native.

Mode Selection

Condition Result
Basis commit has a valid annotated tag AND clean worktree Concrete version - exact tag
Otherwise Development version - snapshot with metadata

"Dirty" includes modified tracked files and untracked files (excluding ignored).

Concrete Version

At a clean tagged commit, the exact tag version is returned:

Tag: v2.3.1  ->  2.3.1

Development Version

Between releases, a target version is computed:

Priority:

  1. Valid target directive - explicit target: X.Y.Z in commits
  2. Absolute sets - version: major: 3 (highest per component wins)
  3. Relative changes - version: major or breaking: (coalesced)
  4. Default fallback:
Condition Target Core
Base is pre-release Core unchanged
Base is final Patch + 1
No base, repo has tags Highest tag + breaking bump
No tags anywhere 0.1.0

While the base is in initial development (major version 0), a major/breaking relative change advances the minor component; reaching 1.0.0 requires an explicit target: or absolute set. See SemVer Behaviour.

Build Metadata

Development versions ship with structured metadata describing the basis commit. The resolver populates a DevelopmentMetadata record; the chosen Formatter decides whether and how it appears in the rendered version string.

For the SemVer scheme, the default developmentVersion writes these identifiers, in order, into the + build-metadata section:

Position Identifier Condition
1 yyyymmddhhmm (UTC committer time) Basis commit available
2 <branch> (sanitised) or detached Always
3 <sha> (lowercase hex, full length) Always
4 pr<N> PR number supplied
5 dirty Worktree dirty

The 12-character UTC timestamp leads so that raw string comparison of two snapshots of the same base sorts them in commit-time order. The timestamp is the basis commit's committer time, not the build time, so re-runs are reproducible. Git-recorded times can be skewed (backdated or forward-dated commits); the resolver renders them as-is.

For PR builds, the branch slot carries the target branch (where the merge will land); the source branch is typically too volatile to be useful in a version string. Branch names are sanitised for the SemVer build-metadata grammar at render time (lowercased; non-[0-9a-z-] replaced with -; runs of - collapsed; leading/trailing - trimmed). The raw branch label remains in DevelopmentMetadata.branch for programmatic consumers.

v.show and the default version setting exclude build metadata. Configure versionResolver with a SemVer.Formatter.Full (optionally with a truncated SHA via .withShaLength(N)) to include metadata in the rendered string.

Example: 1.2.4-SNAPSHOT+202605170145.main.1234567890ab.pr42.dirty

Tag Recognition

A tag is recognised as a valid version tag if:

  • It is an annotated tag (lightweight tags are ignored)
  • Its name parses as valid SemVer (optional v/V prefix)
  • Pre-release classifiers map to known aliases
  • All numeric components are within bounds

When multiple valid tags exist on one commit, a final release outranks a pre-release of the same core. Otherwise, the highest version wins.

Commit Scanning

Condition Scan Range
Base tag exists Commits after base up to HEAD
No base tag All commits reachable from HEAD

All commits are scanned for keywords, including merge commits and commits from merged branches. See the Specification for ignore-directive semantics and the commit-count convention.

API Usage

Most consumers use the sbt plugin. For direct API usage:

import version.semver.*
import version.resolution.*
import version.resolution.logging.{NullLogger, Verbose}

val config = ResolutionConfig.default[SemVer]("/path/to/repo")
val result = VersionCliCore.resolve(config, openRepository, NullLogger, Verbose.disabled)
result match
  case Right(version) => println(version.show)
  case Left(error)    => println(error.message)

Customise via .copy:

val config = ResolutionConfig.default[SemVer]("/path/to/repo").copy(
  prNumber = Some(42),
  branchOverride = Some("feature/xyz")
)

See Also