CI/CD für WinForms-/WPF-Anwendungen in der Praxis ── Vom Build über die Signierung bis zur Distribution mit GitHub Actions automatisieren

· · CI/CD, GitHub Actions, WinForms, WPF, C#, .NET, Code-Signierung, MSIX, Distribution, Windows-Entwicklung, Entscheidungstabelle

„Das Release lässt sich nur auf dem Rechner dieser einen Person bauen.“ Das hören wir in Beratungen zu WinForms- und WPF-Business-Anwendungen ständig. Jemand macht in seinem lokalen Visual Studio einen Release-Build, packt ihn in ein Zip und legt ihn in einen gemeinsamen Ordner. Es funktioniert, aber niemand kann sagen, ob an dem Tag, an dem dieser Entwickler zufällig nicht da ist, ein Bugfix-Release überhaupt ausgeliefert werden kann.

Informationen zu CI/CD für Web-Anwendungen gibt es überall, doch sobald man bei Desktop-Anwendungen ankommt, wird es plötzlich dünn. Ein Teil davon ist tatsächlich gerechtfertigt – „das Ziel der Bereitstellung ist der PC eines Kunden, kein Server“ ist ein grundlegender Unterschied, der bedeutet, dass man einen Web-Artikel nicht einfach wortwörtlich übernehmen kann. Dennoch gilt: Die Automatisierung von Build- und Teststufen ist bei einer Desktop-Anwendung fast ebenso wenig Aufwand wie im Web. Die Mauer taucht weiter hinten auf, bei Signierung und Distribution, wo die pragmatische Antwort je nach Distributionsformat unterschiedlich ausfällt.

In diesem Blog haben wir bereits behandelt, wie man ein Distributionsformat auswählt, in „Eine Entscheidungstabelle für Windows-App-Distributionsmethoden“, und wie man über Signierung nachdenkt, in „SmartScreen und Code-Signierung“. Aufbauend auf diesen beiden arbeitet dieser Artikel aus praktischer Perspektive auf, wie weit sich Build, Test, Versionsnummerierung, Signierung und die Erzeugung von Distributionsdateien einer WinForms-/WPF-Anwendung mit GitHub Actions automatisieren lassen.

1. Das Wichtigste zuerst

  • Das größte Risiko ist ein Zustand, in dem „sich das nur auf dem PC eines Entwicklers bauen lässt“. Das erste Ziel von CI/CD ist nicht die vollständige Automatisierung der Distribution – es ist, den Build reproduzierbar zu machen, ohne von der Maschine einer bestimmten Person abzuhängen.
  • Eine minimale Einrichtung – nur die Automatisierung von Build+Test – ist bereits für sich genommen viel wert. Sie lässt sich in einer einzigen YAML-Datei zusammenstellen: windows-latest + actions/checkout + actions/setup-dotnet + dotnet build / test + actions/upload-artifact.12
  • WinForms / WPF setzen einen Windows-Runner voraus. Da sie auf ein reines Windows-TFM wie net8.0-windows abzielen,3 erfordert das Ausführen von Tests in CI eine Windows-Umgebung.
  • Tag-gesteuerte Versionierung ist die praktische Antwort für die Versionsnummerierung. Pushen Sie ein Tag wie v1.2.3, um den Release-Build auszulösen, und speisen Sie den Wert des Tags in die MSBuild-Eigenschaft Version ein.4
  • Die Signierung ist die größte Mauer für die Automatisierung. Seit Juni 2023 muss der private Schlüssel eines öffentlichen OV-Zertifikats auf einem HSM liegen, sodass sich der alte Standardweg „PFX in ein Secret legen und signtool ausführen“ nicht mehr unverändert nutzen lässt. Der Weg über einen Cloud-Signaturdienst ist der realistische Pfad, wenn CI das durchgängig übernehmen soll.5
  • Wie leicht sich ein Distributionsformat in CI einbinden lässt, variiert stark. Der Reihe nach: xcopy (zip) ist am einfachsten, MSIX erfordert eine Signierung,6 MSI funktioniert über ein CLI-gesteuertes Werkzeug, und ClickOnce benötigt msbuild /target:publish und hat einige Eigenheiten.7
  • Machen Sie automatisierte UI-Tests nicht zu einem verpflichtenden CI-Gate. Unit-Tests als verpflichtendes Gate, mit UI-Tests, die auf Smoke-Tests in einem separaten Job beschränkt bleiben, ist die praktische Antwort.

