Si ha intentado producir un paquete MSIX en un runner Windows de GitHub Actions, probablemente haya escrito un paso que va rebuscando por C:\Program Files (x86)\Windows Kits\10\bin en busca de makeappx.exe, porque su ruta contiene un número de versión del SDK que cambia cuando cambia la imagen del runner.

Ese paso funciona hasta que la imagen se actualiza. Entonces se rompe, y se rompe de una forma que parece un fallo de empaquetado en lugar de un fallo de ruta.

Hay una vía más limpia, y se reduce a un detalle sobre dónde vive realmente el empaquetado MSIX.

Lo que conviene saber

El empaquetado MSIX no es algo que haga el Windows SDK. El motor AppxPackaging es un componente del propio Windows. makeappx.exe es un envoltorio de línea de comandos alrededor de ese motor que da la casualidad de que se distribuye dentro del SDK.

Así que una herramienta puede llamar al mismo componente del sistema operativo directamente, sin ningún SDK en la máquina. Eso es lo que hace forge_msix. Es un sustituto directo de makeappx.exe: los mismos verbos, los mismos modificadores, las mismas expectativas sobre entrada y salida. Si su workflow llama actualmente a makeappx pack, cambia el nombre del ejecutable y el paso sigue funcionando.

Los verbos compatibles con makeappx son pack, unpack, bundle, unbundle, validate y make-pri. Entre ellos cubren el lado SDK de un trabajo de empaquetado normal: construir un paquete a partir de una carpeta, desarmar uno para ver qué entró, combinar paquetes por arquitectura en un bundle y construir el índice de recursos que el manifiesto espera.

Diagrama de flujo de una compilación MSIX en un runner Windows de GitHub Actions sin Windows SDK instalado: checkout y dotnet publish producen una carpeta de compilación que contiene un manifiesto de app, forge_msix pack llama al motor Windows AppxPackaging ya presente en el runner para convertir esa carpeta en un paquete MSIX, forge_msix validate frena el trabajo devolviendo el código de salida 2 cuando el paquete se analiza y se considera inválido, forge_msix sign aplica una firma con marca de tiempo desde un certificado que el runner puede alcanzar, y el paquete firmado se sube como artefacto del workflow y se publica a través de un feed de actualización de App Installer que los clientes instalados consultan en busca de versiones nuevas.

Compilar, empaquetar, validar, firmar, publicar, con el motor de empaquetado viniendo de Windows en lugar de una instalación del SDK.

Llevar la herramienta al runner

La CLI se distribuye con EtherApps Forge, así que ponerla en un runner es una instalación de herramienta corriente. Deje el ejecutable en algún punto del disco y añada esa carpeta a GITHUB_PATH para que los pasos posteriores puedan llamarlo por su nombre:

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

En un runner alojado eso significa un paso de instalación al principio del trabajo, cacheado como cualquier otra cosa. En un runner autoalojado significa instalar una vez cuando construye la imagen. En cualquier caso está aprovisionando un único ejecutable de línea de comandos en lugar de un kit de desarrollo, que es de lo que se trata.

Los runners Windows usan pwsh por defecto para los pasos run:, así que los ejemplos siguientes asumen PowerShell en lugar de cmd.

Cómo es el paso del workflow

La gracia está en lo poco llamativo que es.

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

Sin paso de instalación del SDK. Sin descubrimiento de rutas. Sin fijar versiones contra la imagen del runner.

Tres cosas hacen tropezar de forma fiable la primera ejecución de ese paso.

El directorio de contenido necesita un AppxManifest.xml en su raíz. Una salida de publicación simple no lo tiene salvo que su proyecto lo produzca, y pack rechaza una carpeta sin él. Este es el primer fallo más común, y se lee como un problema de herramientas cuando es un problema de configuración del proyecto.

Volver a ejecutar sobre una salida existente falla salvo que pase /o. En un runner alojado limpio nunca ve esto. En un runner autoalojado con un espacio de trabajo persistente lo ve en la segunda compilación, que es cuando ha dejado de mirar.

