Si vous avez déjà tenté de produire un package MSIX sur un runner Windows GitHub Actions, vous avez probablement écrit une étape qui part fouiller C:\Program Files (x86)\Windows Kits\10\bin à la recherche de makeappx.exe, parce que son chemin contient un numéro de version du SDK qui change quand l'image du runner change.

Cette étape fonctionne jusqu'à la mise à jour de l'image. Puis elle casse, et elle casse d'une manière qui ressemble à un échec de packaging plutôt qu'à un échec de chemin.

Il existe une voie plus propre, et elle tient à un détail sur l'endroit où réside réellement le packaging MSIX.

Le point qui mérite d'être connu

Le packaging MSIX n'est pas quelque chose que fait le Windows SDK. Le moteur AppxPackaging est un composant de Windows lui-même. makeappx.exe est une enveloppe en ligne de commande autour de ce moteur, qui se trouve être livrée dans le SDK.

Un outil peut donc appeler directement le même composant du système d'exploitation, sans aucun SDK sur la machine. C'est ce que fait forge_msix. C'est un remplacement direct de makeappx.exe : mêmes verbes, mêmes commutateurs, mêmes attentes en matière d'entrée et de sortie. Si votre workflow appelle aujourd'hui makeappx pack, vous changez le nom de l'exécutable et l'étape continue de fonctionner.

Les verbes compatibles makeappx sont pack, unpack, bundle, unbundle, validate et make-pri. À eux tous, ils couvrent tout le volet SDK d'un travail de packaging normal : construire un package à partir d'un dossier, en démonter un pour voir ce qui y est réellement entré, combiner des packages par architecture en un bundle, et construire l'index de ressources attendu par le manifeste.

Diagramme de flux d'un build MSIX sur un runner Windows GitHub Actions sans Windows SDK installé : checkout et dotnet publish produisent un dossier de build contenant un manifeste d'application, forge_msix pack appelle le moteur Windows AppxPackaging déjà présent sur le runner pour transformer ce dossier en package MSIX, forge_msix validate verrouille le job en renvoyant le code de sortie 2 lorsque le package est analysé et jugé non valide, forge_msix sign applique une signature horodatée depuis un certificat que le runner peut atteindre, et le package signé est téléversé comme artefact de workflow puis publié via un flux de mise à jour App Installer que les clients installés consultent pour de nouvelles versions.

Générer, packager, valider, signer, publier, avec le moteur de packaging venant de Windows plutôt que d'une installation du SDK.

Mettre l'outil sur le runner

La CLI est livrée avec EtherApps Forge, donc la poser sur un runner est une installation d'outil ordinaire plutôt qu'une installation de kit de développement. Déposez l'exécutable quelque part sur le disque et ajoutez ce dossier à GITHUB_PATH pour que chaque étape ultérieure puisse l'appeler par son nom :

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

Sur un runner hébergé, cela signifie une étape d'installation en tête du job, que vous pouvez mettre en cache comme n'importe quoi d'autre. Sur un runner auto-hébergé, cela signifie une installation unique au moment de construire l'image, puis plus d'y penser. Dans les deux cas, ce que vous provisionnez est un unique exécutable en ligne de commande, et c'est cette différence qui rend le reste utile.

Les runners Windows utilisent pwsh par défaut pour les étapes run:, donc les exemples ci-dessous supposent une sémantique PowerShell plutôt que cmd.

À quoi ressemble l'étape du workflow

Tout l'intérêt tient à quel point elle est banale.

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

Aucune étape d'installation du SDK. Aucune découverte de chemin. Aucun épinglage de version par rapport à l'image du runner.

Trois choses font régulièrement trébucher la première exécution de cette étape.

Le répertoire de contenu a besoin d'un AppxManifest.xml à sa racine. Une simple sortie de publication n'en contient pas, sauf si votre projet le produit, et pack refusera un dossier qui en est dépourvu. C'est l'échec initial le plus courant et il se lit comme un problème d'outillage alors que c'est un problème de configuration de projet.

Relancer le job sur une sortie existante échoue à moins de passer /o. Sur un runner hébergé propre, vous ne voyez jamais cela. Sur un runner auto-hébergé avec un espace de travail persistant, vous le voyez au deuxième build, c'est-à-dire exactement quand vous avez cessé de regarder.