2. Wie sich CI/CD für Desktop-Anwendungen vom Web unterscheidet

Beginnen wir damit, aufzuarbeiten, warum sich das CI/CD-Muster von Web-Anwendungen (Push -> Build -> Test -> Deploy auf einen Server) nicht einfach vollständig übernehmen lässt.

Aspekt Web-Anwendung WinForms-/WPF-Desktop-Anwendung
Bereitstellungsziel Ein selbst verwalteter Server PCs bei Kunden/vor Ort (außerhalb der eigenen Kontrolle)
Distributionseinheit Ein pauschaler Umschalt auf dem Server MSI / MSIX / ClickOnce / zip und mehr – der Rollout-Zeitpunkt hängt von der Gegenseite ab
Rollback Lässt sich auf dem Server rückgängig machen Lässt sich auf PCs, die es bereits erhalten haben, nicht einfach rückgängig machen. Ältere Installer müssen aufbewahrt werden
Signierung In der Regel nicht nötig (TLS wird auf Infrastrukturebene gehandhabt) Code-Signierung auf der ausführbaren Datei/dem Paket ist faktisch zwingend
Build-Umgebung Funktioniert oft problemlos auf einem Linux-Runner Setzt einen Windows-Runner voraus
Testen Funktioniert oft problemlos headless Unit-Tests sind gleich; UI-Tests benötigen eine Desktop-Sitzung
Was „Deploy“ bedeutet Erreichen der Produktion Die Aufgabe von CI endet bei „die Distributionsdatei ist fertig“. Die Installation ist ein separater Schritt

Die letzte Zeile ist die wichtige. Bei einer Desktop-Anwendung ist der Ausstiegspunkt der CI/CD-Pipeline nicht „die Produktion erreichen“ – es ist „eine signierte Distributionsdatei liegt an einem Ort, von dem sie stets abrufbar ist“. Alles danach (das Ausrollen an Kunden, automatische Aktualisierung) ist eine Frage des Distributionsdesigns, dem Terrain, das in „Eine Entscheidungstabelle für Distributionsmethoden“ abgedeckt wird. Andersherum betrachtet: Sobald Sie diese Grenze am Ausstiegspunkt ziehen, lässt sich CI/CD für Desktop-Anwendungen mit genau demselben Werkzeugsatz aufbauen wie im Web.

3. Die minimale Einrichtung — Build+Test mit GitHub Actions

Hier das Erste und zunächst Einzige, was Sie einrichten sollten. Da GitHub-gehostete Runner jedem Job eine frische VM zuweisen,1 läuft bei jedem Push Build und Test auf einer unberührten Windows-Maschine, und jede Abhängigkeit von „einem SDK, das nur auf dem PC dieser einen Person installiert ist“ tritt sofort zutage.

name: build-and-test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: windows-latest   # WinForms / WPF benötigen einen Windows-Runner
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Test
        run: dotnet test --configuration Release --no-build

      - name: Publish
        run: dotnet publish src/MyApp/MyApp.csproj -c Release -o publish

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp
          path: publish

Drei Punkte sind hervorzuheben.

Erstens ist runs-on: windows-latest die Grundlage. Ein WinForms-/WPF-Projekt hat ein TargetFramework, das ein reines Windows-TFM wie net8.0-windows ist, und ist ein .NET-Desktop-SDK-Projekt mit aktiviertem UseWindowsForms oder UseWPF.3 Streng genommen könnten Sie, wenn Sie nur kompilieren müssten, EnableWindowsTargeting aktivieren und auch auf einem Linux-Runner bauen – doch ein Schritt mit tatsächlicher Ausführung wie dotnet test erfordert eine Windows-Umgebung, sodass für eine Einrichtung wie diese, die Tests im selben Job ausführt, ein Windows-Runner die naheliegende Wahl ist. Bei .NET Framework 4.x (dem alten csproj-Stil) würden Sie statt dotnet build MSBuild und die NuGet-CLI verwenden, doch beide sind auf dem Windows-Runner bereits vorinstalliert, und der zugrunde liegende Ansatz ist derselbe.

