Se avete provato a produrre un pacchetto MSIX su un runner Windows di GitHub Actions, probabilmente avete scritto uno step che va a caccia dentro C:\Program Files (x86)\Windows Kits\10\bin alla ricerca di makeappx.exe, perché il suo percorso contiene un numero di versione dell'SDK che cambia quando cambia l'immagine del runner.

Quello step funziona finché l'immagine non si aggiorna. Poi si rompe, e si rompe in un modo che sembra un fallimento di packaging anziché un fallimento di percorso.

C'è una strada più pulita, e si riduce a un dettaglio su dove risieda davvero il packaging MSIX.

Il dettaglio che vale la pena conoscere

Il packaging MSIX non è una cosa che fa il Windows SDK. Il motore AppxPackaging è un componente di Windows stesso. makeappx.exe è un wrapper da riga di comando attorno a quel motore, che si dà il caso venga distribuito dentro l'SDK.

Quindi uno strumento può richiamare direttamente lo stesso componente del sistema operativo, senza alcun SDK sulla macchina. È ciò che fa forge_msix. È un sostituto diretto di makeappx.exe: stessi verbi, stessi switch, stesse aspettative su input e output. Se il vostro workflow oggi chiama makeappx pack, cambiate il nome dell'eseguibile e lo step continua a funzionare.

I verbi compatibili con makeappx sono pack, unpack, bundle, unbundle, validate e make-pri. Insieme coprono tutto il lato SDK di un normale lavoro di packaging: costruire un pacchetto da una cartella, smontarne uno per vedere che cosa ci sia finito davvero, combinare pacchetti per architettura in un bundle e costruire l'indice delle risorse che il manifesto si aspetta.

Diagramma di flusso di una build MSIX su un runner Windows di GitHub Actions senza Windows SDK installato: il checkout e dotnet publish producono una cartella di build contenente un manifesto dell'app, forge_msix pack richiama il motore Windows AppxPackaging già presente sul runner per trasformare quella cartella in un pacchetto MSIX, forge_msix validate blocca il job restituendo il codice di uscita 2 quando il pacchetto viene analizzato e risulta non valido, forge_msix sign applica una firma con marca temporale da un certificato che il runner può raggiungere, e il pacchetto firmato viene caricato come artefatto del workflow e pubblicato tramite un feed di aggiornamento App Installer che i client installati controllano per le nuove versioni.

Build, pack, validate, sign, publish, con il motore di packaging che arriva da Windows anziché da un'installazione dell'SDK.

Portare lo strumento sul runner

La CLI è distribuita con EtherApps Forge, quindi metterla su un runner è un'installazione di strumento ordinaria anziché l'installazione di un kit di sviluppo. Lasciate l'eseguibile da qualche parte su disco e aggiungete quella cartella a GITHUB_PATH, così ogni step successivo può richiamarlo per nome:

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

Su un runner ospitato questo significa uno step di installazione in cima al job, che potete mettere in cache come qualsiasi altra cosa. Su un runner self-hosted significa installarla una volta quando costruite l'immagine e poi non pensarci più. In entrambi i casi ciò che state predisponendo è un singolo eseguibile da riga di comando, ed è questa la differenza che rende sensato tutto il resto.

I runner Windows usano pwsh per impostazione predefinita negli step run:, quindi gli esempi qui sotto presuppongono la semantica di PowerShell anziché quella di cmd.

Che aspetto ha lo step del workflow

Il punto è quanto sia poco notevole.

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

Nessuno step di installazione dell'SDK. Nessuna ricerca di percorsi. Nessun aggancio a una versione dell'immagine del runner.

Tre cose fanno regolarmente inciampare la prima esecuzione di quello step.

La directory dei contenuti ha bisogno di un AppxManifest.xml nella sua radice. Un semplice output di publish non ne ha uno a meno che il vostro progetto non lo produca, e pack rifiuterà una cartella che ne è priva. È il primo fallimento più comune e si legge come un problema di strumenti quando è un problema di configurazione del progetto.

Rieseguire il job su un output esistente fallisce a meno che non passiate /o. Su un runner ospitato pulito non lo vedrete mai. Su un runner self-hosted con un workspace persistente lo vedrete alla seconda build, che è esattamente quando avete smesso di guardare.

