Har du försökt producera ett MSIX-paket på en Windows-runner i GitHub Actions har du förmodligen skrivit ett steg som letar igenom C:\Program Files (x86)\Windows Kits\10\bin efter makeappx.exe, eftersom dess sökväg innehåller ett SDK-versionsnummer som ändras när runner-avbildningen ändras.
Det steget fungerar tills avbildningen uppdateras. Sedan går det sönder, och det går sönder på ett sätt som ser ut som ett paketeringsfel snarare än ett sökvägsfel.
Det finns ett renare sätt, och det handlar om en detalj kring var MSIX-paketering faktiskt bor.
Detaljen värd att känna till
MSIX-paketering är inte något Windows SDK gör. AppxPackaging-motorn är en komponent i Windows självt. makeappx.exe är ett kommandoradsomslag runt den motorn som råkar levereras inuti SDK:n.
Ett verktyg kan alltså anropa samma operativsystemskomponent direkt, utan någon SDK på maskinen alls. Det är vad forge_msix gör. Det är en direkt ersättare för makeappx.exe: samma verb, samma växlar, samma förväntningar på indata och utdata. Anropar ditt arbetsflöde i dag makeappx pack byter du namnet på det körbara programmet och steget fortsätter fungera.
De makeappx-kompatibla verben är pack, unpack, bundle, unbundle, validate och make-pri. Tillsammans täcker de SDK-sidan av ett normalt paketeringsjobb: bygga ett paket från en mapp, plocka isär ett för att se vad som kom med, kombinera paket per arkitektur till en bunt, och bygga resursindexet som manifestet förväntar sig.

Bygg, paketera, validera, signera, publicera, med paketeringsmotorn hämtad ur Windows i stället för ur en SDK-installation.
Att få verktyget på runnern
CLI:t levereras med EtherApps Forge, så att lägga det på en runner är en vanlig verktygsinstallation. Lägg det körbara programmet någonstans på disk och lägg till den mappen i GITHUB_PATH så att senare steg kan anropa det med namn:
- name: Add the packaging CLI to PATH
run: echo "C:\tools\forge" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
På en värdbaserad runner betyder det ett installationssteg högst upp i jobbet, cachat som allt annat. På en självhostad runner betyder det installation en gång när du bygger avbildningen. Hur som helst tillhandahåller du ett enda kommandoradsprogram snarare än ett utvecklingskit, vilket är hela poängen.
Windows-runners använder som standard pwsh för run:-steg, så exemplen nedan förutsätter PowerShell snarare än cmd.
Hur arbetsflödessteget ser ut
Poängen är hur oanmärkningsvärt det är.
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
Inget SDK-installationssteg. Ingen sökvägsletning. Ingen versionslåsning mot runner-avbildningen.
Tre saker får tillförlitligt den första körningen av det steget på fall.
Innehållskatalogen behöver en AppxManifest.xml i sin rot. En vanlig publish-utdata har ingen om inte ditt projekt producerar den, och pack avvisar en mapp utan den. Detta är det vanligaste första felet, och det läses som ett verktygsproblem när det är ett projektkonfigurationsproblem.
Att köra om över en befintlig utdata misslyckas om du inte skickar /o. På en ren värdbaserad runner ser du aldrig detta. På en självhostad runner med en bestående arbetsyta ser du det vid det andra bygget, vilket är precis när du slutat titta.
Resurssträngar behöver en resources.pri. Refererar ditt manifest till ms-resource:-värden och inget resursindex byggdes kan paketet installeras och sedan visa tomma namn på Start-menyn. make-pri bygger det indexet, och det hör hemma före pack.
Vill du hellre slippa lägga upp filer i en mapp bara för att forma paketet, använd en mappningsfil i stället för /d:
[Files]
"publish\win-x64\ContosoApp.exe" "ContosoApp.exe"
"publish\win-x64\AppxManifest.xml" "AppxManifest.xml"
"assets\Square150x150Logo.png" "Assets\Square150x150Logo.png"
Sedan bygger forge_msix pack /f .\mapping.txt /p .\artifacts\ContosoApp.msix exakt det du listade och inget annat. När du inte är säker på vad som kom med ger forge_msix unpack /p .\artifacts\ContosoApp.msix /d .\inspect tillbaka paketet som en mapp du kan jämföra.
En sak till som hör hemma i pipelinen snarare än i en persons huvud: paketversionen kommer från manifestet, inte från paketeringskommandot. Stämpla den från bygget före paketeringen, och håll det fjärde fältet på noll eftersom den positionen är reserverad.
$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))
Uppdateringar tillämpas bara när versionen ökar, så en pipeline som levererar samma version två gånger producerar en uppdatering ingen tar emot och inget fel som förklarar varför.
Låt bygget fallera, inte releasen
validate returnerar en avslutskod som inte är noll när paketet inte är giltigt, och det är vad som låter jobbet fallera vid problemets punkt snarare än vid utrullningen.
En detalj värd att hantera: avslutskod 2 betyder att paketet analyserats framgångsrikt och är ogiltigt, till skillnad från att verktyget inte kunde köras. Att behandla de två fallen likadant ger ett förvirrande rött bygge. Att behandla dem olika ger dig en grind som berättar något användbart.
Att läsa den avslutskoden kräver mer omsorg än det ser ut, för GitHub Actions kör pwsh-steg med $ErrorActionPreference satt till stop, och senare PowerShell-versioner tillämpar den inställningen även på inbyggda kommandon. Steget kan därför avslutas på den nollskilda koden innan din jämförelse körs. Stäng av det för steget och granska koden själv:
- 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" }
Skillnaden betyder mer i en delad pipeline än i din egen. "Paketet är fel" är ett meddelande den som gjorde sönder det kan agera på. "Paketeringssteget misslyckades" skickar hen till runnerloggarna för att lista ut vilken av de två sakerna som hände.
Signering i en pipeline
Paketering utan signering ger en artefakt du inte kan installera på en hanterad enhet, så signeringssteget hör hemma i samma jobb.
forge_msix sign /p .\artifacts\ContosoApp.msix `
/thumbprint $env:SIGNING_THUMBPRINT `
/timestamp http://timestamp.digicert.com
Två saker som betyder mer än de ser ut:
Signera med tumavtryck från certifikatarkivet, snarare än att skicka en PFX och ett lösenord genom arbetsflödesvariabler. Den privata nyckeln stannar där den hör hemma.
Tidsstämpla alltid, med en RFC 3161-server. Utan den slutar paketet validera i samma stund som signeringscertifikatet går ut, trots att signaturen skapades korrekt medan certifikatet var giltigt. Varje paket du någonsin levererat blir omöjligt att installera samma dag. Med en tidsstämpel överlever signaturer certifikatet.
Den första punkten avgör var signeringen körs. En värdbaserad runner är kortlivad och har inget certifikatarkiv värt namnet, och sedan CA/Browser Forum skärpte sina krav på kodsignering i juni 2023 måste publikt betrodda signeringsnycklar bo på certifierad hårdvara eller i en molntjänst för signering, så en PFX i en repositoryhemlighet är ändå inte ett alternativ. I praktiken betyder det en självhostad runner med token eller hårdvarumodul ansluten, eller en värdbaserad runner som autentiserar mot en signeringstjänst och aldrig håller nyckeln.
Felsignaturen att känna igen är 0x8007000B på signeringssteget. Den betyder oftast att värdet Publisher i manifestet inte exakt matchar ämnet i signeringscertifikatet. Inte ungefär, exakt, inklusive skiljetecken och mellanrum. Feltexten säger inget om utgivare, vilket är varför den kostar en eftermiddag första gången.
Notera att sign är ett av verben med mervärde snarare än ett av de makeappx-kompatibla, så det kräver en aktiv EtherApps Forge-licens. Detsamma gäller create, test, assess, cert--verben och dev--verben.
Värdbaserade runners mot självhostade
På värdbaserade runners är vinsten att slippa installera en SDK på flera gigabyte vid varje ren körning, vilket är tid vid varje enskilt bygge.
På självhostade runners är vinsten en annan: din avbildning slutar bära SDK:n, och slutar vara känslig för vilken SDK-version den bär. Två runners med olika SDK-versioner kan ge olika paketeringsbeteende, vilket hör till de minst njutbara klasserna av byggfel att diagnostisera.
Det finns en styrningsaspekt också. Ett utvecklingskit på byggmiljön är en stor yta att försvara inför den som godkänner vad som hamnar på de maskinerna, och det gör ett jobb: förvandlar en mapp till ett paket.
Bor din pipeline i Azure DevOps snarare än GitHub Actions gäller samma resonemang oförändrat. Att bygga och signera MSIX i CI/CD täcker motsvarande steg där, inklusive verben dev-register och dev-test som kortar den lokala slingan innan något når en pipeline.
Att publicera resultatet
När paket väl produceras och signeras av pipelinen är distributionen den återstående frågan. make-appinstaller genererar en App Installer-uppdateringsfeed: publicera paketet och feeden, så kontrollerar befintliga installationer den efter nya versioner. För en ISV som levererar utanför en hanterad miljö är det ofta hela distributionshistorien.
Två detaljer avgör om det fungerar första gången. Feeden och paketet måste levereras över HTTPS med rätt innehållstyper, för en server som skickar tillbaka .appinstaller eller .msix som en generisk binär ström ger en nedladdningsfråga i stället för en installation. Och uppdateringsbeteendet sätts i själva feeden, inklusive hur ofta klienter kontrollerar och om en uppdatering tillämpas vid start eller i bakgrunden.
Båda är lätta att få rätt en gång och mycket irriterande att diagnostisera utifrån en användares beskrivning, vilket är ett argument för att validera den publicerade feeden från en ren maskin som en del av releasen.
Var EtherApps Forge passar in
forge_msix är kommandoradsytan för EtherApps Forge, en Windows-skrivbordsapplikation snarare än en värdbaserad tjänst, med en kostnadsfri provperiod på 7 dagar. Det spelar roll för en byggpipeline: verktyget körs på infrastruktur du styr och paketen lämnar den aldrig.
Det bredare sammanhanget finns på rutten MSIX-paketering för utvecklare och ISV:er, som förutsätter vad den här artikeln förutsätter: en frisk byggmapp, inget att fånga in, och all friktion efter att bygget lyckats. Det är ett annat problem än det miljöomfattande, där en applikation dyker upp utan installationsprogram och måste byggas om från en körande maskin. Är det din situation är MSIX-paketering och utrullning rutten och att konvertera en EXE till MSIX täcker den applikation för applikation. Versionsanteckningarna för 1.0.6 förklarar varifrån funktionerna för utvecklare och ISV:er kom.
Prova det mot ditt eget arbetsflöde
Det ärliga testet är om ditt befintliga paketeringssteg fortsätter fungera när du byter namnet på det körbara programmet. Ta en gren, byt makeappx mot forge_msix i stegen pack och validate, och se om jobbet blir grönt utan att du rör något annat. Om det gör det bar SDK-beroendet på din agent aldrig sin vikt.
Utforska MSIX-paketering för utvecklare och ISV:er
Paketeringsmotorn finns redan på runnern; enda frågan är vad du installerar för att nå den.
