Wenn Sie versucht haben, ein MSIX-Paket auf einem GitHub Actions Windows-Runner zu erzeugen, haben Sie wahrscheinlich einen Schritt geschrieben, der C:\Program Files (x86)\Windows Kits\10\bin nach makeappx.exe durchsucht, weil dessen Pfad eine SDK-Versionsnummer enthält, die sich ändert, wenn sich das Runner-Image ändert.

Dieser Schritt funktioniert, bis das Image aktualisiert wird. Dann bricht er, und er bricht auf eine Weise, die eher nach einem Packaging-Fehler als nach einem Pfadfehler aussieht.

Es gibt einen saubereren Weg, und er läuft auf ein Detail darüber hinaus, wo MSIX-Packaging tatsächlich lebt.

Das Wissenswerte

MSIX-Packaging ist nichts, was das Windows SDK leistet. Die AppxPackaging-Engine ist eine Komponente von Windows selbst. makeappx.exe ist ein Kommandozeilen-Wrapper um diese Engine, der zufällig im SDK ausgeliefert wird.

Ein Werkzeug kann also dieselbe Betriebssystemkomponente direkt aufrufen, ganz ohne SDK auf dem Rechner. Genau das tut forge_msix. Es ist ein Drop-in-Ersatz für makeappx.exe: dieselben Verben, dieselben Schalter, dieselben Erwartungen an Ein- und Ausgabe. Wenn Ihr Workflow derzeit makeappx pack aufruft, ändern Sie den Namen der Programmdatei und der Schritt läuft weiter.

Die makeappx-kompatiblen Verben sind pack, unpack, bundle, unbundle, validate und make-pri. Zusammen decken sie die SDK-Seite einer normalen Packaging-Aufgabe ab: ein Paket aus einem Ordner bauen, eines auseinandernehmen, um zu sehen, was hineinkam, Pakete je Architektur zu einem Bundle kombinieren und den Ressourcenindex bauen, den das Manifest erwartet.

Flussdiagramm eines MSIX-Builds auf einem GitHub Actions Windows-Runner ohne installiertes Windows SDK: Checkout und dotnet publish erzeugen einen Build-Ordner mit einem App-Manifest, forge_msix pack ruft die bereits auf dem Runner vorhandene Windows AppxPackaging-Engine auf, um diesen Ordner in ein MSIX-Paket zu verwandeln, forge_msix validate hält den Job an, indem es den Exit-Code 2 zurückgibt, wenn das Paket analysiert und als ungültig befunden wird, forge_msix sign bringt eine zeitgestempelte Signatur von einem Zertifikat auf, das der Runner erreichen kann, und das signierte Paket wird als Workflow-Artefakt hochgeladen und über einen App Installer Update-Feed veröffentlicht, den installierte Clients auf neue Versionen prüfen.

Bauen, packen, validieren, signieren, veröffentlichen, wobei die Packaging-Engine von Windows kommt statt von einer SDK-Installation.

Das Werkzeug auf den Runner bringen

Die CLI wird mit EtherApps Forge ausgeliefert, sodass sie auf einen Runner zu bringen eine gewöhnliche Werkzeuginstallation ist. Legen Sie die Programmdatei irgendwo auf die Platte und hängen Sie diesen Ordner an GITHUB_PATH an, damit spätere Schritte sie beim Namen aufrufen können:

      - name: Add the packaging CLI to PATH
        run: echo "C:\tools\forge" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

Auf einem gehosteten Runner bedeutet das einen Installationsschritt oben im Job, zwischengespeichert wie alles andere. Auf einem selbst gehosteten Runner bedeutet es, einmal beim Bauen des Images zu installieren. So oder so stellen Sie eine einzelne Kommandozeilen-Programmdatei statt eines Entwicklungskits bereit, und darum geht es.

Windows-Runner nutzen für run:-Schritte standardmäßig pwsh, die folgenden Beispiele setzen also PowerShell statt cmd voraus.

Wie der Workflow-Schritt aussieht

Der Punkt ist, wie unspektakulär er ist.