Zweitens: Bewahren Sie die Ausgabe stets mit actions/upload-artifact auf. „Der vollständige Satz an Build-Ausgaben für diesen Build lässt sich von GitHub abrufen“ – so sieht es in der Praxis tatsächlich aus, wenn man die Abhängigkeit von der Maschine einer bestimmten Person durchbricht. Selbst ein schneller Prüf-Build wird so zu einer Sache, bei der man lediglich ein Zip vom Actions-Bildschirm herunterlädt.

Drittens: An diesem Punkt betreiben Sie noch keine Signierung oder Distribution. Diese minimale Einrichtung allein verschafft Ihnen zwei Garantien – „main lässt sich stets bauen und testen“ und „jeder kann dieselbe Build-Ausgabe abrufen“ – und meiner Erfahrung nach löst das den Großteil dessen, womit ein kleines Team zu kämpfen hat.

4. Automatische Versionsnummerierung — tag-gesteuerte Releases

Die nächste Stufe löst das Problem „welche Version ist dieses Zip eigentlich genau?“. In einem auf lokalen Builds aufgebauten Workflow ist es ein klassisches Missgeschick, das Erhöhen von Version im csproj zu vergessen, sodass über viele Generationen hinweg dieselbe 1.0.0 stehen bleibt. Die praktische Antwort sind tag-gesteuerte Releases: Markieren Sie den Commit, den Sie veröffentlichen möchten, mit etwas wie v1.2.3, lassen Sie das einen Workflow auslösen und speisen Sie die aus dem Tag-Namen entnommene Version in den Build ein.

name: release

on:
  push:
    tags: [ 'v*' ]

jobs:
  release:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      # Ein Tag-Push löst den Build+Test-Workflow aus Kapitel 3 nicht aus,
      # daher hier vor der Erzeugung des Release-Artefakts erneut die Tests ausführen
      - name: Test
        run: dotnet test --configuration Release

      - name: Publish with version from tag
        shell: pwsh
        run: |
          $version = $env:GITHUB_REF_NAME.TrimStart('v')   # v1.2.3 -> 1.2.3
          dotnet publish src/MyApp/MyApp.csproj `
            -c Release -o publish `
            -p:Version=$version

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp-${{ github.ref_name }}
          path: publish

Übergeben Sie es als MSBuild-Eigenschaft wie -p:Version=1.2.3, werden in einem .NET-SDK-Projekt AssemblyVersion und FileVersion standardmäßig aus dem Präfix von Version erzeugt (dem Teil ohne ein etwaiges Suffix) und InformationalVersion aus Version selbst.4 Belassen Sie im csproj nur einen Platzhalter-Entwicklungswert, wird die offizielle Version des Releases zu etwas, das ausschließlich, an einer einzigen Stelle, im Tag lebt.

Ein Vorbehalt bleibt. Artefakte von actions/upload-artifact haben eine repositoryweite Aufbewahrungsfrist (standardmäßig 90 Tage) und werden nach deren Ablauf gelöscht. Da eine Desktop-Anwendung alte Installer langfristig für Rollback-Zwecke aufbewahren muss, sollten Sie Tag-Build-Artefakte an einem dauerhaften Ort veröffentlichen, etwa indem Sie sie an ein GitHub-Release anhängen, und das Actions-Artefakt als rein temporäre Übergabe behandeln.

Der Vorteil liegt darin, dass der Betrieb vollständig innerhalb von Git verbleibt. „Welcher Commit ist 1.2.3 in der Umgebung des Kunden?“ wird durch das Tag festgelegt, und die im Eigenschaftenfenster der EXE angezeigte Dateiversion stimmt mechanisch mit dem Git-Tag überein. Zusätzlich wird ab dem .NET-8-SDK standardmäßig der Git-Commit-Hash (SourceRevisionId) an InformationalVersion angehängt,4 sodass Sie, wenn Sie dies auf einem Versionsanzeigebildschirm sichtbar machen, den exakten Commit direkt aus einem Build-Artefakt bestimmen können.

5. Code-Signierung in CI einbinden — dies ist die größte Mauer

Teams, die Build und Versionierung reibungslos automatisiert haben, kommen fast immer bei der Signierung ins Stocken. Warum Code-Signierung für eine außerhalb des Stores verteilte Windows-Anwendung faktisch zwingend ist (SmartScreen, Sicherheitsprodukte in Unternehmen, Manipulationserkennung), haben wir bereits in „SmartScreen und Code-Signierung“ behandelt, weshalb wir uns hier eng darauf konzentrieren, wo in CI sie ausgeführt wird und wie.