Las cadenas de recursos necesitan un resources.pri. Si su manifiesto se refiere a valores ms-resource: y no se construyó ningún índice de recursos, el paquete puede instalarse y luego mostrar nombres en blanco en el menú Inicio. make-pri construye ese índice, y le corresponde ir antes de pack.

Si prefiere no colocar archivos en una carpeta solo para dar forma al paquete, use un archivo de mapeo en lugar de /d:

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

Entonces forge_msix pack /f .\mapping.txt /p .\artifacts\ContosoApp.msix construye exactamente lo que ha listado y nada más. Cuando no está seguro de qué entró, forge_msix unpack /p .\artifacts\ContosoApp.msix /d .\inspect le devuelve el paquete como una carpeta que puede comparar.

Una cosa más que pertenece a la pipeline en lugar de a la cabeza de una persona: la versión del paquete viene del manifiesto, no del comando de empaquetado. Estámpela desde la compilación antes de empaquetar, y mantenga el cuarto campo a cero porque esa posición está reservada.

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

Las actualizaciones solo se aplican cuando la versión aumenta, así que una pipeline que entrega dos veces la misma versión produce una actualización que nadie recibe y ningún error que explique por qué.

Que falle la compilación, no la entrega

validate devuelve un código de salida distinto de cero cuando el paquete no es válido, que es lo que permite que el trabajo falle en el punto del problema en lugar de en el despliegue.

Un detalle que merece la pena manejar: el código de salida 2 significa que el paquete se analizó correctamente y es inválido, a diferencia de que la herramienta no haya podido ejecutarse. Tratar esos dos casos igual produce una compilación en rojo confusa. Tratarlos de forma distinta le da un control que le dice algo útil.

Leer ese código de salida exige más cuidado de lo que parece, porque GitHub Actions ejecuta los pasos pwsh con $ErrorActionPreference puesto en stop, y las versiones recientes de PowerShell aplican esa preferencia también a los comandos nativos. El paso puede por tanto terminar en el código distinto de cero antes de que su comparación se ejecute. Desactive eso para el paso e inspeccione el código usted mismo:

      - 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 distinción importa más en una pipeline compartida que en la suya propia. "El paquete está mal" es un mensaje sobre el que quien lo rompió puede actuar. "El paso de empaquetado falló" lo manda a los registros del runner a averiguar cuál de las dos cosas ocurrió.

Firmar en una pipeline

Empaquetar sin firmar produce un artefacto que no puede instalar en un dispositivo gestionado, así que el paso de firma pertenece al mismo trabajo.

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

Dos cosas que importan más de lo que parecen:

Firme por huella digital desde el almacén de certificados, en lugar de pasar un PFX y una contraseña a través de variables del workflow. La clave privada se queda donde le corresponde.

Ponga siempre marca de tiempo, usando un servidor RFC 3161. Sin ella, el paquete deja de validar en el momento en que caduca el certificado de firma, aunque la firma se creara legítimamente mientras el certificado estaba vigente. Todos los paquetes que haya entregado se vuelven no instalables el mismo día. Con marca de tiempo, las firmas sobreviven al certificado.

Ese primer punto decide dónde se ejecuta la firma. Un runner alojado es efímero y no tiene un almacén de certificados digno de ese nombre, y desde que el CA/Browser Forum endureció sus requisitos de firma de código en junio de 2023, las claves de firma de confianza pública tienen que vivir en hardware certificado o en un servicio de firma en la nube, así que un PFX en un secreto del repositorio no es una opción de todos modos. En la práctica eso significa un runner autoalojado con el token o el módulo de hardware conectado, o un runner alojado que se autentica contra un servicio de firma y nunca guarda la clave.

La firma de fallo que hay que reconocer es 0x8007000B en el paso de firma. Suele significar que el valor Publisher del manifiesto no coincide exactamente con el subject del certificado de firma. No aproximadamente, exactamente, incluidos la puntuación y los espacios. El texto del error no dice nada sobre editores, que es por lo que cuesta una tarde la primera vez.