jobs:
  package:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: dotnet publish -c Release -o publish/win-x64
      - name: Package as MSIX
        run: forge_msix pack /d .\publish\win-x64 /p .\artifacts\ContosoApp.msix
      - name: Validate
        run: forge_msix validate /p .\artifacts\ContosoApp.msix
      - uses: actions/upload-artifact@v4
        with:
          name: msix
          path: artifacts/ContosoApp.msix

Kein SDK-Installationsschritt. Keine Pfadsuche. Kein Festnageln von Versionen gegen das Runner-Image.

Drei Dinge bringen den ersten Lauf dieses Schritts zuverlässig zu Fall.

Das Inhaltsverzeichnis braucht eine AppxManifest.xml in seinem Wurzelverzeichnis. Eine schlichte Publish-Ausgabe hat keine, sofern Ihr Projekt sie nicht erzeugt, und pack verweigert einen Ordner ohne sie. Das ist der häufigste erste Fehlschlag, und er liest sich wie ein Werkzeugproblem, obwohl es ein Projektkonfigurationsproblem ist.

Ein erneuter Lauf über eine bestehende Ausgabe schlägt fehl, wenn Sie nicht /o übergeben. Auf einem sauberen gehosteten Runner sehen Sie das nie. Auf einem selbst gehosteten Runner mit dauerhaftem Arbeitsbereich sehen Sie es beim zweiten Build, also dann, wenn Sie nicht mehr hinschauen.

Ressourcen-Strings brauchen eine resources.pri. Wenn Ihr Manifest auf ms-resource:-Werte verweist und kein Ressourcenindex gebaut wurde, kann sich das Paket installieren und dann leere Namen im Startmenü zeigen. make-pri baut diesen Index, und er gehört vor pack.

Wenn Sie Dateien lieber nicht erst in einen Ordner stellen wollen, nur um das Paket zu formen, nutzen Sie eine Mapping-Datei statt /d:

[Files]
"publish\win-x64\ContosoApp.exe" "ContosoApp.exe"
"publish\win-x64\AppxManifest.xml" "AppxManifest.xml"
"assets\Square150x150Logo.png" "Assets\Square150x150Logo.png"

Dann baut forge_msix pack /f .\mapping.txt /p .\artifacts\ContosoApp.msix genau das, was Sie aufgelistet haben, und sonst nichts. Wenn Sie nicht sicher sind, was hineinkam, gibt Ihnen forge_msix unpack /p .\artifacts\ContosoApp.msix /d .\inspect das Paket als Ordner zurück, den Sie vergleichen können.

Noch etwas, das in die Pipeline gehört statt in den Kopf einer Person: Die Paketversion kommt aus dem Manifest, nicht aus dem Packaging-Befehl. Prägen Sie sie vor dem Packen aus dem Build ein und lassen Sie das vierte Feld auf null, da diese Stelle reserviert ist.

$manifest = ".\publish\win-x64\AppxManifest.xml"
$xml = [xml](Get-Content $manifest)
$xml.Package.Identity.Version = "1.4.$env:GITHUB_RUN_NUMBER.0"
$xml.Save((Resolve-Path $manifest))

Updates greifen nur, wenn die Version steigt, eine Pipeline, die zweimal dieselbe Version ausliefert, erzeugt also ein Update, das niemand erhält, und keinen Fehler, der erklärt, warum.

Lassen Sie den Build scheitern, nicht das Release

validate gibt einen Exit-Code ungleich null zurück, wenn das Paket nicht gültig ist, und genau das lässt den Job an der Stelle des Problems scheitern statt bei der Bereitstellung.

Ein Detail lohnt die Behandlung: Exit-Code 2 bedeutet, dass das Paket erfolgreich analysiert wurde und ungültig ist, im Gegensatz dazu, dass das Werkzeug nicht laufen konnte. Diese beiden Fälle gleich zu behandeln, erzeugt einen verwirrenden roten Build. Sie unterschiedlich zu behandeln, gibt Ihnen ein Tor, das Ihnen etwas Nützliches sagt.