Le stringhe di risorse hanno bisogno di un resources.pri. Se il vostro manifesto fa riferimento a valori ms-resource: e non è stato costruito alcun indice delle risorse, il pacchetto può installarsi e poi mostrare nomi vuoti nel menu Start. make-pri costruisce quell'indice, e va messo prima di pack anziché dopo la segnalazione del bug.

Se preferite non copiare file in una cartella di staging solo per dare forma al pacchetto, usate un file di mapping anziché /d:

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

Poi forge_msix pack /f .\mapping.txt /p .\artifacts\ContosoApp.msix costruisce esattamente ciò che avete elencato e nient'altro. Quando non siete sicuri di che cosa ci sia finito davvero, forge_msix unpack /p .\artifacts\ContosoApp.msix /d .\inspect vi restituisce il pacchetto come cartella su cui fare diff, il che trasforma «perché quel file è lì dentro» in una domanda da trenta secondi.

Un'altra cosa che appartiene alla pipeline anziché alla testa di una persona: la versione del pacchetto. Arriva dal manifesto, non dal comando di packaging, quindi timbratela dalla build prima di impacchettare e tenete il quarto campo a zero, dato che quella posizione è riservata.

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

Gli aggiornamenti si applicano solo quando la versione aumenta, quindi una pipeline che rilascia due volte la stessa versione produce un aggiornamento che nessuno riceve e nessun errore che ne spieghi il motivo.

Fate fallire la build, non il rilascio

validate restituisce un codice di uscita diverso da zero quando il pacchetto non è valido, ed è questo che permette al job di fallire nel punto del problema anziché al momento della distribuzione.

Un dettaglio da gestire: il codice di uscita 2 significa che il pacchetto è stato analizzato correttamente e non è valido, a differenza dello strumento che non riesce a eseguirsi. Trattare quei due casi allo stesso modo produce una build rossa che confonde. Trattarli in modo diverso vi dà un controllo che dice qualcosa di utile.

Leggere il codice di uscita in uno step PowerShell richiede un po' più di attenzione di quanto sembri, perché GitHub Actions esegue gli step pwsh con $ErrorActionPreference impostato su stop, e le versioni recenti di PowerShell applicano quella preferenza anche ai comandi nativi. Lo step può quindi terminare sull'uscita diversa da zero prima che il vostro confronto venga eseguito. Disattivate quel comportamento per lo step e ispezionate voi stessi il codice:

      - 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 distinzione conta più su una pipeline condivisa che sulla vostra. «Il pacchetto è sbagliato» è un messaggio su cui chi lo ha rotto può agire. «Lo step di packaging è fallito» lo manda nei log del runner a capire quale delle due cose sia successa, ed è la tassa che pagate su ogni build successiva.

Firmare in una pipeline

Impacchettare senza firmare produce un artefatto che non potete installare su un dispositivo gestito, quindi lo step di firma appartiene allo stesso job.

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

Due cose che contano più di quanto sembri:

Firmate per impronta digitale dall'archivio certificati, anziché passare un PFX e una password attraverso le variabili del workflow. La chiave privata resta dove le compete.

Applicate sempre una marca temporale, usando un server RFC 3161. Senza, il pacchetto smette di essere valido nel momento in cui il certificato di firma scade, anche se la firma è stata creata legittimamente mentre il certificato era attivo. Ogni pacchetto che avete mai rilasciato diventa non installabile lo stesso giorno. Con una marca temporale, le firme sopravvivono al certificato.

Quel primo punto ha una conseguenza pratica su dove viene eseguita la firma. Un runner ospitato è effimero e non ha un archivio certificati degno di questo nome, e da quando il CA/Browser Forum ha irrigidito i suoi requisiti per la firma del codice a giugno 2023 le chiavi di firma del codice pubblicamente attendibili devono risiedere su hardware certificato o in un servizio di firma cloud, quindi un PFX in un segreto del repository non è un'opzione per un certificato pubblico, anche se lo voleste. In pratica significa o un runner self-hosted con il token o il modulo hardware collegato, oppure un runner ospitato che si autentica a un servizio di firma e non detiene mai la chiave. Decidere presto quale delle due state facendo vi risparmia di ricostruire il job dopo.

La firma di fallimento da riconoscere è 0x8007000B sullo step di firma. Di solito significa che il valore Publisher nel manifesto non corrisponde esattamente al soggetto del certificato di firma. Non approssimativamente, esattamente, punteggiatura e spaziatura comprese. Il testo dell'errore non dice nulla sugli editori, ed è per questo che la prima volta costa un pomeriggio a chiunque.

Notate che sign è uno dei verbi a valore aggiunto anziché uno di quelli compatibili con makeappx, quindi richiede una licenza EtherApps Forge attiva. Lo stesso vale per create, test, assess, i verbi cert- e i verbi dev-.

Runner ospitati contro self-hosted

Sui runner ospitati il vantaggio è non dover installare un SDK da diversi gigabyte a ogni esecuzione pulita, che è tempo su ogni singola build.

Sui runner self-hosted il vantaggio è diverso: la vostra immagine smette di portarsi dietro l'SDK, e smette di essere sensibile a quale versione dell'SDK si porta dietro. Due runner con versioni diverse dell'SDK possono produrre comportamenti di packaging diversi, che è fra le classi meno piacevoli di fallimento di build da diagnosticare.

C'è anche un aspetto di governance. Un kit di sviluppo sull'infrastruttura di build è una superficie ampia da giustificare a chi approva ciò che finisce su quelle macchine, e sta facendo un lavoro solo: trasformare una cartella in un pacchetto. Sostituirlo con un singolo eseguibile rende breve quella conversazione, e rende l'immagine del runner qualcosa che potete ricostruire da zero senza una lunga lista della spesa.

Se la vostra pipeline vive in Azure DevOps o altrove anziché in GitHub Actions, lo stesso ragionamento si applica invariato. Costruire e firmare MSIX in CI/CD copre le fasi equivalenti là, compresi i verbi dev-register e dev-test che accorciano il ciclo locale prima che qualcosa arrivi a una pipeline.

Pubblicare il risultato

Una volta che i pacchetti sono prodotti e firmati dalla pipeline, resta la questione della distribuzione. make-appinstaller genera un feed di aggiornamento App Installer: pubblicate il pacchetto e il feed, e le installazioni esistenti lo controllano per le nuove versioni. Per un ISV che rilascia fuori da un parco gestito, spesso è tutta la storia della distribuzione.

Due dettagli decidono se funziona al primo tentativo. Il feed e il pacchetto devono essere serviti su HTTPS con i tipi di contenuto corretti, perché un server web che restituisce .appinstaller o .msix come flusso binario generico produce una richiesta di download anziché un'installazione. E il comportamento di aggiornamento si imposta nel feed stesso, compresi la frequenza con cui i client installati controllano e se un aggiornamento viene applicato all'avvio o in background, quindi è una decisione da prendere deliberatamente anziché accettare per impostazione predefinita.

Entrambe sono facili da azzeccare una volta e molto fastidiose da diagnosticare dalla descrizione di un utente, il che è un buon argomento per validare il feed pubblicato da una macchina pulita come parte del rilascio anziché confidare che abbia funzionato.

Dove si inserisce EtherApps Forge

forge_msix è la superficie da riga di comando di EtherApps Forge, che è un'applicazione desktop per Windows anziché un servizio ospitato, con una prova gratuita di 7 giorni. Per una pipeline di build questo conta, perché lo strumento viene eseguito su infrastruttura che controllate e i pacchetti non la lasciano mai.

Il contesto più ampio è sul percorso packaging MSIX per sviluppatori e ISV, che presuppone ciò che presuppone questo articolo: c'è una cartella di build sana, non c'è nulla da catturare, e l'attrito è tutto ciò che accade dopo che la build è riuscita. È un problema diverso da quello su scala di parco, dove un'applicazione arriva senza installer e va ricostruita da una macchina in esecuzione. Se la vostra situazione è invece questa, packaging e distribuzione MSIX è il percorso giusto, e convertire un EXE in MSIX copre il terreno applicazione per applicazione. Le note di rilascio della 1.0.6 spiegano da dove arrivano le funzionalità per sviluppatori e ISV.

Provatelo sul vostro workflow

La prova onesta è se il vostro step di packaging esistente continua a funzionare quando cambiate il nome dell'eseguibile. Prendete un branch, sostituite makeappx con forge_msix negli step di pack e validate, e vedete se il job diventa verde senza toccare altro. Se lo fa, la dipendenza dall'SDK sul vostro agent non si è mai ripagata. Se non lo fa, il fallimento indicherà qualcosa di specifico, che è più di quanto la versione a caccia di percorsi di quello step vi abbia mai dato.

Esplora il packaging MSIX per sviluppatori e ISV

Il motore di packaging è già sul runner; l'unica domanda è che cosa state installando per raggiungerlo.