Automatyczne numerowanie buildów i paczek NuGeta

W tym poście kontyuuję temat z poprzedniego, gdyż ostatnio wzbogaciłem swój proces generowania paczek o niezwykle istotną funkcję, a mianowicie wersjonowanie plików binarnych i paczek nugeta. Początkowo numer paczki był nadawany na podstawie release notes, zaś dllki nie były wersjonwane w ogóle. Tak oczywiście być nie może, nie tylko dlatego, że kiedyś mam zamiar wypuścić Pizzę w świat. Nawet testując paczki lokalnie, sam się nieraz gubię w tym, którą wersję testuję, czy prawidłowo się zaktualizowała, i czy zawiera pożądane zmiany. Nie da się tego stwierdzić, jeśli po każdym buildzie paczka ma ten sam numer, a dllki na stałe mają wpisane 1.0.0.0.

Jak numerować wersje?

Ogólnie przyjętym w świecie .NET wzorcem jest [major].[minor].[build].[revision]. Czym jest revision? Założenia są takie, że binarki z róznymi numerami revision mogą być stosowane zamiennie. Po co więc ten numerek? Moim zdaniem nie ma on za bardzo sensu. Kolejne pytanie, które można sobie zadać - kiedy powinny się zmieniać poszczególne cyfry? Zapewne są jakieś wytyczne Microsoftu w tej kwestii, ale niespecjalnie chcę je nawet poznawać. Od razu wolę wybrać sensowne rozwiazanie, czyli semantic versioning.

Ponieważ obecnie Pizza jest w fazie alfa i API zmienia się jak szalone, to do odwołania major będzie miał wartość 0, zaś paczki nugeta muszą informować o tym, że nie są jeszcze niestabilne. A zatem wymagania odnośnie wersjonowania można zdefiniować tak:

  1. Wszystkie binarki i paczki miały zawsze tę samą wersję.
  2. Pliki binarne miały numer w formacie: [major].[minor].[patch].[build].
  3. Paczki nugeta miały numer w formacie: [major].[minor].[patch]-alpha[build].
  4. [major].[minor].[patch] były jak dotąd odczytywane z release notes. Nic w tym dziwnego - zmiana numeru wersji musi być świadomą i przede wszystkim trzeźwą decyzją programisty.
  5. [build] - ma być automatycznie zwiększany po każdym buildzie. (Buildzie przez skrypt Fake, nie prez Visual Studio.)

Implementacja przy użyciu FAKE

Co jest potrzebne, aby zrealizować powyższe wymagania?

Po pierwsze odczyt ostatniego numeru builda

W normalnej sytuacji zajmuje się tym serwer CI, ale że ja go nie mam, to mam dwa wyjścia - albo oddzielny plik, albo wykorzystać fakt, że ten numer i tak znajduje się w AssemblyInfo.cs. We frameworku mam na razie cztery pliki o tej nazwie, więc musiałem wybrać jeden z nich. Wybrałem ten z modułu Pizza.Framework, ponieważ zakładam, że ma on najmniejsze szanse na zmianę nazwy czy zniknięcie w dającej się przewidzieć przyszłości.

Implementacja w F# jest banalna, wystarczy dodać open Fake.AssemblyInfoFile i napisać taką funkcję:

1 let readFrameworkAssemblyVersion () =
2     GetAttributeValue "AssemblyVersion" (srcRootDir @@ @"Pizza.Framework\Properties\AssemblyInfo.cs")
3         |> fun f -> f.Value.Trim('"')
4         |> Version.Parse

Po co tam ten Trim? Ano dlatego, że GetAttributeValue opakowuje wartość atrybutu typu string w dodatkowe cudzysłowy. Męczyłem się z tym trochę czasu, gdyż te dodatkowe nawiasy sprawiały, że Version.Parse rzucało wyjątkiem FormatException. Straciłem dobry kwadrans zanim załapałem czemu.

Po drugie - utworzenie numeru dla paczki nugetowej

To także banał:

1 let buildPackageVersion (version: Version) =
2     sprintf "%i.%i.%i-alpha%04i" version.Major version.Minor version.Build version.Revision

Na wyjściu otrzymamy coś takiego: 0.3.1-alpha0012. Istotne jest, aby numer buildu (version.Revision w kodzie powyżej) miał zera wiodące, gdyż NuGet stosuje sortowanie leksykograficzne. Bez zer wiodących uznałby wersję alpha11 za wcześniejszą niż alpha9.

Po trzecie - wstawienie zaktualizowanego numeru paczki do zależności od innego modułu Pizzy

Jest to zmiana w tej funkcji:

1 let getProjectReferencesFromCsproj (csprojContent: XDocument) =
2     let packageVersion = buildPackageVersion(readFrameworkAssemblyVersion())
3     csprojContent.Root.Descendants()
4         |> Seq.filter (fun el -> el.Name.LocalName = "ProjectReference")
5         |> Seq.map (fun el -> el.Element(XName.Get("Name", "http://schemas.microsoft.com/developer/msbuild/2003")))
6         |> Seq.map (fun el -> (el.Value, packageVersion))
7         |> Seq.toList

Wcześniej zamiast packageVersion używana była wartość releaseNotes.NugetVersion.

Po czwarte - aktualizacja wersji w kodzie generujacym paczkę

Wewnąrz funkcji createPackage:

 1 let packageVersion = buildPackageVersion(readFrameworkAssemblyVersion())
 2 
 3 NuGet (fun app ->
 4     { app with
 5         NoPackageAnalysis = true
 6         Authors = ["dwdkls"]
 7         Project = packageName
 8         Description = "Pizza description"
 9         Summary = "Pizza summary"
10         ReleaseNotes = releaseNotes.Notes |> toLines
11         Version = packageVersion
12         Publish = false
13         OutputPath = nupkgOutDir
14         WorkingDir = packagePath
15         FrameworkAssemblies = frameworkAssemblies
16         Files = [ ( @"*.*", Some @"lib\net45", None ) ]
17         Dependencies = dependencies
18     }) nuspecFile

Po piąte - dodanie kroku aktualizującego pliki AssemblyInfo

Na szczęście FAKE ma wbudowane funkcje do tego, wystarczy tylko podać odpowiednie wartości.

 1 Target "UpgradeAssemblyInfos" (fun _ ->
 2     trace "Upgrading AssemblyInfo information"
 3 
 4     let version = readFrameworkAssemblyVersion()
 5     let currentBuildNumber =
 6         match version.Revision with
 7         | -1 -> 1
 8         | _ -> version.Revision + 1
 9 
10     let currentVersion = sprintf "%s.%d" releaseNotes.NugetVersion currentBuildNumber
11 
12     let assemblyInfos = !!(srcRootDir @@ "**\AssemblyInfo.cs")
13     ReplaceAssemblyInfoVersionsBulk assemblyInfos (fun f ->
14         { f with
15                 AssemblyVersion = currentVersion
16                 AssemblyFileVersion = currentVersion
17         })
18  )

Dziwne wydaje się to wyznaczanie currentBuildVersion. Wynika to z faktu, że jeśli version.Revision nie istnieje w pliku, to jej wartość wynosi -1, a nie 0.

Na końcu wystarczy target UpgradeAssemblyInfos dać przed BuildAll i można już cieszyć się z prostego systemu CI dla ubogich. ;)

Opublikowano: