Als u ooit een MSIX-package hebt geprobeerd te maken op een GitHub Actions Windows-runner, hebt u waarschijnlijk een stap geschreven die door C:\Program Files (x86)\Windows Kits\10\bin gaat zoeken naar makeappx.exe, omdat het pad ervan een SDK-versienummer bevat dat verandert wanneer de runner-image verandert.

Die stap werkt tot de image wordt bijgewerkt. Dan breekt hij, en hij breekt op een manier die eruitziet als een packagingfout in plaats van een padfout.

Er is een schonere manier, en die komt neer op een detail over waar MSIX-packaging eigenlijk woont.

Het detail dat het waard is te weten

MSIX-packaging is niet iets dat de Windows SDK doet. De AppxPackaging-engine is een onderdeel van Windows zelf. makeappx.exe is een commandoregel-wrapper rond die engine die toevallig binnen de SDK wordt meegeleverd.

Een tool kan dus hetzelfde onderdeel van het besturingssysteem rechtstreeks aanroepen, zonder enige SDK op de machine. Dat is wat forge_msix doet. Het is een rechtstreekse vervanging van makeappx.exe: dezelfde werkwoorden, dezelfde schakelaars, dezelfde verwachtingen over invoer en uitvoer. Roept uw workflow nu makeappx pack aan, dan wijzigt u de naam van het uitvoerbare bestand en blijft de stap werken.

De makeappx-compatibele werkwoorden zijn pack, unpack, bundle, unbundle, validate en make-pri. Samen dekken ze de SDK-kant van een normale packagingklus: een package bouwen uit een map, er een uit elkaar halen om te zien wat erin ging, packages per architectuur combineren tot een bundel, en de bronnenindex bouwen die het manifest verwacht.

Stroomdiagram van een MSIX-build op een GitHub Actions Windows-runner zonder geïnstalleerde Windows SDK: checkout en dotnet publish leveren een buildmap met een app-manifest, forge_msix pack roept de AppxPackaging-engine van Windows aan die al op de runner aanwezig is om die map om te zetten in een MSIX-package, forge_msix validate bewaakt de job door exitcode 2 terug te geven wanneer het package is geanalyseerd en ongeldig bevonden, forge_msix sign zet een handtekening met tijdstempel vanuit een certificaat dat de runner kan bereiken, en het gesigneerde package wordt geüpload als workflow-artefact en gepubliceerd via een App Installer-updatefeed die geïnstalleerde clients raadplegen op nieuwe versies.

Bouwen, packen, valideren, signen, publiceren, met de packaging-engine afkomstig uit Windows in plaats van uit een SDK-installatie.

De tool op de runner krijgen

De CLI wordt meegeleverd met EtherApps Forge, dus hem op een runner zetten is een gewone toolinstallatie. Zet het uitvoerbare bestand ergens op schijf en voeg die map toe aan GITHUB_PATH zodat latere stappen hem bij naam kunnen aanroepen:

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

Op een gehoste runner betekent dat één installatiestap bovenaan de job, gecachet zoals al het andere. Op een zelfgehoste runner betekent het één keer installeren wanneer u de image bouwt. Hoe dan ook levert u één commandoregel-uitvoerbaar bestand in plaats van een ontwikkelkit, en daar draait het om.

Windows-runners gebruiken standaard pwsh voor run:-stappen, dus de voorbeelden hieronder gaan uit van PowerShell in plaats van cmd.

Hoe de workflowstap eruitziet

Het punt is hoe onopvallend hij is.

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

Geen SDK-installatiestap. Geen padontdekking. Geen versievastzetting tegen de runner-image.

Drie dingen laten de eerste uitvoering van die stap betrouwbaar struikelen.

De inhoudsmap heeft een AppxManifest.xml in de root nodig. Een gewone publish-uitvoer heeft die niet tenzij uw project hem produceert, en pack weigert een map zonder. Dit is de meest voorkomende eerste fout, en hij leest als een toolingprobleem terwijl het een projectconfiguratieprobleem is.

Opnieuw draaien over een bestaande uitvoer faalt tenzij u /o meegeeft. Op een schone gehoste runner ziet u dit nooit. Op een zelfgehoste runner met een blijvende werkruimte ziet u het bij de tweede build, en dat is precies wanneer u niet meer oplet.

Bronstrings hebben een resources.pri nodig. Verwijst uw manifest naar ms-resource:-waarden en is er geen bronnenindex gebouwd, dan kan het package installeren en daarna lege namen tonen in het startmenu. make-pri bouwt die index, en hij hoort vóór pack.

Wilt u liever geen bestanden in een map klaarzetten alleen om het package vorm te geven, gebruik dan een mappingbestand in plaats van /d:

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

Vervolgens bouwt forge_msix pack /f .\mapping.txt /p .\artifacts\ContosoApp.msix precies wat u hebt opgesomd en niets anders. Weet u niet zeker wat erin is beland, dan geeft forge_msix unpack /p .\artifacts\ContosoApp.msix /d .\inspect het package terug als een map die u kunt vergelijken.

Nog iets dat in de pipeline hoort in plaats van in iemands hoofd: de packageversie komt uit het manifest, niet uit het packagingcommando. Stempel hem vanuit de build voordat u packt, en houd het vierde veld op nul omdat die positie gereserveerd is.

$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 worden alleen toegepast wanneer de versie toeneemt, dus een pipeline die tweemaal dezelfde versie levert, produceert een update die niemand ontvangt en geen fout die uitlegt waarom.

Laat de build falen, niet de release

validate geeft een exitcode terug die niet nul is wanneer het package niet geldig is, en dat is wat de job laat falen op het punt van het probleem in plaats van bij de uitrol.

Eén detail is het afhandelen waard: exitcode 2 betekent dat het package succesvol is geanalyseerd en ongeldig is, in tegenstelling tot de tool die niet kon draaien. Die twee gevallen identiek behandelen levert een verwarrende rode build op. Ze verschillend behandelen geeft u een poort die u iets nuttigs vertelt.

Die exitcode uitlezen vraagt meer zorg dan het lijkt, want GitHub Actions draait pwsh-stappen met $ErrorActionPreference op stop, en recente PowerShell-versies passen die voorkeur ook toe op native commando's. De stap kan daardoor stoppen op de exitcode die niet nul is voordat uw vergelijking draait. Zet dat uit voor de stap en inspecteer de code zelf:

      - 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" }

Het onderscheid telt zwaarder in een gedeelde pipeline dan in uw eigen. "Het package is fout" is een boodschap waar degene die het brak iets mee kan. "De packagingstap is mislukt" stuurt hem naar de runnerlogboeken om uit te zoeken welke van de twee dingen er gebeurde.

Signen in een pipeline

Packagen zonder signen levert een artefact op dat u niet op een beheerd apparaat kunt installeren, dus de signingstap hoort in dezelfde job.

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

Twee dingen die zwaarder tellen dan ze lijken:

Sign op vingerafdruk vanuit het certificaatarchief, in plaats van een PFX en een wachtwoord door workflowvariabelen te sturen. De private sleutel blijft waar hij hoort.

Zet altijd een tijdstempel, met een RFC 3161-server. Zonder tijdstempel stopt het package met valideren zodra het signingcertificaat verloopt, ook al is de handtekening rechtmatig gemaakt toen het certificaat nog geldig was. Elk package dat u ooit hebt geleverd wordt op dezelfde dag oninstalleerbaar. Met een tijdstempel overleven handtekeningen het certificaat.

Dat eerste punt bepaalt waar het signen draait. Een gehoste runner is kortstondig en heeft geen certificaatarchief dat die naam waard is, en sinds het CA/Browser Forum in juni 2023 zijn eisen voor code signing aanscherpte, moeten publiek vertrouwde signingsleutels op gecertificeerde hardware of in een clouddienst voor signing wonen, dus een PFX in een repositorygeheim is sowieso geen optie. In de praktijk betekent dat een zelfgehoste runner met de token of hardwaremodule aangesloten, of een gehoste runner die authenticeert bij een signingdienst en de sleutel nooit vasthoudt.

De foutsignatuur om te herkennen is 0x8007000B op de signingstap. Hij betekent meestal dat de Publisher-waarde in het manifest niet exact overeenkomt met het onderwerp van het signingcertificaat. Niet bij benadering, exact, inclusief leestekens en spaties. De fouttekst zegt niets over publishers, en daarom kost hij de eerste keer een middag.

Let op dat sign een van de werkwoorden met toegevoegde waarde is en niet een van de makeappx-compatibele, dus hij vereist een actieve EtherApps Forge-licentie. Hetzelfde geldt voor create, test, assess, de cert--werkwoorden en de dev--werkwoorden.

Gehoste runners tegenover zelfgehoste

Op gehoste runners is de winst dat u niet bij elke schone uitvoering een SDK van meerdere gigabytes hoeft te installeren, en dat is tijd bij elke afzonderlijke build.

Op zelfgehoste runners is de winst anders: uw image draagt de SDK niet meer, en wordt niet meer gevoelig voor welke SDK-versie hij draagt. Twee runners met verschillende SDK-versies kunnen verschillend packaginggedrag opleveren, en dat hoort bij de minst plezierige soorten buildfouten om te diagnosticeren.

Er is ook een governance-invalshoek. Een ontwikkelkit op buildinfrastructuur is een groot oppervlak om te verantwoorden aan wie goedkeurt wat er op die machines komt, en hij doet één taak: een map omzetten in een package.

Leeft uw pipeline in Azure DevOps in plaats van GitHub Actions, dan geldt dezelfde redenering ongewijzigd. MSIX bouwen en signen in CI/CD behandelt daar de equivalente fasen, inclusief de werkwoorden dev-register en dev-test die de lokale lus verkorten voordat er iets een pipeline bereikt.

Het resultaat publiceren

Zodra packages door de pipeline zijn gemaakt en gesigneerd, is distributie de overgebleven vraag. make-appinstaller genereert een App Installer-updatefeed: publiceer het package en de feed, en bestaande installaties raadplegen die op nieuwe versies. Voor een ISV die buiten een beheerde omgeving levert, is dat vaak het hele distributieverhaal.

Twee details bepalen of het de eerste keer werkt. De feed en het package moeten via HTTPS worden geserveerd met de juiste inhoudstypen, want een server die .appinstaller of .msix teruggeeft als een generieke binaire stroom levert een downloadprompt op in plaats van een installatie. En het updategedrag wordt in de feed zelf ingesteld, inclusief hoe vaak clients controleren en of een update bij het starten of op de achtergrond wordt toegepast.

Beide zijn makkelijk in één keer goed te doen en heel vervelend om te diagnosticeren op basis van de beschrijving van een gebruiker, wat een argument is om de gepubliceerde feed vanaf een schone machine te valideren als onderdeel van de release.

Waar EtherApps Forge past

forge_msix is het commandoregeloppervlak van EtherApps Forge, een Windows-desktopapplicatie en geen gehoste dienst, met een gratis proefperiode van 7 dagen. Dat telt voor een buildpipeline: de tool draait op infrastructuur die u beheert en de packages verlaten die nooit.

De bredere context staat op de route MSIX-packaging voor ontwikkelaars en ISV's, die aanneemt wat dit artikel aanneemt: een gezonde buildmap, niets te capturen, en alle wrijving na een geslaagde build. Dat is een ander probleem dan het probleem over de hele omgeving, waar een applicatie zonder installer aankomt en vanaf een draaiende machine moet worden herbouwd. Is dat uw situatie, dan is MSIX-packaging en uitrol de route en behandelt een EXE naar MSIX converteren het applicatie voor applicatie. De releasenotities van 1.0.6 leggen uit waar de mogelijkheden voor ontwikkelaars en ISV's vandaan kwamen.

Probeer het tegen uw eigen workflow

De eerlijke test is of uw bestaande packagingstap blijft werken wanneer u de naam van het uitvoerbare bestand wijzigt. Neem een branch, verwissel makeappx voor forge_msix in de stappen pack en validate, en kijk of de job groen wordt zonder dat u iets anders aanraakt. Als dat zo is, droeg de SDK-afhankelijkheid op uw agent nooit zijn gewicht.

Ontdek MSIX-packaging voor ontwikkelaars en ISV's

De packaging-engine staat al op de runner; de enige vraag is wat u installeert om hem te bereiken.