Git Tag als Version bei FAKE Builds

Git Tag als Version bei FAKE Builds

Versionsgeschichte
Version Aktualisierungsdatum Änderung
0.1 02.05.2016 Erste Fassung

Seit einiger zeit nutze ich FAKE als Build-Script für meine .Net Projekte (nicht nur Privat). Außerdem nutze ich git-flow als Branching-Model für die Entwicklung. Somit erzeuge ich die neuen Versionsnummern beim Beenden von release- und hotfix-Branches. Diese Information will ich nicht jedes mal auch beim CI-Server nachtragen, sondern im Idealfall direkt aus dem jeweiligen Tag ableiten. Dazu habe ich nun ein kleines F# Snippet geschrieben (Link zum Quellcode ist in der Seitenleiste), der mir die Arbeit erleichtert.

Da mein F#-Kenntnisse momentan nur rudimentär sind, ist der Feedback ausdrücklich gewünscht.

Enum (Union) für den Branch-Typ

type ReleaseType = Release | Beta | Alpha | ReleaseCandidate

In der Zeile 4 definiere ich einen Union (entspricht im Grunde einem Enum bei anderen .Net Sprachen). Dieser dient später für die Definition der nuget-Version (z.B.: 0.2.3-Beta23).

SHA1 für den letzten Tag

/// Get the SHA1 for the last available tag or None,
/// if not tags set in repository
let getShaFromLastTag =
    let gitLastTagShaCommand = "rev-list --tags --max-count=1"
    let ok,msg,error = runGitCommand "" gitLastTagShaCommand
    if ok then Some msg.[0] else None

Dieser Code bestimmt den SHA1 Hash von dem Commit, der dem letzten Tag zugeordnet ist (egal in welchen Branch). Da bei Git-flow die Tags nur auf dem master-Branch gesetzt werden, ist es wichtig, den Tag von allen Branches zu holen, auch wenn man aktuell zum Beispiel auf einen feature-Branch ist.

Wenn das Repository noch keinen Tag hat, wird None zurückgegeben (ein Wert, der bei F# bei optionalen Variablen angibt, dass diese keinen Wert zugeordnet bekommen hat). Bitte nich mit null aus C# verwechseln.

Tag aus SHA1

/// Get the tag name for the given SHA1 commit or None,
/// if no tag for this commit
let getTagForCommit sha =
    let gitLastTagCommand = sprintf "describe --tag %s" sha
    let ok,msg,error = runGitCommand "" gitLastTagCommand
    if ok then Some (msg |> Seq.head) else None

Dieser Code extrahiert nun den Tag Namen aus dem vorher bestimmten SHA1 Commit, falls vorhanden. Wenn zu dem SHA1 Commit kein Tag bestimmt werden konnte, wird wieder None zurückgegeben.

Parsen des Tags als Version

/// Get version from the last tag in git
/// or "0.0.0" version as fallback
let getLastTag prefix =
    let fallbackVersion = "0.0.0"
    let tagVersion =
        match getShaFromLastTag with
        | Some s ->
            match getTagForCommit s with
            | Some ss -> ss
            | None _ -> prefix + fallbackVersion
        | None _ -> prefix + fallbackVersion
    let pattern =
        if isNullOrEmpty prefix then "^" else sprintf "(?<=^%s){1}" prefix
    let pattern = sprintf "%s%s" pattern "(\d+\.\d+){1}(\.\d+){0,2}$"
    let versionRegex = System.Text.RegularExpressions.Regex(pattern)
    match versionRegex.Match tagVersion with
    | s when s.Success -> SemVerHelper.parse s.Value
    | _ -> SemVerHelper.parse fallbackVersion

Nun wird aus dem ausgelesenen Tag die Versionsnummer bestimmt. Dabei kann der Tag auch einen vordefinierten Prefix (z.B.: V_0.2 als Tag hat Prefix V_) haben. Zum Parsen wird der SemVer Parser von FAKE benutzt. Falls kein Tag bestimmt werden konnte, oder der Tag nicht dem Pattern einer Version entspricht, wird die Standard-Version "0.0.0" benutzt.

Release Status bestimmen

/// Release state
let getReleaseState =
    let currentBranch = Git.Information.getBranchName ""
    match currentBranch with
    | "master" -> Release
    | "develop" -> Beta
    | s when startsWith s "hotfix/" -> ReleaseCandidate
    | s when startsWith s "release/" -> ReleaseCandidate
    | _ -> Alpha

Nun wird der Release-Status aus dem aktuellen Branch bestimmt. Die Bestimmung erfolgt anhand der standard git-flow Branch-Namen:

  • master: Release
  • develop: Beta
  • Beginnt mit hotfix/ oder release/: ReleaseCandidate
  • Alle anderen: Alpha

Versionsnummer für Assembly

/// Get the version for assembly (+1 on patch level, if not on master)
let getAssemblyVersion prefix =
    let version = getLastTag prefix
    let patch =
        match getReleaseState with
        | Release -> version.Patch
        | _ -> version.Patch + 1
    sprintf "%d.%d.%d.%s" version.Major version.Minor patch buildCounter

In diesem Code-Abschnitt wird die Versionsnummer für die Assembly bestimmt. Diese soll ja vierstellig und numerisch sein. Die vierte Stelle wird von dem jeweiligen CI-Server (abhängig von Server heißen diese Buildnummer, Buildcounter usw.) vergeben. Bei den lokalen Builds nutze ich dazu den Platzhalter "0".

Wenn das Build nicht auf dem master Branch läuft, erhöhe ich die Patch-Nummer um eins, um zu zeigen, dass es sich um einen Prerelease für die nächste Version handelt (Tag für die nächste Version existiert ja noch nicht).

Beispiele

  • Tag: 0.1, Counter: 15, Branch: master
    • Assembly-Version: 0.1.0.15
  • Tag: 0.1, Counter: 15, Branch: develop
    • Assembly-Version: 0.1.1.15

Versionsnummer für nuget Package

/// Get the version for nuget package (+1 on patch level, if not on master)
let getNugetVersion prefix =
    let version = getLastTag prefix
    let releaseState = getReleaseState
    let patch =
        match releaseState with
        | Release -> version.Patch
        | _ -> version.Patch + 1
    match releaseState with
    | Release -> sprintf "%d.%d.%d" version.Major version.Minor patch
    | s -> sprintf "%d.%d.%d-%A%s" version.Major version.Minor patch s buildCounter

Nun wird auch die Versionsnummer für das nuget-Package bestimmt. Die ersten drei Stellen entsprechen denen der Assembly-Version. Die letzte Stelle enthält eventuell noch den Prerelease-Suffix mit dem Counter des CI-Servers.

Beispiele

  • Tag: 0.1, Counter: 15, Branch: master
    • nuget-Version: 0.1.0
  • Tag 0.1, Counter: 15, Branch: develop
    • nuget-Version: 0.1.1-Beta15
  • Tag 0.2.7, Counter: 25, branch: feature/Feature1
    • nuget-Version: 0.2.8-Alpha25

Nutzung in FAKE Script

let assemblyVersion = getAssemblyVersion ""
let nugetVersion = getNugetVersion ""
...
Description "Update assembly info"
Target "UpdateAssembly" (fun _ ->
    BulkReplaceAssemblyInfoVersions "src/" (fun p ->
        {p with
            AssemblyVersion = assemblyVersion
            AssemblyFileVersion = assemblyVersion
            AssemblyInformationalVersion = assemblyVersion
        }
    )
)
...
Description "Create nuget package of the library"
Target "CreatePackage" (fun _ ->
    NuGet (fun p ->
        {p with
            Authors = authors
            Project = project
            Version = nugetVersion
            OutputPath = artifactOutput
            Summary = summary
            Description = description
            WorkingDir = "./"
            Tags = tags
            Publish = false
        }
    ) "Template.nupkg"
)
...

Nach dem Snippet können nun die notwendigen Versionen aus dem Git-Tag für die Assembly und die nuget-Version bestimmt werden.