Featured image of post Nuke – wygodne CI/CD programu w C#. Część 1

Nuke – wygodne CI/CD programu w C#. Część 1

Nuke pozwala nam na proste opisanie procesu budowania i publikowania naszej aplikacji w języku C#. Niniejszy artykuł jest wstępem do serii. W tej części dowiesz się czym jest Nuke i jak go uruchomić w swoim projekcie. Oprócz konfiguracji środowiska i wyjaśnienia kilku podstawowych elementów pokażę Ci jak dodać wykonanie testów jednostkowych.

Spis treści

Końcowy kod z opisanymi tutaj elementami, i kilkoma więcej, znajdziesz na moim GitHubie: Ztr.AI.

Wstęp

Wyobraź sobie, że całe CI/CD swojej aplikacji możesz opisać za pomocą C#. Bez potrzeby nauki środowiska graficznego TeamCity, czy YAML’i GitHuba.

Nuke jest jednym ze sposobów opisu budowania naszej aplikacji w języku C#. Jeżeli chcemy, aby nasz projekt był cały czas łatwo zarządzalny, musimy mieć sposób na opis wszystkich wymagań stawianych przed nim. I tak oto, możemy opisać:

  1. (Czyszczenie) jak powinny wyglądać katalogi przed rozpoczęciem budowania projektu,

  2. (Przygotowanie) jakich zależności potrzebuje aplikacja do kompilacji,

  3. (Budowanie) w jakiej kolejności budować poszczególne elementy,

  4. (Testowanie) jak uruchamiać testy jednostkowe i jakie są wymagania co do pokrycia nimi naszego kodu,

  5. (Publikacja) jak spakować aplikację i wysłać ją na środowisko produkcyjne.

Nie jest to kompletna lista, tego, co możemy zrobić w trakcie procesu ciągłej integracji i dostarczania. Na pewno pokazuje ona podstawowe zagadnienia, przed którymi jesteśmy stawiani podczas wydawania kolejnej wersji naszego oprogramowania. I jak to w życiu bywa, jeśli czegoś nie zapiszemy, to pewno o tym zapomnimy. A po co zapisywać coś na skrawku papieru, gdy można to zrobić za pomocą kodu, który wykona się sam?

Wszystkie wyżej opisane kroki możemy opisywać w różnych językach. Robimy to często w tak wyspecjalizowanych formach, jak projekty budujące na TeamCity, czy GitHub Actions. Nuke przekonał mnie tym, że nie muszę uczyć się tych wszystkich konfiguracji osobno – mogę opisać wszystko, co niezbędne za pomocą dobrze mi znanego języka C# i wykorzystywać tam, gdzie potrzebuję. W przypadku GitHub Actions mogę bez problemu również wykorzystać raz napisany kod w wielu różnych przepływach, co stanowczo upraszcza pracę i testowanie.

Wstępna konfiguracja z pomocą kreatora

Wstępną konfigurację naszego projektu warto zacząć od instalacji narzędzia Nuke, które stanowczo ułatwia pracę dewelopera. Co ważne, nie jest ono niezbędne na serwerze budującym.

dotnet tool install Nuke.GlobalTool --global
nuke setup
Ilustracja 1. Widok konfiguratora Nuke

Narzędzie poprosi o podjęcie szeregu decyzji, jak widać na ilustracji powyżej:

  1. Wybierz nazwę projektu budującego.

  2. Katalog, w którym będzie on zapisany.

  3. Wersję Nuke.

  4. Domyślne rozwiązanie, które będzie budowane.

  5. Czy chcesz, aby podstawowe polecenia budujące zostały już umieszczone w nowym projekcie.

  6. Środowisko, które będzie budować twoje rozwiązanie.

  7. Miejsce, gdzie są umieszczone twoje projekty (w tym katalogu będzie przeprowadzane czyszczenie dodatkowe).

  8. Gdzie mają trafiać artefakty, czyli pliki wynikowe budowania, jak paczki nuget.

  9. Gdzie są twoje projekty, które testują rozwiązanie.

  10. Czy używasz GitVersion. Ja wybrałem, że tak, jednak opis tego narzędzia znajdzie się w innym artykule.

Rezultatem takich wyborów, jest klasa C#, która będzie wyglądać mniej więcej tak, jak poniżej. Obok tego dostaniemy kilka plików build.sh, build.cmd i build.ps1, które pozwalają nam na budowę naszej aplikacji nawet w środowisku, które nie ma zainstalowanego środowiska .Net. Pojawi się również katalog .nuke, który przechowuje kilka ustawień.

Widok klasy Build.cs
[CheckBuildProjectConfigurations]
[ShutdownDotNetAfterServerBuild]
class Build : NukeBuild
{
    public static int Main () => Execute<Build>(x => x.Compile);

    [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
    readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;

    [Solution] readonly Solution Solution;
    [GitRepository] readonly GitRepository GitRepository;
    [GitVersion] readonly GitVersion GitVersion;

    AbsolutePath SourceDirectory => RootDirectory / "src";
    AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts";

    Target Clean => _ => _
        .Before(Restore)
        .Executes(() =>
        {
            SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory);
            EnsureCleanDirectory(ArtifactsDirectory);
        });

    Target Restore => _ => _
        .Executes(() =>
        {
            DotNetRestore(s => s
                .SetProjectFile(Solution));
        });

    Target Compile => _ => _
        .DependsOn(Restore)
        .Executes(() =>
        {
            DotNetBuild(s => s
                .SetProjectFile(Solution)
                .SetConfiguration(Configuration)
                .SetAssemblyVersion(GitVersion.AssemblySemVer)
                .SetFileVersion(GitVersion.AssemblySemFileVer)
                .SetInformationalVersion(GitVersion.InformationalVersion)
                .EnableNoRestore());
        });

}
Łączenie ścieżek

Już w tym miejscu warto zwrócić naszą uwagę na bardzo ciekawe zastosowanie operatora / do łączenia ścieżek: AbsolutePath SourceDirectory ⇒ RootDirectory / "src";. W trakcie pracy z Nuke zauważyłem, że jest to bardzo poręczne podejście i gorąco zachęcam Cię do jego używania.

Jak budować projekt z pomocą Nuke

Projekt budujący Nuke możemy uruchomić przynajmniej na trzy sposoby:

Niezależnie od wybranej metody, często, aby zmiany w kodzie budującym zostały zastosowane, niezbędne jest przebudowanie projektu. Samo budowanie, bez czyszczenia, rzadko daje efekty.

Z konsoli

  • dotnet run - Budować możesz poleceniem dotnet run wywołanym w katalogu, gdzie znajduje się nasz projekt budujący (u mnie jest to katalog CICD).

  • Narzędziem nuke - Jeśli zainstalowałeś wcześniej globalne narzędzie nuke, to możesz użyć również go. Wywołaj w konsoli polecenie nuke. Spowoduje ono wywołanie domyślnego celu budowania, czyli kompilację. Podejście to jest bardziej elastyczne, ponieważ zadziała niezależnie od katalogu, w którym je wywołasz. Potrafi ono samo znaleźć katalog główny rozwiązania i tam poszukać odpowiednich plików.

Niezależnie od podejścia, pamiętaj, że przy uruchomieniu możesz podawać własne parametry uruchomieniowe. Możesz spróbować poprzez dodanie flagi --Configuration Release, co spowoduje zbudowanie aplikacji w trybie release. Więcej o definiowaniu własnych parametrów znajdziesz w dalszej części artykułu, w sekcji na temat CI/CD.

Jeśli chcesz wywołać inny cel, wystarczy, że podasz jego nazwę: nuke restore (dotnet run restore).

Plugin do Visual Studio 2022

Plugin do Visual Studio pozwala nam na wywoływanie akcji budowania prosto z IDE. Do tego dochodzi możliwość debugowania. Plugin ściągniesz tutaj.

Po instalacji zobaczysz dodatkową ikonkę obok każdego celu budowania:

vs22 withnuke
Ilustracja 2. Visual Studio 2022 z zainstalowanym wsparciem dla Nuke

Testy jednostkowe

Mając już przygotowane środowisko, możemy dodać testy jednostkowe.

Target Tests => _ => _
        .DependsOn(Compile) (1)
        .TriggeredBy(Compile) (2)
        .Executes(() =>
        {
            EnsureCleanDirectory(TestResultDirectory); (3)
            DotNetTest(new DotNetTestSettings()
                .SetConfiguration(Configuration) (4)
                .EnableNoBuild() (5)
                .SetProjectFile(Solution)); (6)
        });

Powyższy kod w zupełności wystarczy, aby uruchomić testy jednostkowe znajdujące się w całym naszym rozwiązaniu.

1 Najpierw określamy, że testy muszą zostać wykonane po kompilacji.
2 Następnie, że są one wywoływane po zakończeniu kompilacji. Więcej na temat tych dwóch metod przeczytasz w ramkach poniżej.
3 W tym miejscu upewniamy się, że folder wynikowy testów jednostkowych jest pusty. Czasem potrafią znaleźć się tam ciekawe rzeczy, zwłaszcza gdy coś nie działa.
4 W tym miejscu ustawiamy konfigurację, czyli to, w jaki sposób chcemy budować naszą aplikację, czy w trybie debug, czy release. Jak spojrzysz na kod wygenerowany przez konfigurator, zobaczysz właściwość o nazwie Configuration, która dostarcza nam takową informację. Zawsze możesz go nadpisać, używając parametru --Configuration [Debug|Release].
5 Ustawiamy flagę, informującą o tym, że mechanizm testowy ma nie budować ponownie naszych projektów. Zrobiliśmy to w kroku Compile`, więc powinno nam to zaoszczędzić trochę czasu.
6 Określamy projekt, a w tym przypadku całe rozwiązanie, które chcemy przetestować.

Mając dodane te kilka linijek do naszej klasy Build.cs możemy wywołać polecenie nuke Compile. Powinniśmy ostatecznie uzyskać wynik na kształt:

═══════════════════════════════════════
Target             Status      Duration
───────────────────────────────────────
Clean              Succeeded     < 1sec
Restore            Succeeded     < 1sec
Compile            Succeeded       0:02
Tests              Succeeded       0:02
───────────────────────────────────────
Total                              0:15
═══════════════════════════════════════

Build succeeded on 29.05.2022 18:38:46. \(^ᴗ^)/
DependsOn() i TriggeredBy()

DependsOn pozwala nam na określenie, jakie kroki muszą zostać wykonane przed wykonaniem wybranej akcji. Natomiast TriggeredBy powoduje, że krok ten zostanie wywołany przez ten, podany jako argument. W powyższym kodzie, w punkcie <1> i <2> mamy przykład, że testy muszą być wykonane po kompilacji i są też przez nią wywoływane. Dzięki temu nie ważne, czy wykonamy polecenie nuke compile czy nuke tests, zawsze zostaną wykonane testy jednostkowe.

Polecenia te pozwalają nam kształtować łańcuch wywołań bez konieczności zmiany innych elementów wywołujących.

Dodatkowe informacje

Pomoc

W każdym momencie możesz wywołać pomoc przy budowaniu. Można zrobić to na wiele rożnych sposobów:

  • nuke help w dowolnym katalogu rozwiązania, jeśli masz zainstalowane narzędzie Nuke.

  • dotnet run — --help w katalogu projektu budujacego.

  • .\build.ps1 --help w katalogu, gdzie znajduje się skrypt budujący.

Przykładowy rezultat takiego polecenia jest widoczny ponizej. Zwróć uwagę na to, że widoczne są wszystkie wcześniej określone cele budowania oraz parametry wraz z opisem. Daje nam to bardzo fajną odkrywalność naszego procesu budującego.

███╗   ██╗██╗   ██╗██╗  ██╗███████╗
████╗  ██║██║   ██║██║ ██╔╝██╔════╝
██╔██╗ ██║██║   ██║█████╔╝ █████╗  
██║╚██╗██║██║   ██║██╔═██╗ ██╔══╝  
██║ ╚████║╚██████╔╝██║  ██╗███████╗
╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝

NUKE Execution Engine version 6.0.3 (Windows,.NETCoreApp,Version=v6.0)

Targets (with their direct dependencies):

  Clean
  Restore
  Compile (default)    -> Clean, Restore
  Tests                -> Compile
  Publish              -> Compile
  PushToNetlify        -> Publish
  TestCoverage         -> Tests

Parameters:

  --configuration            Configuration to build - Default is 'Debug' (local) or
                             'Release' (server).
  --netlify-site-access-token   <no description>
  --netlify-site-id          <no description>

  --continue                 Indicates to continue a previously failed build attempt.
  --help                     Shows the help text for this build assembly.
  --host                     Host for execution. Default is 'automatic'.
  --no-logo                  Disables displaying the NUKE logo.
  --plan                     Shows the execution plan (HTML).
  --profile                  Defines the profiles to load.
  --root                     Root directory during build execution.
  --skip                     List of targets to be skipped. Empty list skips all
                             dependencies.
  --target                   List of targets to be invoked. Default is 'Compile'.
  --verbosity                Logging verbosity during build execution. Default is
                             'Normal'.

Jaka jest kolejność?

Gdy już ilość celów budowania będzie duża, a zależności między nimi będzie co niemiara, warto pamiętać o narzędziu, które w przejrzysty sposób wyświetli nam, co będzie się działo. Do tego służy flaga plan, która używamy w następujący sposób: nuke --plan, lub, jeśli chcemy zobaczyć plan dla niestandardowego wywołania, to możemy podać dodatkowe parametry, jak na przykład nazwę celu budowania: nuke PushToNetlify --plan. Pamiętaj, że podobnie jak polecenie help, również to można wywołać na analogiczne sposoby.

Wynik działania polecenie nuke --plan

nuke plan

Podsumowanie

W następnej części zamierzam pokazać Ci jak wymusić odpowiednie pokrycie kodu testami jednostkowymi oraz jak przygotować aplikację do publikacji. Opiszę również sposób przygotowania CI/CD dla Github Actions z uwzględnieniem parametrów pobierania sekretów repozytorium. Jeśli masz pomysły, co mógłbym jeszcze opisać w sprawie Nuke, daj znać w komentarzu!

Końcowy kod z opisanymi tutaj elementami, i kilkoma więcej, znajdziesz na moim GitHubie: Ztr.AI.

Photo by Burgess Milner on Unsplash.

comments powered by Disqus
Proszę pamiętaj, że blog jest na ten moment w wersji poglądowej i może zawierać wiele błędów (ale nie merytoycznych!). Mimo to mam nadzieję, że blog Ci się podoba! Wiele ilustracji pojawiło się na blogu dzięki unsplash.com!
Zbudowano z Hugo
Motyw Stack zaprojektowany przez Jimmy