Fake it... ale nie tak jak myślisz - Build - Run Unit Tests - Publish
Posty z tej serii:
- Fake it… ale nie tak jak myślisz - NServiceBus Web Host
- Fake it… ale nie tak jak myślisz - ASP.NET Web Host
- Fake it… ale nie tak jak myślisz - NServiceBus Windows Service Host
- Fake it… ale nie tak jak myślisz - Build - Run Unit Tests - Publish
Witam Cię w ostatniej części serii, w której opisuję, w jaki sposób zrealizowałem mechanizmy wdrażania nowych wersji Systemu komentarzy na blogu, wykorzystując do tego narzędzie FAKE. Trzy poprzednie artykuły dotyczyły wgrywania artefaktów ze zmianami. W tym artykule przejdziemy przez funkcjonalności przygotowania kodu do wdrożenia.
Design
Cały proces składa się z kroków:
- Pobranie źródeł z systemu kontroli wersji
- Kompilacja kodu
- Uruchomienie testów jednostkowych
- Przygotowanie do wdrożenia Endpointa BlogComments, za pomocą komendy dotnet publish
- Przygotowanie do wdrożenia komponentu Web, za pomocą tej samej komendy
dotnet publish
W tym momencie natrafiamy na klasyczny element do rozstrzygnięcia - w jaki sposób podzielić kod. Co powinno być razem, a co osobno. Pisałem o tym trochę w drugiej części. Strategia, która w takich sytuacjach pomaga mi podjąć decyzję, zwłaszcza wtedy, kiedy od razu nie widać rozwiązania, polega na rozważaniu dwóch skrajnych podejść:
- Maksymalnie wszystko uwspólnić
- Maksymalnie wszystko rozdzielić
W ten sposób zwiększam swoją szansę na zauważenie plusów oraz minusów. Następnie bazując na tej wiedzy, podejmuję decyzję.
W pierwszym przypadku mógłbym zakodować wszystko w jednym Targecie w jednym pliku .fsx. Plus byłby taki, że miałbym wszystko w jednym miejscu i od razu wiedziałbym, w którym miejscu wprowadzać ewentualne zmiany. Minusem mogłoby być to, że z każdą zmianą musiałbym testować cały proces od początku do końca.
W drugim przypadku mógłbym umieścić każdy krok w osobnym Targecie, a każdy Target w osobnym pliku .fsx. Plusem byłoby to, że mógłbym wprowadzać zmiany niezależnie dla każdego kroku. Minus byłby taki, że miałbym kod w różnych miejscach.
Jeśli udało Ci się zrealizować ćwiczenie opisane w drugiej części, to zauważysz, że mamy tutaj taką samą sytuację, a mianowicie plusy pierwszego podejścia stają się automatycznie minusami podejścia drugiego i odwrotnie.
Jeśli żadne z podejść mi nie pasuję, to przechodzę do drugiej iteracji, patrząc, co mogłoby być razem, a co osobno itd.
Ostatecznie wyszło mi, że najbardziej pasującą opcją jest zakodowanie poniższych kroków w jednym pliku .fsx:
- Target o nazwie Compile Code:
- Pobranie źródeł z systemu kontroli wersji
- Kompilacja kodu
- Target o nazwie Run Unit Tests:
- Uruchomienie testów jednostkowych
Krok przygotowania do wdrożenia za pomocą komendy dotnet publish pasuje, aby był w osobnym pliku .fsx jako jeden Target o nazwie Publish Artifacts, który w parametrze przyjmuję ścieżkę do projektu, który ma zostać opublikowany.
Develop
Mając zaprojektowane rozwiązanie, możemy przejść do implementacji. Na początek kompilacja kodu oraz testy jednostkowe np. w pliku o nazwie build.fsx:
// Targets
Target.create "Compile Code" (fun _ ->
let buildArtifactsWorkingDirectoryPath = retrieveParam buildArtifactsWorkingDirectoryPathParamName
let gitRepositoryUrl = retrieveParam gitRepositoryUrlParamName
let buildArtifactsSubDirectoryName = retrieveParam buildArtifactsSubDirectoryNameParamName
let solutionRelativePath = retrieveParam solutionRelativePathParamName
let buildArtifactsPath = buildArtifactsWorkingDirectoryPath + "\\" + buildArtifactsSubDirectoryName
let slnPath = buildArtifactsPath + "\\" + solutionRelativePath
Trace.trace ("-> Solution " + slnPath)
Trace.trace ("-> Clean " + buildArtifactsPath)
Shell.cleanDir buildArtifactsPath
Trace.trace ("-> Clone " + gitRepositoryUrl)
Repository.clone buildArtifactsWorkingDirectoryPath gitRepositoryUrl buildArtifactsSubDirectoryName
Trace.trace ("-> Build " + slnPath)
DotNet.build (fun p -> { p with Configuration = DotNet.BuildConfiguration.Release }) slnPath
Trace.trace "-> Code built successfully."
)
Target.create "Run Unit Tests" (fun _ ->
let buildArtifactsWorkingDirectoryPath = retrieveParam buildArtifactsWorkingDirectoryPathParamName
let buildArtifactsSubDirectoryName = retrieveParam buildArtifactsSubDirectoryNameParamName
let solutionRelativePath = retrieveParam solutionRelativePathParamName
let buildArtifactsPath = buildArtifactsWorkingDirectoryPath + "\\" + buildArtifactsSubDirectoryName
let slnPath = buildArtifactsPath + "\\" + solutionRelativePath
Trace.trace ("-> Solution " + slnPath)
DotNet.test (fun p ->
{ p with
NoBuild = true
Configuration = DotNet.BuildConfiguration.Release
}) slnPath
Trace.trace "-> Unit Tests run successfully."
)
//// Dependencies
open Fake.Core.TargetOperators
"Compile Code"
==> "Run Unit Tests"
// start build
Target.runOrDefaultWithArguments "Run Unit Tests"Standardowo w powyższym kodzie pominąłem elementy opisane w poprzednich częściach tej serii. Całość implementacji możesz zobaczyć na moim GitHubie.
Elementem startowym jest uruchomienie testów jednostkowych, które zależne są od sukcesu kompilacji kodu. Kod pobierany jest z Gita za pomocą funkcji clone należącej do modułu Repository, który udostępniany jest przez FAKEa. Następnie uruchamiana jest kompilacja za pomocą funkcji build należącej do modułu DotNet, który również dostarcza FAKE.
Kiedy dotarłem do momentu kompilacji kodu, wpadła mi nowa wiedza z języka F#. W poprzedniej implementacji, zakodowanej w PowerShellu, jawnie podawałem konfiguracją Release. W implementacji z wykorzystaniem funkcji DotNet.build chciałem uzyskać taki sam efekt. Sygnatura funkcji w piątej wersji FAKEa wygląda tak:
val build:setParams:(DotNet.BuildOptions -> DotNet.BuildOptions) -> project:string -> unitCo oznacza, że funkcja przyjmuje dwa parametry:
- Pierwszy parametr jest funkcją, która przyjmuje parametr typu
BuildOptionsi zwraca wartość typuBuildOptions - Drugi parametr to ścieżka do projektu/solucji do kompilacji
Funkcja nie zwraca żadnej wartości.
Pierwsza myśl, jaka przyszła mi do głowy, to podejście obiektowe. Podstawię pod zmienną BuildOptions.Configuration nową wartość udostępnianą przez FAKEa - DotNet.BuildConfiguration.Release. Efekt? Błąd kompilacji z informacją:
This field is not mutable
No tak. W F# domyślnie wszystkie już utworzone wartości są niezmienne. Poza tym BuildOptions nie jest klasą, tylko typem Record, który nie występuję np. w wersji ósmej języka C#. Research w internecie naprowadził mnie na nową konstrukcję języka F# - Copy and Update Record Expressions. Połączenie tej konstrukcji z możliwością tworzenia funkcji anonimowych okazało się tym, czego potrzebowałem. Fragment kodu:
DotNet.build (fun p -> { p with Configuration = DotNet.BuildConfiguration.Release }) slnPathoznacza utworzenie anonimowej funkcji z parametrem p, który jest typu BuildOptions, z ciałem funkcji, w której za pomocą słowa kluczowego with podmieniany jest Record field o nazwie Configuration na wartość DotNet.BuildConfiguration.Release. Drugim parametrem funkcji DotNet.build jest ścieżka do pliku .sln.
Uruchomienie testów jednostkowych odbywa się poprzez wywołanie funkcji DotNet.test, gdzie test jest nazwą funkcji, a DotNet modułem udostępnianym przez FAKEa. Wywołanie funkcji odbywa się za pomocą tej samej konstrukcji, którą opisałem w powyższym akapicie.
Przejdźmy teraz przez implementację publikacji, umieszczając kod np. w pliku publish.fsx:
// Targets
Target.create "Publish Artifacts" (fun _ ->
let projectPath = retrieveParam projectPathParamName
let rid = retrieveParam ridParamName
Trace.trace ("-> Project " + projectPath)
Trace.trace ("-> Publish " + projectPath)
DotNet.publish (fun p ->
{ p with
Configuration = DotNet.BuildConfiguration.Release
NoBuild = true
Runtime = if rid = "empty" then p.Runtime else Some rid
}) projectPath
Trace.trace "-> Code published successfully."
)
//// Dependencies
// start build
Target.runOrDefaultWithArguments "Publish Artifacts"Zgodnie z założeniami projektowymi, jeden Target odpowiada za publikację, która odbywa się poprzez wywołaniem funkcji publish należącej do modułu DotNet. Funkcja przyjmuje konfigurację pod postacią rekordu BuildOptions oraz ścieżkę do projektu, który ma zostać opublikowany. Tym razem w opcjach podmieniamy trzy wartości:
Configuration- tak, jak poprzednio, ustawiamy wersję ReleaseNoBuild- mamy już zbuildowany kod, więc nie trzeba powtarzać tej operacjiRuntime- jeśli chcemy opublikować kod pod konkretne środowisko np. Windows
Test & Deploy
Stworzyliśmy dwa skrypty FAKEa. Do przygotowania do wdrożenia całego Systemu komentarzy potrzebne są trzy skrypty PowerShell. Pierwszy np. run_build.ps1 uruchamia kompilację oraz testy jednostkowe:
# fake build parameters
$buildArtifactsWorkingDirectoryPath = "buildArtifactsWorkingDirectoryPath=[path_to_sln_directory]"
$gitRepositoryUrl = "gitRepositoryUrl=[path_to_git_repository]"
$buildArtifactsSubDirectoryName = "buildArtifactsSubDirectoryName=build_artifacts"
$solutionRelativePath = "solutionRelativePath=src"
#execute script
fake run build.fsx -e $buildArtifactsWorkingDirectoryPath -e $gitRepositoryUrl -e $buildArtifactsSubDirectoryName -e $solutionRelativePathDrugi np. run_publish_blogcomments.ps1 publikuje kod Endpointa BlogComments:
# fake build parameters
$projectPath = "projectPath=[path_to_endpoint_project]"
$rid = "rid=empty"
#execute script
fake run publish.fsx -e $projectPath -e $ridTrzeci np. run_publish_web.ps1 publikuje kod komponentu Web:
# fake build parameters
$projectPath = "projectPath=[path_to_web_project]"
$rid = "rid=empty"
#execute script
fake run publish.fsx -e $projectPath -e $ridKod uruchamiany jest tak samo, niezależnie od tego, czy artefakty mają być wdrażane na środowisko testowe, czy produkcyjne.
W ten oto sposób dotarliśmy do końca serii, w której podzieliłem się z Tobą moimi doświadczeniami przy realizacji funkcjonalności CI/CD z wykorzystaniem narzędzia FAKE. Samo narzędzie posiada o wiele więcej możliwości. Jeśli chcesz sprawdzić w praktyce, jak działa mechanizm, wdrożony za pomocą skryptów, z którymi zapoznałeś/zapoznałaś się w tej serii, zostaw komentarz pod tym artykułem. Jestem ciekaw, jakich narzędzi używasz do realizacji procesów CI/CD dla funkcjonalności, którymi się zajmujesz.
=