Les chaînes de ressources ont besoin d'un resources.pri. Si votre manifeste fait référence à des valeurs ms-resource: et qu'aucun index de ressources n'a été construit, le package peut s'installer puis afficher des noms vides dans le menu Démarrer. make-pri construit cet index, et sa place est avant pack plutôt qu'après le rapport de bogue.

Si vous préférez ne pas copier des fichiers dans un dossier de préparation juste pour donner sa forme au package, utilisez un fichier de mappage au lieu de /d :

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

Ensuite, forge_msix pack /f .\mapping.txt /p .\artifacts\ContosoApp.msix construit exactement ce que vous avez listé et rien d'autre. Quand vous ne savez pas trop ce qui y est réellement entré, forge_msix unpack /p .\artifacts\ContosoApp.msix /d .\inspect vous rend le package sous forme de dossier que vous pouvez comparer, ce qui transforme « pourquoi ce fichier est-il là » en une question de trente secondes.

Encore une chose dont la place est dans le pipeline plutôt que dans la tête de quelqu'un : la version du package. Elle vient du manifeste, pas de la commande de packaging, donc estampillez-la depuis le build avant de packager et gardez le quatrième champ à zéro, car cette position est réservée.

$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))

Les mises à jour ne s'appliquent que si la version augmente, donc un pipeline qui livre deux fois la même version produit une mise à jour que personne ne reçoit et aucune erreur pour expliquer pourquoi.

Faire échouer le build, pas la mise en production

validate renvoie un code de sortie non nul lorsque le package n'est pas valide, ce qui permet au job d'échouer au point du problème plutôt qu'au déploiement.

Un détail mérite d'être traité : le code de sortie 2 signifie que le package a été analysé avec succès et qu'il n'est pas valide, par opposition à l'outil qui n'a pas réussi à s'exécuter. Traiter ces deux cas de façon identique produit un build rouge déroutant. Les traiter différemment vous donne un verrou qui vous apprend quelque chose d'utile.

Lire le code de sortie dans une étape PowerShell demande un peu plus de soin qu'il n'y paraît, car GitHub Actions exécute les étapes pwsh avec $ErrorActionPreference réglé sur stop, et les versions récentes de PowerShell appliquent cette préférence aux commandes natives également. L'étape peut donc se terminer sur le code non nul avant même que votre propre comparaison ne s'exécute. Désactivez ce comportement pour l'étape et inspectez le code vous-même :

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

La distinction compte davantage sur un pipeline partagé que sur le vôtre. « Le package est incorrect » est un message sur lequel la personne qui l'a cassé peut agir. « L'étape de packaging a échoué » l'envoie dans les journaux du runner pour déterminer laquelle des deux choses s'est produite, et c'est l'impôt que vous payez sur chaque build ensuite.

Signer dans un pipeline

Packager sans signer produit un artefact que vous ne pouvez pas installer sur un poste managé, donc l'étape de signature a sa place dans le même job.

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

Deux points qui comptent plus qu'il n'y paraît :

Signez par empreinte depuis le magasin de certificats, plutôt que de passer un PFX et un mot de passe par des variables de workflow. La clé privée reste là où elle doit être.

Horodatez toujours, avec un serveur RFC 3161. Sans cela, le package cesse d'être validé dès l'expiration du certificat de signature, même si la signature a été créée légitimement pendant que le certificat était actif. Chaque package que vous avez livré devient impossible à installer le même jour. Avec un horodatage, les signatures survivent au certificat.

Ce premier point a une conséquence pratique sur l'endroit où la signature s'exécute. Un runner hébergé est éphémère et n'a pas de magasin de certificats digne de ce nom, et depuis que le CA/Browser Forum a durci ses exigences de signature de code en juin 2023, les clés de signature de code publiquement approuvées doivent résider sur du matériel certifié ou dans un service de signature cloud, donc un PFX dans un secret de dépôt n'est pas une option pour un certificat public, même si vous le vouliez. En pratique, cela signifie soit un runner auto-hébergé avec le jeton ou le module matériel raccordé, soit un runner hébergé qui s'authentifie auprès d'un service de signature et ne détient jamais la clé. Décider tôt lequel des deux vous appliquez évite de reconstruire le job plus tard.

La signature d'échec à reconnaître est 0x8007000B sur l'étape de signature. Elle signifie généralement que la valeur Publisher du manifeste ne correspond pas exactement au sujet du certificat de signature. Pas approximativement, exactement, ponctuation et espacement compris. Le texte de l'erreur ne dit rien des éditeurs, ce qui coûte un après-midi la première fois.

Notez que sign fait partie des verbes à valeur ajoutée plutôt que des verbes compatibles makeappx, il nécessite donc une licence EtherApps Forge active. Il en va de même pour create, test, assess, les verbes cert- et les verbes dev-.

Runners hébergés contre auto-hébergés

Sur les runners hébergés, le gain est de ne pas avoir à installer un SDK de plusieurs gigaoctets à chaque exécution propre, ce qui représente du temps sur chaque build.

Sur les runners auto-hébergés, le gain est différent : votre image cesse de porter le SDK, et cesse d'être sensible à la version du SDK qu'elle porte. Deux runners avec des versions de SDK différentes peuvent produire des comportements de packaging différents, ce qui compte parmi les classes d'échec de build les moins agréables à diagnostiquer.

Il y a aussi un angle de gouvernance. Un kit de développement sur une infrastructure de build est une large surface à justifier auprès de celui qui approuve ce qui atterrit sur ces machines, et il fait un seul travail : transformer un dossier en package. Le remplacer par un unique exécutable rend cette conversation courte, et fait de l'image du runner quelque chose que vous pouvez reconstruire de zéro sans une longue liste de courses.

Si votre pipeline vit dans Azure DevOps ou ailleurs plutôt que dans GitHub Actions, le même raisonnement s'applique tel quel. Générer et signer des MSIX en CI/CD couvre les étapes équivalentes là-bas, y compris les verbes dev-register et dev-test qui raccourcissent la boucle locale avant que quoi que ce soit n'atteigne un pipeline.

Publier le résultat

Une fois les packages produits et signés par le pipeline, la distribution est la question restante. make-appinstaller génère un flux de mise à jour App Installer : publiez le package et le flux, et les installations existantes y cherchent de nouvelles versions. Pour un ISV qui livre en dehors d'un parc managé, c'est souvent toute l'histoire de la distribution.

Deux détails décident si cela fonctionne du premier coup. Le flux et le package doivent être servis en HTTPS avec les bons types de contenu, car un serveur web qui renvoie .appinstaller ou .msix comme un flux binaire générique produit une invite de téléchargement plutôt qu'une installation. Et le comportement de mise à jour se règle dans le flux lui-même, y compris la fréquence à laquelle les clients installés vérifient et si une mise à jour s'applique au lancement ou en arrière-plan, c'est donc une décision à prendre délibérément plutôt qu'à subir par défaut.

Ces deux points sont faciles à régler une bonne fois et très pénibles à diagnostiquer d'après la description d'un utilisateur, ce qui plaide pour valider le flux publié depuis une machine propre dans le cadre de la mise en production plutôt que de supposer que cela a marché.

La place d'EtherApps Forge

forge_msix est la surface en ligne de commande d'EtherApps Forge, qui est une application de bureau Windows plutôt qu'un service hébergé, avec un essai gratuit de 7 jours. Cela compte pour un pipeline de build, car l'outil s'exécute sur une infrastructure que vous contrôlez et les packages n'en sortent jamais.

Le contexte plus large se trouve sur la route packaging MSIX pour les développeurs et les ISV, qui suppose ce que suppose cet article : il existe un dossier de build sain, rien n'a besoin d'être capturé, et la friction est tout ce qui se passe après la réussite du build. C'est un problème différent de celui à l'échelle du parc, où une application arrive sans installateur et doit être reconstruite depuis une machine en fonctionnement. Si c'est plutôt votre cas, packaging et déploiement MSIX est la route, et convertir un EXE en MSIX couvre le terrain application par application. Les notes de version 1.0.6 expliquent d'où viennent les capacités développeurs et ISV.

Essayez-le sur votre propre workflow

Le vrai test est de savoir si votre étape de packaging existante continue de fonctionner quand vous changez le nom de l'exécutable. Prenez une branche, remplacez makeappx par forge_msix dans les étapes pack et validate, et voyez si le job passe au vert sans rien toucher d'autre. Si c'est le cas, la dépendance au SDK sur votre agent ne valait pas son poids. Sinon, l'échec pointera vers quelque chose de précis, ce qui est déjà plus que ce que la version fouilleuse de chemins de cette étape vous a jamais donné.

Découvrez le packaging MSIX pour les développeurs et les ISV

Le moteur de packaging est déjà sur le runner ; la seule question est ce que vous installez pour l'atteindre.