Diesen Exit-Code zu lesen, verlangt mehr Sorgfalt, als es aussieht, denn GitHub Actions führt pwsh-Schritte mit $ErrorActionPreference auf stop aus, und neuere PowerShell-Versionen wenden diese Einstellung auch auf native Befehle an. Der Schritt kann daher beim Exit ungleich null abbrechen, bevor Ihr Vergleich läuft. Schalten Sie das für den Schritt ab und prüfen Sie den Code selbst:

      - name: Validate
        shell: pwsh
        run: |
          $PSNativeCommandUseErrorActionPreference = $false
          forge_msix validate /p .\artifacts\ContosoApp.msix
          if ($LASTEXITCODE -eq 2) { throw "Package analysed and found invalid" }
          if ($LASTEXITCODE -ne 0) { throw "Validator failed to run, exit $LASTEXITCODE" }

Der Unterschied zählt in einer gemeinsam genutzten Pipeline mehr als in Ihrer eigenen. "Das Paket ist falsch" ist eine Meldung, auf die derjenige, der es kaputt gemacht hat, reagieren kann. "Der Packaging-Schritt ist fehlgeschlagen" schickt ihn in die Runner-Protokolle, um herauszufinden, welches der beiden Dinge passiert ist.

Signieren in einer Pipeline

Paketieren ohne Signieren erzeugt ein Artefakt, das Sie auf einem verwalteten Gerät nicht installieren können, der Signierschritt gehört also in denselben Job.

forge_msix sign /p .\artifacts\ContosoApp.msix `
  /thumbprint $env:SIGNING_THUMBPRINT `
  /timestamp http://timestamp.digicert.com

Zwei Dinge, die mehr zählen, als sie aussehen:

Signieren Sie per Fingerabdruck aus dem Zertifikatspeicher, statt eine PFX und ein Kennwort durch Workflow-Variablen zu reichen. Der private Schlüssel bleibt, wo er hingehört.

Setzen Sie immer einen Zeitstempel, mit einem RFC 3161 Server. Ohne ihn hört das Paket in dem Moment auf zu validieren, in dem das Signaturzertifikat abläuft, obwohl die Signatur rechtmäßig erstellt wurde, während das Zertifikat gültig war. Jedes Paket, das Sie je ausgeliefert haben, wird am selben Tag uninstallierbar. Mit einem Zeitstempel überleben Signaturen das Zertifikat.

Dieser erste Punkt entscheidet, wo das Signieren läuft. Ein gehosteter Runner ist flüchtig und hat keinen nennenswerten Zertifikatspeicher, und seit das CA/Browser Forum im Juni 2023 seine Anforderungen an Code Signing verschärft hat, müssen öffentlich vertrauenswürdige Signaturschlüssel auf zertifizierter Hardware oder in einem Cloud-Signaturdienst liegen, sodass eine PFX in einem Repository-Geheimnis ohnehin keine Option ist. In der Praxis heißt das ein selbst gehosteter Runner mit angeschlossenem Token oder Hardwaremodul, oder ein gehosteter Runner, der sich an einem Signaturdienst authentifiziert und den Schlüssel nie besitzt.

Die Fehlersignatur, die Sie erkennen sollten, ist 0x8007000B im Signierschritt. Sie bedeutet meist, dass der Publisher-Wert im Manifest nicht exakt mit dem Subject des Signaturzertifikats übereinstimmt. Nicht ungefähr, exakt, einschließlich Zeichensetzung und Abständen. Der Fehlertext sagt nichts über Herausgeber, weshalb er beim ersten Mal einen Nachmittag kostet.

Beachten Sie, dass sign eines der wertsteigernden Verben ist statt eines der makeappx-kompatiblen, es benötigt also eine aktive EtherApps Forge Lizenz. Dasselbe gilt für create, test, assess, die cert--Verben und die dev--Verben.

Gehostete Runner gegenüber selbst gehosteten

Auf gehosteten Runnern besteht der Gewinn darin, kein mehrere Gigabyte großes SDK bei jedem sauberen Lauf installieren zu müssen, was Zeit bei jedem einzelnen Build ist.

