Wie man mit Gitlab CI/CD und NSIS eine .Net WPF Anwendung erstellt und veröffentlicht

Wie man mit Gitlab CI/CD und NSIS eine .Net WPF Anwendung erstellt und veröffentlicht
Bildrechte: Marco Griep (CC BY-NC-ND) - Geschrieben von: Marco Griep

How to create and publish a .Net WPF application with Gitlab CI/CD and NSIS

In der Regel habe ich, seitdem ich mich mit CI/CD beschäftige ausschließlich Web-Anwendungen und Websites welche auf einem Linux Server laufen müssen. Dies habe ich immer simpel gehalten. Im ersten Schritt wurde die Anwendung gebaut, danach in ein Docker-Image gepackt und auf dem Zielserver vom Repository mit dem latest Tag gepullt. Aktuell arbeite ich jedoch an der Windows UI für meine 360°Image Viewer Software welche ausschließlich für Windows Geräte zur Verfügung stehen wird. In meiner Solution gibt es 2 Projekte, das eine für das Tray Icon, das andere für die eigentliche GUI, dazu noch ein NSIS Script um den Installer zu bauen. Den Build Prozess und Installer-Bau möchte ich nicht immer manuell machen müssen und brauchte dazu eine Pipeline. Wie ich diese Umgesetzt habe versuche ich in diesem Artikel verständlich zu erklären.

Folgendes haben wir vor

Wir werden in diesem Beitrag eine CI/CD Pipeline mit Gitlab bauen. Diese Pipeline soll uns unser Projekt builden, in ein “Publish” Verzeichnis mit all den notwendigen Dateien zwischenspeichern und dann mit NSIS einen aktuellen Installer bauen. Dieser Installer wird dann mit SSH auf einen Server hochgeladen damit das Setup z. B. auch automatisch auf einer Website verlinkt werden kann. Die Pipeline wird wie folgt aussehen:

CI/CD Pipeline

Voraussetzungen

Die Docker-Images für .Net bringen uns in GitLab leider nicht viel da GitLabs Docker-Host wohl keine Windows Container Images bereitstellen kann. Wir benötigen also einen Build Server der auf Windows läuft. Ich habe hierzu einen alten Fujitsu Esprimo Q920 genommen und ein Windows Server 2019 Image darauf installiert. Für mich reicht erstmal die 180 Tage Testversion. Zukünftig werde ich wohl hier aus Kostengründen eher ein Windows 10 Home einsetzen. Das reicht vollkommen aus für das, was wir tun möchten.

Den GitLab Runner installieren und registrieren

Damit die GitLab Pipeline angewendet wird muss mindestens ein GitLab Runner im Projekt registriert sein. Sofern Ihr eine Pipeline für Linux braucht, reicht in der regel die Shared Runner. Da GitLab uns jedoch keinen Shared Runner für Windows bringt, installieren wir uns selbst einen. Das kann entweder auf einer VM erfolgen oder an dem Rechner an dem Sie gerade Sitzen oder auf einem Build Server. Wir benötigen lediglich die MSBuild Tools sowie Nuget zum builden der Solution. Laden Sie sich von GitLab den aktuellen GitLab Runner für Windows herunter. Entpacken Sie das Zip-Archiv und legen Sie die .Exe Datei von Gitlab-Runner in folgenden Pfad ab: “C:\Gitlab-Runner\gitlab-runner.exe” (Ich habe den Namen der EXE-Datei angepasst).

Der obige Link beinhaltet auch die Installationsanleitung, falls Sie lieber der offiziellen GitLab Dokumentation folgen möchten. Den GitLab-Runner müssen Sie nach dem Ablegen im richtigen Verzeichnis noch installieren. Führen Sie dazu folgende Befehle mit erhöhten Rechten aus.

cd C:\GitLab-Runner
gitlab-runner.exe install
gitlab-runner.exe start

Anschließend müssen wir den GitLab-Runner noch registrieren. Der Runner möchte eine URL von Ihnen haben sowie auch einen Secret Key. Diese Informationen finden Sie in Ihrem GitLab Repository unter: Einstellungen -> CI/CD -> Runners -> Specific Runners.

GitLab Runner Settings

Kopieren Sie sich den Token und geben Sie folgenden Befehl in Ihrer Kommandozeile ein um Ihren GitLab Runner zu registrieren:

gitlab-runner.exe register

Geben Sie hier die Informationen an die Sie aus Ihrem GitLab Repository haben. Der Runner sollte anschließend unter Specific Runners erscheinen. Deaktivieren Sie die Shared Runners damit GitLab nicht versucht den Build Prozess mit diesen auszuführen.

Info: Bei mir war es nötig einen Tag einzugeben auf den der GitLab Runner reagieren soll. Der GitLab Runner sollte bei mir immer verwendet werden unabhängig vom Tag. Ich habe einfach als Tag “test” genommen und im GitLab unter den Runner Einstellungen den Haken bei “Gibt an, ob dieser Runner Jobs ohne Tags auswählen kann” gesetzt. Ihr GitLab Runner sollte nun Einsatzbereit sein.

Unbekannte Zertifikate erlauben (optional)

Ich hatte das Problem das mein GitLab Runner die Pipeline Artefakte nicht herunterladen konnte, weil er das Zertifikat nicht kannte. Dies hatte ich zwar manuell heruntergeladen und installiert, wurde jedoch trotzdem geblockt. Sollte bei Ihnen das auch der Fall sein, können Sie im GitLab unter “Einstellungen -> CI/CD -> Variablen” folgende Variable anlegen:

Schlüssel: GIT_SSL_NO_VERIFY

Wert: true

Git SSL Issue

NuGet Installieren

Damit in der Pipeline die Abhängigkeiten heruntergeladen werden können, benötigen wir NuGet auf unserem System installiert. Ich habe aktuelle NuGet Version (5.9.1) von der offiziellen Website heruntergeladen und hier abgelegt: “C:\Tools\NuGet\NuGet.exe”. Die Pfad-Variable habe ich nicht angepasst da ich in meiner Pipeline direkt auf den absoluten Pfad adressiere.

MSBuild Tools installieren

Zum Erstellen der Release Dateien für Ihr Projekt benötigen Sie MSBuild. Sofern Sie Visual Studio installieren haben, ist dies bei Ihnen bereits installiert. Ansonsten müssten Sie entweder die zu Ihrem Projekt passende Visual Studio Version oder MSBuild herunterladen und installieren. In meinem Fall war dies Visual Studio 2019 Community. MSBuild ist dann unter folgendem Pfad installiert: “C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe”. Diesen Pfad müssen wir in unserer Pipeline auch angeben. Passen Sie den Pfad gegebenfalls an.

Aufbau meines Repositories

Mein Repository habe ich so aufgebaut, das alle Dateien welche zur Windows App gehören im gleichen Repository sind. Das Serverfrontend und Backend sind in einem anderen Repositories und werden hier nicht weiter beschrieben. Zu meiner Windows Applikation gehören folgende Komponenten:

  • Tray Icon Projekt
  • WPF Projekt
  • ElectronJS Tool für 360 Grad Bilder Preview
  • NSIS Installer
  • Lizenzdatei

Je nach Projektaufbau müssen Sie natürlich Ihre GitLab CI/CD Datei anpassen. Einige Punkte von meiner Anwendung werden Sie nicht benötigen. z. B. Gibt es das ElectronJS Tool das in dieser Pipeline keinem Build durchläuft, sondern einfach alle Bestandteile (im Verzeichnis AdditionalRessources) mit in den Installer gepackt werden, diesen Punkt brauchen Sie natürlich nicht zu übernehmen. Auch die Lizenzdatei wird nur für den Installer benötigt. Der Build vom Tray Projekt ist identisch mit dem vom WPF Projekt. Sofern Ihre Windows Applikation nur aus einem Projektbesteht, können Sie einfach die nicht benötigten Pipeline Schritte löschen.

Repository Struktur

Alle C# Projektdateien befinden sich unter “360Tray”.

  • “360Tray\360Tray”: Die WPF GUI
  • “360Tray\Tray”: Das Tray Menü

CI / CD Yaml Datei

Nun möchten wir uns unsere Pipeline bauen. Zuerst definieren wir uns Variablen für den Pfad zu unserem NuGet und MSBuild.

variables:
  NUGET_PATH: 'C:\Tools\Nuget\nuget.exe'
  MSBUILD_PATH: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe'

Dann soll es drei Stages geben:

stages:
  - build
  - installer
  - backup

Den letzten Stage könnte man auch “deploy” nehmen. Im Build Stage lassen wir unser(e) C# Projektdateien builden sodass wir im Release Ordner die ausführbaren Programmdateien haben. Im zweiten Stage “Installer” werden alle für unsere Anwendung benötigten Dateien in ein temporäres Verzeichnis “Publish” kopiert und unser NSIS Script wird die Setup Datei bauen. Im letzten Stage “Backup” wird unsere Anwendung per SSH auf einen Server kopiert. Einmal als “Setup.exe” und einmal mit der Commit-ID im Dateinamen. Die Setup.exe stellt dabei immer die neuste Version von unserem Installer dar. Die Setup Dateien mit dem Commit ID im Dateinamen dienen zur Historisierung damit wir auch ältere Versionen veröffentlichen können. Alternativ könnte man auch eine Pipeline starten, sobald das Projekt getaggt wird. Den Tag könnte man dann als Versionsnamen verwenden.

1. Build Stage

In diesem Stage werden die Projektabhängigkeiten mit NuGet wiederhergestellt und das Projekt mit MSBuild kompiliert.

build_ui:
  stage: build
  only:
    - branches
  script:
    - cd 360Tray
    - '& "$env:NUGET_PATH" restore'
    - '& "$env:MSBUILD_PATH" /p:Configuration=Release /clp:ErrorsOnly'
    - '& "$env:MSBUILD_PATH" 360Tray\360Tray.csproj /p:DeployOnBuild=true /p:Configuration=Release /P:PublishProfile=FolderProfile.pubxml'
  artifacts:
    expire_in: 2 days
    paths:
      - '.\360Tray\360Tray\bin\Release\'

Hier müsst Ihr Eure Pfade anpassen. Wie Ihr sehen könnt, welche ich im Script Bereich als Erstes in den Unterverzeichnis “360Tray”. Dort liegt meine Solution Datei. NuGet lädt hier alle Abhängigkeiten herunter, welche für die Projekte in der Solution notwendig sind. Anschließend wird MSBuild Befehl mit dem Parameter “Release” ausgerufen. Die erstellen Release Dateien werden als Artefakt in der Pipeline hinterlegt und 2 Tage aufbewahrt.

Das Gleiche mache ich für das Tray Projekt auch nochmal, hier ändern sich nur die Pfade. Der Vollständigkeit halbe zeige ich den YAML Code hier auch auf:

build_tray:
  stage: build
  only:
    - branches
  script:
    - cd 360Tray
    - '& "$env:NUGET_PATH" restore'
    - '& "$env:MSBUILD_PATH" /p:Configuration=Release /clp:ErrorsOnly'
    - '& "$env:MSBUILD_PATH" Tray\Tray.csproj /p:DeployOnBuild=true /p:Configuration=Release /P:PublishProfile=FolderProfile.pubxml'
  artifacts:
    expire_in: 2 days
    paths:
      - '.\360Tray\Tray\bin\Release\'

Die beiden Build Prozesse laufen in der Pipeline dann parallel.

2. Installer Stage erstellen

Im nächsten Schritt bereite ich ein Verzeichnis unter C:\temp\publish vor. In diesem Verzeichnis landen all die Release-Dateien. Des Weiteren kopiere ich die Electron App (welche sich unter AdditionalRessources) und die Lizenz.txt Datei mit in publish Verzeichnis. Nun muss nur noch mit makensis das Install Script aufgerufen werden und der Installer wird erzeugt.

create_installer:
  stage: installer
  only:
    - master
  script:
    - if (Test-Path C:\temp\publish) { Remove-Item -Recurse -Force C:\temp\publish }
    - New-Item -ItemType directory -Path C:\temp\publish
    - Get-ChildItem .\360Tray\Tray\bin\Release | Copy-Item -Destination C:\temp\publish -Recurse -filter *.* -Force
    - Get-ChildItem .\AdditionalRessources | Copy-Item -Destination C:\temp\publish -Recurse -filter *.* -Force
    - Get-ChildItem .\360Tray\360Tray\bin\Release | Copy-Item -Destination C:\temp\publish -Recurse -filter *.* -Force
    - copy-item "installer.nsi" "C:\temp\publish" -Force
    - copy-item "license.txt" "C:\temp\publish" -Force
    - '& "C:\Program Files (x86)\NSIS\makensis.exe" C:\temp\publish\installer.nsi'
    - copy-item "C:\temp\publish\Setup.exe" ".\Setup.exe" -Force
  artifacts:
    expire_in: 2 days
    paths:
      - '.\Setup.exe'
  dependencies:
    - build_tray
    - build_ui

Unter Dependencies habe ich definiert das dieser Schritt nur ausgeführt werden soll, sofern die beiden vorherigen Build Schritte auch erfolgreich waren. Wieder wird das Setup als Artefakt in der Pipeline 2 Tage lang gespeichert. In diesem Fall ist das hinterlegen als Artefakt nicht notwendig. Jedoch komme ich dann die nächsten 2 Tage immer an die aktuellste Setup Datei und muss nicht über SSH auf den Server.

Das NSIS Script

Falls Sie NSIS nicht kennen, NSIS ist eine Skriptsprache mit der Sie einen Installer inkl. Logik erzeugen können. Ein NSIS Script können Sie mit HM-NSIS einfach selbst erstellen. Letztendlich werden hier nur alle für den Installer erforderlichen Dateien verlinkt. Aus Platzgründen werde ich das Installer-Script hier nicht reinkopieren. Sollten Sie das Script sehen wollen, schreiben Sie mir einfach eine Nachricht.

Hinweis: Im bevor das NSIS Script kompiliert wird, könnte man noch die Version automatisch mit String-Replacement austauschen lassen. Beispielhaft durch GitLab Tag Namen. Somit würde das Setup auch immer eine neue Versionsnummer erhalten.

3. Backup Stage (Deploy)

Im letzten Schritt wollen wir die erzeugte Setup Datei auf unseren Server hochladen damit z. B. Website-Besucher die aktuelle Version auch herunterladen können. Da wir uns ja auf einem Windows System befinden fehlt uns rsync oder ein einfacher Befehl um eine Datei von A nach B mit SSH zu kopieren. Ich verwende hier das Putty Tool “pscp”. Dieses Tool können Sie einfach nutzen, wenn Sie Putty auf Ihrem System installieren.

backup_release:
  stage: backup
  only:
    - master
  script:
    - '& echo y | pscp -P 22 -pw "$SSH_PW" C:\temp\publish\Setup.exe admin@192.168.112.200:/share/Public/Archive/Setup.exe.$CI_COMMIT_SHA'
    - '& echo y | pscp -P 22 -pw "$SSH_PW" C:\temp\publish\Setup.exe admin@192.168.112.200:/share/Public/Archive/Setup.exe'
  dependencies:
    - create_installer

Die Variable "$SSH_PW" habe ich zuvor unter den GitLab Einstellungen -> Variablen definiert.

Die komplette .gitlab-ci.yaml Datei

variables:
  NUGET_PATH: 'C:\Tools\Nuget\nuget.exe'
  MSBUILD_PATH: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe'

stages:
  - build
  - installer
  - backup

build_ui:
  stage: build
  only:
    - branches
  script:
    - cd 360Tray
    - '& "$env:NUGET_PATH" restore'
    - '& "$env:MSBUILD_PATH" /p:Configuration=Release /clp:ErrorsOnly'
    - '& "$env:MSBUILD_PATH" 360Tray\360Tray.csproj /p:DeployOnBuild=true /p:Configuration=Release /P:PublishProfile=FolderProfile.pubxml'
  artifacts:
    expire_in: 2 days
    paths:
      - '.\360Tray\360Tray\bin\Release\'

build_tray:
  stage: build
  only:
    - branches
  script:
    - cd 360Tray
    - '& "$env:NUGET_PATH" restore'
    - '& "$env:MSBUILD_PATH" /p:Configuration=Release /clp:ErrorsOnly'
    - '& "$env:MSBUILD_PATH" Tray\Tray.csproj /p:DeployOnBuild=true /p:Configuration=Release /P:PublishProfile=FolderProfile.pubxml'
  artifacts:
    expire_in: 2 days
    paths:
      - '.\360Tray\Tray\bin\Release\'

create_installer:
  stage: installer
  only:
    - master
  script:
    - if (Test-Path C:\temp\publish) { Remove-Item -Recurse -Force C:\temp\publish }
    - New-Item -ItemType directory -Path C:\temp\publish
    - Get-ChildItem .\360Tray\Tray\bin\Release | Copy-Item -Destination C:\temp\publish -Recurse -filter *.* -Force
    - Get-ChildItem .\AdditionalRessources | Copy-Item -Destination C:\temp\publish -Recurse -filter *.* -Force
    - Get-ChildItem .\360Tray\360Tray\bin\Release | Copy-Item -Destination C:\temp\publish -Recurse -filter *.* -Force
    - copy-item "installer.nsi" "C:\temp\publish" -Force
    - copy-item "license.txt" "C:\temp\publish" -Force
    - '& "C:\Program Files (x86)\NSIS\makensis.exe" C:\temp\publish\installer.nsi'
    - copy-item "C:\temp\publish\Setup.exe" ".\Setup.exe" -Force
  artifacts:
    expire_in: 2 days
    paths:
      - '.\Setup.exe'
  dependencies:
    - build_tray
    - build_ui

backup_release:
  stage: backup
  only:
    - master
  script:
    - '& echo y | pscp -P 22 -pw "$SSH_PW" C:\temp\publish\Setup.exe admin@192.168.112.200:/share/Public/Archive/Setup.exe.$CI_COMMIT_SHA'
    - '& echo y | pscp -P 22 -pw "$SSH_PW" C:\temp\publish\Setup.exe admin@192.168.112.200:/share/Public/Archive/Setup.exe'
  dependencies:
    - create_installer

Schlusswort

Ich hoffe das ich Ihnen etwas helfen konnte und dass es Ihnen gelingt Ihr .Net Framework Projekt automatisiert builden zu lassen. Die Pipeline ist noch nicht perfekt. Wie sie vielleicht bemerkt haben fehlt der Schritt mit dem “Test”. Mittels XUnit könnte man hier noch eine Stage einbauen welche Unit Tests durchläuft und nur bei Erfolg auch den Build durchführt. Ebenso könnte man vor dem kompilieren der Setup Datei die Versionsnummer mit der Tag-Nummer austauschen, sodass auch automatisch die Version hochgezählt wird. Falls Sie dies benötigen können Sie das Script gerne optimieren.

Wenn ich Ihnen helfen konnte und Sie eventuell durch mein Tutorial Zeit sparen konnten, würde ich mich freuen, wenn Sie diesen Beitrag auf Social Media teilen würden. Social Media Shares helfen mir im SEO Ranking weiter.