Tenga en cuenta que sign es uno de los verbos de valor añadido y no uno de los compatibles con makeappx, así que necesita una licencia activa de EtherApps Forge. Lo mismo se aplica a create, test, assess, los verbos cert- y los verbos dev-.

Runners alojados frente a autoalojados

En los runners alojados la ganancia es no tener que instalar un SDK de varios gigabytes en cada ejecución limpia, que es tiempo en cada una de las compilaciones.

En los runners autoalojados la ganancia es distinta: su imagen deja de cargar con el SDK, y deja de ser sensible a qué versión del SDK carga. Dos runners con versiones distintas del SDK pueden producir comportamientos de empaquetado distintos, que está entre las clases de fallo de compilación menos agradables de diagnosticar.

Hay también un ángulo de gobernanza. Un kit de desarrollo en la infraestructura de compilación es una superficie grande que justificar ante quien aprueba lo que entra en esas máquinas, y está haciendo un solo trabajo: convertir una carpeta en un paquete.

Si su pipeline vive en Azure DevOps en lugar de GitHub Actions, el mismo razonamiento se aplica sin cambios. Construir y firmar MSIX en CI/CD cubre las etapas equivalentes allí, incluidos los verbos dev-register y dev-test que acortan el ciclo local antes de que nada llegue a una pipeline.

Publicar el resultado

Una vez que la pipeline produce y firma los paquetes, la distribución es la pregunta que queda. make-appinstaller genera un feed de actualización de App Installer: publique el paquete y el feed, y las instalaciones existentes lo consultan en busca de versiones nuevas. Para un ISV que entrega fuera de un estate gestionado, esa es a menudo toda la historia de distribución.

Dos detalles deciden si funciona a la primera. El feed y el paquete deben servirse por HTTPS con los tipos de contenido correctos, porque un servidor que devuelve .appinstaller o .msix como un flujo binario genérico produce un aviso de descarga en lugar de una instalación. Y el comportamiento de actualización se fija en el propio feed, incluidos cada cuánto consultan los clientes y si una actualización se aplica al arrancar o en segundo plano.

Ambas son fáciles de acertar una vez y muy molestas de diagnosticar a partir de la descripción de un usuario, lo cual es un argumento para validar el feed publicado desde una máquina limpia como parte de la entrega.

Dónde encaja EtherApps Forge

forge_msix es la superficie de línea de comandos de EtherApps Forge, una aplicación de escritorio de Windows y no un servicio alojado, con una prueba gratuita de 7 días. Eso importa para una pipeline de compilación: la herramienta se ejecuta en infraestructura que usted controla y los paquetes nunca salen de ella.

El contexto más amplio está en la ruta de empaquetado MSIX para desarrolladores e ISVs, que asume lo que asume este artículo: una carpeta de compilación sana, nada que capturar, y toda la fricción después de que la compilación tenga éxito. Ese es un problema distinto del que abarca todo el estate, donde una aplicación llega sin instalador y hay que reconstruirla desde una máquina en ejecución. Si esa es su situación, empaquetado y despliegue MSIX es la ruta y convertir un EXE a MSIX lo cubre aplicación por aplicación. Las notas de la versión 1.0.6 explican de dónde salieron las capacidades para desarrolladores e ISVs.

Pruébelo contra su propio workflow

La prueba honesta es si su paso de empaquetado existente sigue funcionando cuando cambia el nombre del ejecutable. Coja una rama, cambie makeappx por forge_msix en los pasos de pack y validate, y vea si el trabajo se pone en verde sin tocar nada más. Si lo hace, la dependencia del SDK en su agente nunca estuvo justificando su peso.

Explore el empaquetado MSIX para desarrolladores e ISVs

El motor de empaquetado ya está en el runner; la única pregunta es qué está instalando para alcanzarlo.