Auf selbst gehosteten Runnern ist der Gewinn ein anderer: Ihr Image trägt das SDK nicht mehr mit und ist nicht mehr empfindlich dafür, welche SDK-Version es trägt. Zwei Runner mit unterschiedlichen SDK-Versionen können unterschiedliches Packaging-Verhalten erzeugen, was zu den am wenigsten erfreulichen Klassen von Build-Fehlern gehört, die man diagnostizieren kann.

Es gibt auch einen Governance-Aspekt. Ein Entwicklungskit auf der Build-Infrastruktur ist eine große Fläche, die man denjenigen rechtfertigen muss, die freigeben, was auf diese Maschinen kommt, und es erledigt eine Aufgabe: einen Ordner in ein Paket verwandeln.

Wenn Ihre Pipeline in Azure DevOps statt in GitHub Actions liegt, gilt dieselbe Begründung unverändert. MSIX in CI/CD bauen und signieren behandelt die entsprechenden Stufen dort, einschließlich der Verben dev-register und dev-test, die die lokale Schleife verkürzen, bevor etwas eine Pipeline erreicht.

Das Ergebnis veröffentlichen

Sobald Pakete von der Pipeline erzeugt und signiert sind, bleibt die Verteilung als offene Frage. make-appinstaller erzeugt einen App Installer Update-Feed: Veröffentlichen Sie das Paket und den Feed, und vorhandene Installationen prüfen ihn auf neue Versionen. Für einen ISV, der außerhalb einer verwalteten Umgebung ausliefert, ist das oft die gesamte Verteilungsgeschichte.

Zwei Details entscheiden, ob es beim ersten Mal klappt. Der Feed und das Paket müssen über HTTPS mit den richtigen Inhaltstypen ausgeliefert werden, denn ein Server, der .appinstaller oder .msix als generischen Binärstrom zurückgibt, erzeugt eine Download-Abfrage statt einer Installation. Und das Update-Verhalten wird im Feed selbst festgelegt, einschließlich dessen, wie oft Clients prüfen und ob ein Update beim Start oder im Hintergrund angewendet wird.

Beides ist leicht einmal richtig zu machen und sehr lästig aus der Beschreibung eines Nutzers zu diagnostizieren, was ein Argument dafür ist, den veröffentlichten Feed als Teil des Release von einem sauberen Rechner aus zu prüfen.

Wo EtherApps Forge ansetzt

forge_msix ist die Kommandozeilenoberfläche von EtherApps Forge, einer Windows-Desktopanwendung statt eines gehosteten Dienstes, mit einer kostenlosen 7-tägigen Testversion. Das zählt für eine Build-Pipeline: Das Werkzeug läuft auf Infrastruktur, die Sie kontrollieren, und die Pakete verlassen sie nie.

Der weitere Kontext liegt auf dem Weg MSIX Packaging für Entwickler und ISVs, der voraussetzt, was auch dieser Artikel voraussetzt: ein gesunder Build-Ordner, nichts zu erfassen, und alle Reibung nach dem erfolgreichen Build. Das ist ein anderes Problem als das umgebungsweite, bei dem eine Anwendung ohne Installer ankommt und aus einer laufenden Maschine neu gebaut werden muss. Wenn das Ihre Lage ist, ist MSIX Packaging und Deployment der Weg und eine EXE zu MSIX konvertieren deckt es Anwendung für Anwendung ab. Die Release Notes zu 1.0.6 erklären, woher die Fähigkeiten für Entwickler und ISVs kommen.

Probieren Sie es an Ihrem eigenen Workflow aus

Der ehrliche Test ist, ob Ihr bestehender Packaging-Schritt weiterläuft, wenn Sie den Namen der Programmdatei ändern. Nehmen Sie einen Branch, tauschen Sie makeappx gegen forge_msix in den Pack- und Validate-Schritten und sehen Sie, ob der Job grün wird, ohne dass Sie sonst etwas anfassen. Wenn ja, hat die SDK-Abhängigkeit auf Ihrem Agenten nie ihr Gewicht getragen.

MSIX Packaging für Entwickler und ISVs entdecken

Die Packaging-Engine ist bereits auf dem Runner; die einzige Frage ist, was Sie installieren, um sie zu erreichen.