5.1. Die Grundform von signtool

Die eigentliche Ausführung der Signierung ist nur ein einziger Befehl. signtool wird als Teil des Windows SDK ausgeliefert und ist auch auf den Windows-Runnern von GitHub verfügbar. Aktuelle SDK-Versionen erfordern die Angabe sowohl von /fd (File Digest) als auch von /td (Timestamp Digest), wobei SHA256 empfohlen wird.8

signtool sign /f MyCert.pfx /p $env:PFX_PASSWORD `
  /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
  publish\MyApp.exe

Der Zeitstempel (/tr) ist technisch gesehen optional, sollte aber stets angegeben werden. Mit einem Zeitstempel lässt sich auch nach Ablauf des Zertifikats nachweisen, dass „dies zum Zeitpunkt der Signierung gültig war“, wodurch die Signatur auf bereits ausgelieferten Dateien gültig bleibt.86

5.2. Zertifikatstypen und die Realität ihrer Einbindung in CI

Das Problem ist nicht der Befehl – es ist, wo der private Schlüssel liegt. Wie Sie an Ihr Zertifikat gelangen, verändert grundlegend, wie Sie es in CI einbinden können.

Zertifikatsform Ort des privaten Schlüssels Einbindung in CI Anmerkungen
Cloud-Signaturdienst (Azure Artifact Signing = früher Trusted Signing usw.) Cloud-seitig Leicht einzubinden (die Hauptoption). Von Anfang an mit Blick auf eine Integration in GitHub Actions und Ähnliches konzipiert Beschränkt auf bestimmte verfügbare Länder/Regionen (für Unternehmen z. B. USA, Kanada, EU, UK)5
OV-Zertifikat (neu ausgestellt seit Juni 2023) HSM/USB-Token erforderlich So nicht möglich, da sich ein Token nicht in einen Runner stecken lässt. Möglich über die Cloud-HSM-Option einer Zertifizierungsstelle Nach Anforderungen des CA/Browser Forum5
EV-Zertifikat HSM/USB-Token Wie oben Der sofortige Vertrauensvorteil für SmartScreen wurde 2024 abgeschafft – für den Signierbetrieb wie OV behandeln5
Klassische PFX-Datei (in der Vergangenheit ausgestellt, eine interne CA oder selbstsigniert) Eine Datei Base64-kodiert in einem Secret ablegen und wiederherstellen (unten) Diese Form lässt sich für ein neu ausgestelltes Zertifikat zur öffentlichen Distribution in der Regel nicht mehr beschaffen

Mit anderen Worten: Die per Suche häufig gefundene Einrichtung „PFX in ein GitHub-Secret legen und mit signtool signieren“ gilt weiterhin für eine interne CA oder ein bestehendes PFX, doch für alle, die künftig ein öffentliches Zertifikat beschaffen, entfällt diese Voraussetzung. Wenn Sie das neu einrichten, ist es realistisch, von Anfang an einen Cloud-Signaturdienst mit CI-Integration einzuplanen.5 Betreiben Sie es mit einem USB-Token, landen Sie bei einer Hybrideinrichtung, bei der nur der Signierschritt auf einer lokalen PC oder einem selbst gehosteten Runner mit angestecktem Token verbleibt.

5.3. Vorbehalte zur Secret-Verwaltung

Hier die Standardpraktiken, um den PFX-Ansatz (interne CA oder bestehendes Zertifikat) in CI einzubinden.

  • Legen Sie das PFX als Base64-String in einem GitHub-Secret ab und stellen Sie es innerhalb des Jobs als Datei wieder her. Das ist das von GitHubs eigener Dokumentation empfohlene Verfahren zur Handhabung von Binärdaten als Secret.9
  • Bewahren Sie das Passwort in einem separaten Secret auf. Secret-Werte werden in Protokollen automatisch maskiert,9 doch diese Maskierung erstreckt sich nicht auf abgeleitete, von Ihnen transformierte Werte. Vermeiden Sie es, sie als Umgebungsvariablen an etwas über den Signierschritt hinaus zu übergeben.
  • Secrets werden nicht an Pull Requests aus Forks weitergegeben (außer GITHUB_TOKEN).9 Allerdings läuft der Job selbst dennoch mit leeren Secrets, sodass der obige Wiederherstellungsschritt beim Versuch, einen leeren String Base64-zu-dekodieren, fehlschlägt. Halten Sie den Signierschritt entweder auf einen tag-ausgelösten Release-Workflow wie den aus Kapitel 4 beschränkt (der bei einem Fork-PR nicht auslöst), oder fügen Sie eine explizite Bedingung wie if: github.event_name != 'pull_request' hinzu, um ihn bewusst zu überspringen.
      - name: Restore signing certificate
        shell: pwsh
        run: |
          $bytes = [Convert]::FromBase64String($env:PFX_BASE64)
          [IO.File]::WriteAllBytes("$env:RUNNER_TEMP\sign.pfx", $bytes)
        env:
          PFX_BASE64: ${{ secrets.SIGNING_PFX_BASE64 }}

Noch ein entscheidender Punkt: Schränken Sie ein, welche Workflows auf den Signierschlüssel zugreifen können. Jeder mit Schreibzugriff auf das Repository kann einen Workflow ändern, legen Sie das Signier-Secret also in einer Environment mit erforderlichen Reviewern ab, sodass nur der Release-Workflow darauf verweisen kann. Eine signierte Binärdatei ist selbst der Nachweis dafür, dass „Ihr Unternehmen dies gebaut hat“, behandeln Sie den Schlüssel also mit derselben Strenge an Vertrauensgrenzen, die Sie auch bei einer Auslieferungs-Pipeline für automatische Updates anwenden würden (siehe dazu auch „Die Sicherheit automatischer Updates“).

6. Eine Entscheidungstabelle zur CI/CD-Integration nach Distributionsformat

Sobald die Signierung steht, ist das letzte Puzzlestück die Form der Distributionsdatei selbst. Die Wahl des Formats überlassen wir „Eine Entscheidungstabelle für Distributionsmethoden“ und vergleichen hier ausschließlich aus Sicht von CI/CD.

Distributionsformat Aufwand des Builds in CI Wie man es in CI baut Signierungspflicht Automatische Aktualisierung
xcopy (zip-Distribution) Am einfachsten Nur dotnet publish + Komprimierung Signierung von EXE/DLL (empfohlen) Keine (manuelle Bereitstellung)
xcopy + selbst entwickelter Updater Einfach (für den Build selbst). Das Design der Update-Auslieferung ist ein separater, aufwendiger Kraftakt dotnet publish + Manifest-Erzeugung EXE-Signierung sowie ein Design zur Verifikation der Update-Dateien sind zwingend Selbst entwickelt (erfordert das Design einer Vertrauensgrenze)
MSI Mittel Aufruf eines Werkzeugs wie WiX aus der CLI Signierung der MSI-Datei (empfohlen bis faktisch zwingend) Keine (benötigt einen separaten Distributionsmechanismus)
MSIX Mittel MSBuild / MakeAppx + signtool Paketsignierung ist zwingend (ein unsigniertes Paket lässt sich nicht installieren)6 Lässt sich über App Installer usw. abwickeln
ClickOnce Voller Eigenheiten msbuild /target:publish + ein Publish-Profil (über die dotnet-CLI nicht unterstützt)7 Manifest-Signierung sowie EXE-Signierung Eingebaut (das Hauptverkaufsargument des Formats)

Einige ergänzende Hinweise.

  • xcopy (zip): Die Workflows aus den Kapiteln 3 und 4 sind im Wesentlichen bereits die fertige Form. Was auch immer das endgültige Distributionsformat wird, diese Form zuerst zum Laufen zu bringen, ist der kürzeste Weg.
  • MSI: Nehmen Sie die Installer-Definition (WiX usw.) ins Repository auf und bauen Sie sie über die CLI. Die eigentliche Substanz liegt hier nicht im Erzeugungsschritt selbst, sondern in der Designfrage „was kommt in das MSI“ (Dienstregistrierung, pro Maschine vs. pro Benutzer).
  • MSIX: Da Windows die Installation eines unsignierten MSIX nicht zulässt, können Sie die CI-Einbindung nicht als abgeschlossen bezeichnen, ohne dass eine automatisierte Signierung fest eingebaut ist.6 Umgekehrt ist dies, sobald Ihre Signierinfrastruktur steht, ein Format, das sich leicht in CI einbinden lässt. Verteilen Sie über den Microsoft Store, gibt es einen alternativen Weg – die erneute Signierung auf Store-Seite bedeutet, dass Sie überhaupt kein eigenes Zertifikat benötigen.5
  • ClickOnce: Dies lässt sich nicht über die dotnet-CLI veröffentlichen – Sie benötigen msbuild /target:publish /p:PublishProfile=... mit einem angegebenen Publish-Profil (.pubxml). Die Revisionsnummer (ApplicationRevision), die die IDE bei jeder Veröffentlichung automatisch erhöht, wird über die Befehlszeile nicht automatisch erhöht,7 was die explizite tag-gesteuerte Versionsübergabe aus Kapitel 4 unerlässlich macht. Ein Vorbehalt dabei: Die Update-Prüfung von ClickOnce richtet sich nicht nach -p:Version (den Assembly-Informationen), sondern nach der bereitstellungsseitigen Version (ApplicationVersion / ApplicationRevision), sodass ein neues Release nicht als Update erkannt wird, sofern Sie nicht separat den aus dem Tag gebildeten vierteiligen Wert übergeben, etwa als /p:ApplicationVersion=1.2.3.0. Den Mechanismus und wann sich das Format eignet, behandeln wir in „Was ist ClickOnce“.

Rein aus Sicht von CI/CD ist „mit zip beginnen und MSIX oder MSI als Job hinzufügen, sobald sich die Distributionsanforderungen gefestigt haben“ der Weg mit dem geringsten inkrementellen Verlust. Die früheren Stufen (Build, Test, Versionierung) werden von jedem Format gemeinsam genutzt, sodass das spätere Nachrüsten eines Distributionsformat-Schritts nichts von dieser früheren Investition verwirft.

7. Wie weit die Testautomatisierung gehen sollte

Zum Schluss ziehen wir die Grenze, wie viel Testen als CI-Gate (verpflichtende Prüfung) verlangt werden sollte.

Testebene Behandlung in CI Begründung
Unit-Tests (Logik) Verpflichtendes Gate. Läuft bei jedem Pull Request Schnell, stabil und läuft unverändert auf einem Windows-Runner
UI-freie Integrationstests (DB, Dateizugriff) Grundsätzlich verpflichtend; bei Langsamkeit in einen nächtlichen Lauf auslagern Die Einrichtung externer Abhängigkeiten erfordert etwas Sorgfalt, doch der Automatisierungsertrag ist hoch
Automatisierte UI-Tests (Smoke) Eine kleine Anzahl, in einem separaten Job Erfordert eine Desktop-Sitzung und birgt reichlich Instabilitätsquellen
Automatisierte UI-Tests (umfassende Abdeckung) Nicht zu einem CI-Gate machen Der Wartungsaufwand übersteigt tendenziell den Nutzen

Bei einer Desktop-Anwendung ist nicht die UI die Automatisierung mit dem höchsten Ertrag – es ist die Schicht darunter. Steckt Geschäftslogik im Code-Behind, lassen sich dafür keine Unit-Tests schreiben, sodass die Trennung von Logik und Bildschirm selbst eine vorgelagerte Investition für CI/CD ist. Die praktische Antwort besteht darin, automatisierte UI-Tests auf einen Smoke-Test zu beschränken – „starten, anmelden, die Hauptbildschirme öffnen sich“ – und diesen über einen separaten Auslöser wie nächtlich laufen zu lassen. UI-Tests auf einem Runner bergen reichlich Fallstricke rund um Bildschirmsitzungen, Auflösung und Timing; diese behandeln wir, einschließlich der Fallstricke bei CI und unbeaufsichtigter Ausführung, in „Automatisierte UI-Tests für Windows-Desktop-Anwendungen“.

8. Zusammenfassung

  • CI/CD für Desktop-Anwendungen lässt sich mit demselben Werkzeugsatz wie im Web aufbauen, sobald Sie den Ausstiegspunkt als „eine signierte Distributionsdatei erzeugen“ definieren.
  • Die minimale Einrichtung ist windows-latest + actions/checkout + actions/setup-dotnet + dotnet build / test + actions/upload-artifact. Allein das beseitigt das Risiko „lässt sich nur auf dem PC eines Entwicklers bauen“.12
  • WinForms / WPF setzen einen Windows-Runner voraus, da sie auf ein reines Windows-TFM wie net8.0-windows abzielen.3
  • Zentrieren Sie die Versionierung um tag-gesteuerte Releases: Ein Tag v1.2.3 fließt in -p:Version ein.4
  • Die Signierung ist die größte Mauer für die CI-Automatisierung. Da OV-Zertifikate nun ebenfalls eine HSM-Speicherung erfordern, ist ein Cloud-Signaturdienst die Hauptoption, wenn CI dies durchgängig übernehmen soll; der Ansatz PFX plus Secret ist im Grunde für eine interne CA oder ein bestehendes Zertifikat gedacht.59
  • Der Aufwand, ein Distributionsformat in CI einzubinden, verläuft der Reihe nach xcopy (zip) -> MSI / MSIX -> ClickOnce. MSIX erfordert eine Signierung,6 und ClickOnce erfordert Sorgfalt rund um msbuild /target:publish und seine nicht automatisch erhöhte Revision.7
  • Machen Sie Unit-Tests zu einem verpflichtenden CI-Gate, beschränken Sie UI-Tests auf Smoke-Tests in einem separaten Job, und behandeln Sie die Trennung von Logik und Bildschirm als vorgelagerte Investition.

Verwandte Artikel

Verwandte Beratungsleistungen

Komura Software LLC übernimmt die Entwicklung von WinForms-/WPF-Anwendungen, den Umzug von Teams aus einem reinen Lokal-Build-Betrieb hin zu CI/CD, den Entwurf von Build-/Signier-/Distributionspipelines mit GitHub Actions sowie die Testbarmachung bestehender Desktop-Anwendungen durch Trennung ihrer Logik.

  1. GitHub Docs, GitHub-hosted runners reference. Zu Runner-Labels wie windows-latest, dazu, dass jedem Job eine frische virtuelle Maschine zugewiesen wird, sowie dazu, dass Standard-Runner bei öffentlichen Repositories kostenlos sind.  2 3

  2. Microsoft Learn, GitHub Actions and .NET. Zu .NET-CI/CD über GitHub Actions, den Rollen von actions/checkout und actions/setup-dotnet, sowie zur Verwendung von dotnet restore / build / test / publish innerhalb eines Workflows.  2

  3. Microsoft Learn, MSBuild reference for .NET Desktop SDK projects. Dazu, dass WinForms-/WPF-Projekte ein Windows-spezifisches TFM wie net8.0-windows angeben, sowie zur Aktivierung des .NET-Desktop-SDK über UseWindowsForms / UseWPF 2 3

  4. Microsoft Learn, Set assembly attributes in a project file. Dazu, dass AssemblyVersion / FileVersion (ohne Suffix) und InformationalVersion standardmäßig aus der Eigenschaft Version erzeugt werden, sowie dazu, dass das .NET-8-SDK und neuer SourceRevisionId (den Commit-Hash) an InformationalVersion anhängt.  2 3 4

  5. Microsoft Learn, Code signing options for Windows app developers. Dazu, dass die Anforderungen des CA/Browser Forum seit Juni 2023 eine HSM-/Hardware-Token-Speicherung für die privaten Schlüssel von OV-Zertifikaten vorschreiben, dazu, dass der First-Run-SmartScreen-Bypass von EV-Zertifikaten 2024 abgeschafft wurde, dazu, dass Azure Artifact Signing (früher Trusted Signing) keinen Token benötigt und sich in GitHub Actions und Ähnliches integrieren lässt, dabei aber auf bestimmte Regionen beschränkt ist, sowie dazu, dass Microsoft über den Store verteilte MSIX-Pakete erneut signiert.  2 3 4 5 6 7

  6. Microsoft Learn, Sign an MSIX package. Dazu, dass Windows eine gültige Code-Signatur auf einem MSIX-Paket erfordert, sowie dazu, dass ein Zeitstempel die Signaturprüfung auch nach Ablauf des Zertifikats gültig hält.  2 3 4 5

  7. Microsoft Learn, Build .NET ClickOnce applications from the command line. Dazu, dass die .NET-ClickOnce-Veröffentlichung msbuild /target:publish mit einem angegebenen Publish-Profil erfordert, sowie dazu, dass ApplicationRevision bei Befehlszeilen-Builds nicht automatisch erhöht wird.  2 3 4

  8. Microsoft Learn, SignTool. Dazu, dass SignTool im Windows SDK enthalten ist, dass aktuelle Builds /fd und /td erfordern, wobei SHA256 empfohlen wird, sowie zur Angabe eines RFC-3161-Zeitstempels über /tr 2

  9. GitHub Docs, Using secrets in GitHub Actions. Dazu, dass Secret-Werte in Protokollen automatisch geschwärzt werden, zum Verfahren, Binärdaten wie Zertifikate als Base64 in einem Secret abzulegen und innerhalb eines Jobs wiederherzustellen, sowie dazu, dass Secrets nicht an von Forks ausgelöste Workflows weitergegeben werden.  2 3 4

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Welchen GitHub-Actions-Runner sollte ich für den Build einer WinForms-/WPF-Anwendung verwenden?
Verwenden Sie einen Windows-Runner wie windows-latest. WinForms-/WPF-Projekte zielen auf ein reines Windows-Target-Framework wie net8.0-windows ab, und sowohl die Überprüfung des Build-Ergebnisses als auch das Ausführen von Tests mit dotnet test erfordern eine Windows-Umgebung. Da GitHub-gehostete Runner jedem Job eine frische virtuelle Maschine zuweisen, erhalten Sie reproduzierbare Builds, die von keinem bestimmten Entwickler-PC abhängen. Standard-Runner sind bei öffentlichen Repositories kostenlos und werden bei privaten Repositories minutenweise abgerechnet.
Lässt sich die Code-Signierung in CI vollständig automatisieren?
Das hängt davon ab, wie das Zertifikat aufbewahrt wird. Der klassische Ansatz, eine PFX-Datei in einem Secret abzulegen und mit signtool zu signieren, lässt sich bei einem neu ausgestellten Zertifikat in der Regel nicht mehr anwenden, da die Anforderungen des CA/Browser Forum seit Juni 2023 die Speicherung des privaten Schlüssels öffentlicher OV-Zertifikate auf einem HSM (Hardware) vorschreiben. Ein Zertifikat auf einem USB-Token lässt sich nicht in einen Cloud-Runner stecken. Wenn Sie eine vollständige Automatisierung in CI wünschen, ist ein Cloud-Signaturdienst wie Azure Artifact Signing (früher Trusted Signing) oder ein von der Zertifizierungsstelle angebotenes Cloud-HSM der realistische Weg. Bleiben Sie beim tokenbasierten Betrieb, muss der Signierschritt selbst auf einer lokalen Maschine oder einem selbst gehosteten Runner verbleiben.
Womit sollte ich die Automatisierung zuerst beginnen?
Beginnen Sie nur mit der Automatisierung von Build und Test. Allein der Zustand, dass bei jedem Push dotnet build / dotnet test auf einem windows-latest-Runner laufen, beseitigt das größte Risiko: „Das lässt sich nur auf dem PC eines Entwicklers bauen“ und „Ein Merge, der den Build kaputt macht, fällt niemandem auf, bis es kurz vor einem Release ist.“ Die Automatisierung von Signierung, Installer-Erzeugung und Distribution lässt sich danach schrittweise ergänzen – der Versuch, alles auf einmal einzurichten, bleibt tendenziell an der Signierung hängen.
Ändert sich der Aufwand für die Einrichtung von CI/CD je nach Distributionsformat (MSI / MSIX / ClickOnce / xcopy)?
Ja, deutlich. Die xcopy-Distribution (zip) ist am einfachsten, da sie lediglich die Ausgabe von dotnet publish komprimiert. MSIX lässt sich mit MSBuild und signtool in CI einbinden, doch die Signierung des Pakets ist zwingend erforderlich. MSI lässt sich automatisieren, indem ein Werkzeug wie WiX aus CI heraus aufgerufen wird. ClickOnce lässt sich über die dotnet-CLI überhaupt nicht veröffentlichen – es benötigt msbuild /target:publish in Kombination mit einem Publish-Profil, und Sie müssen zudem darauf achten, dass die Revisionsnummer über die Befehlszeile nicht automatisch erhöht wird. Wie leicht sich ein Format in CI einbinden lässt, sollte bereits bei der Wahl des Distributionsformats selbst einer der Abwägungsfaktoren sein.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog