Skip to content

Add support for sparse packaging workflows - #607

Merged
Nikola Metulev (nmetulev) merged 47 commits into
mainfrom
zt/286-sparse-packaging
Aug 4, 2026
Merged

Add support for sparse packaging workflows#607
Nikola Metulev (nmetulev) merged 47 commits into
mainfrom
zt/286-sparse-packaging

Conversation

@zateutsch

Copy link
Copy Markdown
Contributor

Add sparse packaging support to the winapp CLI

fixes #286

Summary

Adds first-class support for the production sparse packaging workflow described in the
MS docs: Grant package identity to non-packaged apps.

Previously, the only sparse-related capability was create-debug-identity — a developer-time
helper that requires Developer Mode and registers a raw manifest. There was no supported path to
produce a signed, identity-only .msix for distribution. This PR covers steps 1–3 of the MS
docs workflow (manifest → signed .msix → embed identity into the exe). Steps 4–5
(register/unregister) remain the installer's responsibility.

The sparse packaging flow

A sparse package is an identity-only MSIX: it contains just a manifest (no binaries, no
bundled assets) and grants Windows package identity to an app that installs normally to the file
system. Identity unlocks modern Windows APIs (notifications, background tasks, share target,
startup tasks, Package.Current, etc.) for otherwise unpackaged Win32/WPF/WinForms apps. Assets
and binaries are resolved from an external content location at runtime, registered via
Add-AppxPackage -ExternalLocation.

The three CLI steps map directly to the MS docs:

Step Command What it does
1 winapp init --exe <exe> --sparse Generate a sparse identity manifest + placeholder assets, inferring defaults from the exe's FileVersionInfo
2 winapp pack <manifest> --cert <pfx> Build & sign the identity-only .msix (infers sparse from AllowExternalContent)
3 winapp embed-identity <exe|xml> Embed the <msix> identity element into the app's SxS/fusion manifest
4–5 (installer) Add-AppxPackage -ExternalLocation / Remove-AppxPackagenot the CLI's job

What this adds

1. winapp init --exe <exe> --sparse

  • Generates a sparse-specific appxmanifest.xml from the template, plus placeholder assets in Assets/.
  • Infers package name, publisher CN, version, and description from the exe via FileVersionInfo, with sensible fallbacks.
  • Interactive prompts to override inferred values; --use-defaults / --no-prompt for CI.
  • Skips SDK/package installation entirely — identity packages have no SDK dependencies.
  • Validates that --exe requires --sparse, with a clear error otherwise.
  • Output clarifies that generated assets live at the external install location, not inside the .msix.

2. winapp pack — sparse-aware

  • Auto-detects sparse mode by checking for AllowExternalContent="true" in the manifest.
  • Accepts an appxmanifest.xml directly (no folder required): stages a manifest-only directory and packs it.
  • Signs only when a cert is supplied (--cert / --generate-cert).
  • When a folder is passed for a sparse manifest, emits warnings (not errors) for stray assets/binaries that shouldn't be in an identity package.

3. winapp embed-identity <exe|xml> (new command)

  • EXE mode: embeds the <msix> element into the exe's RT_MANIFEST via mt.exe.
  • XML mode: inserts/replaces the <msix> element in an external SxS manifest file.
  • Mode auto-detected from the file extension.
  • Reads identity from --manifest (default ./appxmanifest.xml) and rejects non-sparse manifests with a clear error, since identity embedding only applies to external-location packages.

4. Sparse template & code corrections (per MS docs schema)

  • RuntimeBehavior="packagedClassicApp""win32App" (correct for plain Win32, not Desktop Bridge).
  • MinVersion raised to 10.0.19041.0 (required for AllowExternalContent / uap10).
  • Added ProcessorArchitecture="neutral" to <Identity> (identity-only packages carry no binaries).

Documentation

  • New guide: docs/guides/sparse.md — overview, prerequisites, end-to-end walkthrough, asset handling, installer integration (NSIS/WiX/Inno), and troubleshooting.
  • Updated: docs/usage.md, docs/npm-usage.md, README, CLI schema, and the Copilot/Claude skill fragments (identity, package, setup).

Sample: samples/sparse-app/

A minimal WPF sample demonstrating the full flow end-to-end, including an Inno Setup
installer (installer/setup.iss) that installs the app, deploys the .msix, and registers the
sparse package on install (and unregisters on uninstall). MainWindow queries
Package.Current and displays the package family name (or "No package identity" when unregistered).
Includes a Pester test.Tests.ps1 and is wired into the test-samples CI matrix.

What the CLI does not do

  • Register/unregister — installer's job (Add-AppxPackage -ExternalLocation).
  • Replace create-debug-identity — kept as-is for the debug workflow.
  • Per-machine vs per-user — installer's concern.

Testing

  • New SparsePackagingTests.cs (+426 lines) covering init inference/validation, sparse pack
    routing (manifest-vs-folder, warnings), embed-identity EXE/XML modes, non-sparse rejection,
    and output-path resolution (including dotted-directory and .msixbundle edge cases).
  • FakeMsixService extended for command-level routing tests.
  • Full sparse suite passes locally (dotnet test ... --filter "FullyQualifiedName~SparsePackaging").

Copilot AI balanced review requested due to automatic review settings July 8, 2026 02:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class support for the production sparse packaging workflow (identity-only MSIX) to the winapp CLI, implementing steps 1–3 of the Microsoft docs flow. It complements the existing developer-time create-debug-identity helper with a supported path to produce a signed, identity-only .msix for distribution, and to embed that identity into an app's side-by-side manifest.

Changes:

  • Adds winapp init --exe <exe> --sparse (generate a sparse identity manifest + placeholder assets, inferring defaults from the exe's FileVersionInfo, skipping SDK install), makes winapp pack sparse-aware (auto-detects AllowExternalContent, accepts a manifest file directly, stages a manifest-only package), and introduces the new winapp embed-identity <exe|xml> command.
  • Corrects the sparse template per the MS docs schema (win32App runtime behavior, MinVersion 10.0.19041.0, ProcessorArchitecture="neutral"), and surfaces the new command/options through the npm SDK and the VS Code extension.
  • Adds a WPF samples/sparse-app/ (with Inno Setup installer + Pester test wired into CI), a new docs/guides/sparse.md, and regenerates docs/schema/skill fragments.

Reviewed changes

Copilot reviewed 50 out of 54 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs New CreateSparseIdentityPackageAsync, EmbedIdentityAsync, XML-manifest embedding, and sparse output-path resolution
src/winapp-CLI/WinApp.Cli/Services/MsixService.cs Sparse detection/warning helpers and template runtime-behavior handling (minor indentation regression flagged)
src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.cs Routes manifest-file input with AllowExternalContent to the sparse packaging path
src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs Adds --exe/--sparse options and sparse init flow with validation
src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs New embed-identity command (EXE/XML modes, non-sparse rejection)
src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.sparse.xml Template corrected to MS docs schema (win32App, MinVersion, neutral arch)
src/winapp-npm/src/winapp-commands.ts npm SDK bindings for embed-identity and new init sparse options
src/winapp-VSC/src/extension.ts, src/winapp-VSC/package.json Registers the winapp.embedIdentity VS Code command
.github/plugin/agents/winapp.agent.md, .claude/agents/winapp.md Agent docs updated for embed-identity (regression: winapp run heading dropped)
samples/sparse-app/* New WPF sample, Inno Setup installer, and Pester test
docs/*, docs/cli-schema.json New sparse guide plus regenerated usage/schema/skill docs
src/winapp-CLI/WinApp.Cli.Tests/SparsePackagingTests.cs, FakeMsixService.cs Tests for init inference/validation, pack routing, and embed-identity modes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/plugin/agents/winapp.agent.md Outdated
Comment thread .claude/agents/winapp.md Outdated
Comment thread samples/sparse-app/installer/setup.iss Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.cs Outdated
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 37.36 MB 37.59 MB 📈 +241.5 KB (+0.63%)
CLI (x64) 37.52 MB 37.75 MB 📈 +228.0 KB (+0.59%)
MSIX (ARM64) 15.53 MB 15.61 MB 📈 +84.1 KB (+0.53%)
MSIX (x64) 16.50 MB 16.59 MB 📈 +91.8 KB (+0.54%)
NPM Package 32.41 MB 32.57 MB 📈 +160.6 KB (+0.48%)
NuGet Package 32.45 MB 32.62 MB 📈 +172.2 KB (+0.52%)

Test Results

4218 passed, 5 skipped out of 4223 tests in 878.9s (+51 tests, +219.2s vs. baseline)

Test Coverage

93% line coverage, 87.2% branch coverage · ⚠️ -0.3% vs. baseline

CLI Startup Time

51ms median (x64, winapp --version) · ✅ no change vs. baseline

Try This Build

Installs the MSIX for your architecture, replacing any previously installed build. Needs the GitHub CLI — the command offers to install it and sign you in if it is missing.

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) 607
Switching between builds often?

Put the tool on your PATH once:

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPath

Then this build is just:

winapp-pr 607

Run winapp-pr with no arguments to pick from a list of open PRs.


Updated 2026-08-04 19:47:48 UTC · commit e8394b4 · workflow run

Zach Teutsch (zateutsch) and others added 3 commits July 8, 2026 11:53
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@nmetulev Nikola Metulev (nmetulev) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: sparse packaging workflow

I reviewed this branch across security, correctness, CLI UX, alternative solutions, test coverage, docs/samples, and packaging, then empirically validated the findings by running the real winapp init --sparse -> pack -> embed-identity workflow against three genuine executables: a .NET exe, the actual electron.exe, and a Rust cargo exe.

Bottom line: the feature works end-to-end on all three frameworks — identity embeds correctly, Electron's existing manifest (Common-Controls, trustInfo, dpiAware, supportedOS) is preserved, and quoted publisher DNs (CN="GitHub, Inc.") are XML-escaped. The items below are worth addressing; details are in the inline comments.

Scope note

Some issues live in EmbedMsixIdentityToExeAsync, which pre-exists on main (previously only used internally for debug-identity embedding). This PR doesn't introduce those bugs, but the new embed-identity command routes user-supplied exes into that method for the first time, which materially widens their blast radius. They're flagged at the new call site (MsixService.Identity.cs:181) and labeled accordingly.

Findings

ID Sev Area Where Issue Repro
H1 High security extension.ts:560/567 (sink :66) PowerShell injection: file-picker path interpolated unescaped into terminal.sendText executed arbitrary code
M1 Med correctness Identity.cs (pre-existing, new call :181) Fixed-name temp manifests deleted from target dir -> silent user-file loss deleted planted files
M2 Med cli-ux EmbedIdentityCommand.cs:51 Auto-detect prefers Package.appxmanifest over sparse appxmanifest.xml; fails when both present exit 1
M3 Med docs sparse.md:68 --cert ./dev.pfx but cert generate writes devcert.pfx verified
M8 Med correctness Identity.cs (pre-existing, new call :181) Re-embedding a changed identity hard-fails with cryptic mt.exe c1010001 exit 1
L2 Low correctness Identity.cs:655 (pre-existing) Stray ; in generated fusion manifest (mt.exe strips it -> cosmetic) fires, stripped

Also worth a look (outside the diff hunks, so no inline anchor)

  • Test coverage: the new embed-identity EXE path, the identity-only .msix contents (CreateSparseIdentityPackageAsync), and init --sparse without --exe don't appear to be asserted in SparsePackagingTests.cs. An idempotency/rerun test would have caught M8.
  • README: the PR links the new sample in the samples table (:259) but not the guide in the "Additional guides" section (:130).
  • File size: this change grows MsixService.Identity.cs to ~1,209 lines (was 850) — over the repo's ~1,000-line guideline; a partial-class split would help.

Reviewed with the pr-review skill + a GPT cross-check, then validated empirically on dotnet/electron/rust repro apps.

Comment thread src/winapp-VSC/src/extension.ts Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Outdated
Comment thread docs/guides/sparse.md Outdated
# Conflicts:
#	docs/npm-usage.md
#	src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs
#	src/winapp-VSC/package.json
#	src/winapp-VSC/src/extension.ts
Copilot AI review requested due to automatic review settings July 24, 2026 00:30
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestTemplateService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 54 changed files in this pull request and generated 5 comments.

Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs
Comment thread samples/sparse-app/installer/setup.iss
Comment thread samples/sparse-app/installer/setup.iss Outdated
Comment thread .github/plugin/skills/winapp-cli/setup/SKILL.md
- embed-identity auto-detect now prefers a sparse manifest so a full
  Package.appxmanifest alongside appxmanifest.xml is no longer picked and
  rejected.
- Manifest embedding writes temp files under the system temp directory with
  unique names instead of fixed names beside the exe, avoiding silent
  deletion of user files.
- Strip any existing <msix> from the extracted manifest before the mt.exe
  merge so re-branding an exe is idempotent instead of failing c1010001.
- Remove stray semicolon in the generated assemblyIdentity fusion snippet.
- Fix cert filename in sparse guide (devcert.pfx, matching cert generate).
- Add regression tests for temp-file safety and <msix> stripping.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
Copilot AI review requested due to automatic review settings July 24, 2026 00:46
Comment thread src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 55 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

samples/sparse-app/installer/setup.iss:26

  • The installer script lives under installer/, and Inno resolves relative [Files] sources from the script directory by default. Consequently this path resolves as installer/bin/...; compiling the documented installer/setup.iss currently fails on line 42 with “No files found matching ...\installer\bin\...”. Set the source root to the sample's parent directory so both PublishDir and MyMsixName resolve to the artifacts produced by the README commands.
#define PublishDir "bin\Release\net10.0-windows10.0.19041.0\win-x64\publish"

Comment thread src/winapp-npm/src/winapp-commands.ts
Comment thread src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs
Comment thread src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs Outdated
Comment thread samples/sparse-app/sparse-app.csproj
Comment thread samples/sparse-app/app.manifest Outdated
Copilot review:
- embed-identity: warn users to re-sign the exe after mt.exe rewrites it
  (invalidates any existing Authenticode signature).
- sparse-app setup.iss: resolve relative [Files] sources from the sample dir
  (SourceDir=..) so PublishDir and the identity .msix resolve correctly.
- sparse-app setup.iss: unregister by exact Identity Name (Get-AppxPackage -Name
  SparseAppSample) instead of a SparseAppSample* wildcard.
- setup skill fragment: document the sparse identity init workflow and outputs;
  regenerate the plugin/claude skill mirrors.

Code quality (CodeQL generic-catch + LINQ/using):
- EmbedIdentityCommand: use LINQ over a static readonly candidate array; narrow
  IsSparseManifest catch; rethrow OperationCanceledException before generic catch.
- PackageCommand: narrow IsSparseManifestAsync catch; rethrow cancellation.
- InitCommand: rethrow cancellation before the sparse-init generic catch.
- ManifestService: using var for the extracted Icon/Bitmap; narrow the
  FileVersionInfo and cleanup catches; rethrow cancellation in logo extraction.

The temp-file data-loss re-flag was already fixed in b18bad3 (temp manifests
live under Path.GetTempPath()); no code change needed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
Copilot AI review requested due to automatic review settings July 24, 2026 01:08
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestTemplateService.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
- init: reject the sparse-only options (--exe/--name/--publisher/--output-dir)
  when --sparse is absent, so scripts fail instead of silently discarding input.
- ManifestService: reject inferred versions whose components exceed 65535 (MSIX
  Identity/@Version is 16-bit) so inference falls back to a packable default;
  add regression cases to NormalizeManifestVersion tests.
- embed-identity: document the actual manifest search order (target dir first,
  then current directory) in the --manifest description and the sparse guide,
  instead of the inaccurate "./appxmanifest.xml" default.
- sparse guide: note that EXE mode invalidates the Authenticode signature.
- sparse-app sample: copy Assets/ to build/publish output so the external
  content location has the logos the manifest references.
- sparse-app app.manifest: fix the invalid XML comment (removed the literal
  "--manifest", which XML comments cannot contain).
- Regenerate docs/cli-schema.json, the identity skill, and the npm
  winapp-commands.ts binding to match (schema version pinned at 0.5.1).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 56 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (3)

src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs:148

  • This sparse branch returns before the normal init path, so the positional base-directory and existing options such as --config-dir, --config-only, --setup-sdks, --ignore-config, and --no-gitignore are silently ignored. For example, winapp init ./identity --exe ./app.exe --sparse succeeds but writes beside the exe instead of ./identity. Reject incompatible arguments with an actionable error, or explicitly map the positional directory to sparse output semantics.
    src/winapp-CLI/WinApp.Cli/Commands/EmbedIdentityCommand.cs:27
  • The help text says the default is ./appxmanifest.xml, but the implementation first searches beside the target, then the current directory, and also considers Package.appxmanifest. This can select a different identity than the documented default. Describe the actual precedence here so --help, CLI schema, npm docs, and generated skills remain accurate after regeneration.
    samples/sparse-app/sparse-app.csproj:18
  • The installer copies only the publish directory, but this project does not mark the checked-in Assets/ files for output or publish copying. A normal dotnet publish therefore omits the external assets referenced by appxmanifest.xml, so the production installer cannot deploy the external-content layout it claims to provide. Add copy metadata for Assets/** and assert the published assets exist in the sample test.

Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Outdated
Comment thread samples/sparse-app/installer/setup.iss Outdated
Comment thread samples/sparse-app/MainWindow.xaml.cs Outdated
Comment thread docs/fragments/skills/winapp-cli/package.md
Comment thread docs/guides/sparse.md Outdated
Comment thread samples/sparse-app/README.md Outdated
Copilot AI review requested due to automatic review settings July 24, 2026 01:17
Comment thread src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs Fixed
Copilot AI review requested due to automatic review settings July 28, 2026 18:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 66 out of 70 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs:128

  • This validation still accepts empty required attributes. ParseAppxManifestAsync only checks whether Identity/@Name, Identity/@Publisher, and Application/@Id are present, so values such as Name="" pass; because packing uses MakeAppx /nv, the command can then report success while producing an undeployable identity package. Explicitly reject null/empty/whitespace values before packaging.
    samples/sparse-app/installer/setup.iss:124
  • The fallback unregisters the currently working package for every Add-AppxPackage failure, not just a same-version conflict. Trust, access, malformed-package, or transient deployment failures therefore remove the old registration and then usually fail again, leaving an upgrade rollback with no working package identity. Only unregister for the specific conflict that requires replacement, or preserve and restore the prior registration on failure.
    src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.cs:186
  • Rejecting --executable leaves no way to package sparse manifests produced by the existing winapp manifest generate --template sparse flow: that generator intentionally leaves Executable="$targetnametoken$.exe" (see ManifestCommandTests.cs:103), and the new manifest-file path copies it unchanged while using MakeAppx /nv. The command can therefore report success for an identity package whose executable is the literal placeholder. Resolve the token from --executable, or fail validation when a manifest-file input still contains it.
    docs/guides/sparse.md:59
  • This describes Assets/ as a build-time input consumed by pack/embed-identity, but neither command consumes it: manifest-file packing intentionally stages only the manifest, and embedding reads only identity metadata. The assets are deployment inputs that must be copied into the registered external location, as the guide correctly explains later. Calling them build-time inputs here can lead users to omit them from the installer.
> **Why a `sparse/` folder and not next to the exe?** The manifest and `Assets/` are **build-time inputs** consumed by `winapp pack` and `winapp embed-identity` — nothing reads them from beside the exe at runtime (runtime identity comes from the `<msix>` element embedded in the exe plus the registered package's external location, and the manifest references the exe by *name*, so its location is independent of where the exe lives). Writing them into a dedicated, source-controlled folder keeps them out of a build-output directory (like `bin/`) that a clean/rebuild would wipe, and keeps the folder free of binaries so the next steps stay clean. `winapp pack` and `winapp embed-identity` look in `sparse/` automatically, so you rarely need to name the path.

docs/guides/sparse.md:211

  • This recommended installer script removes the existing package on any exception from the first add. Failures unrelated to a same-version conflict (for example trust, access, or a corrupt new package) then destroy a working registration before retrying the same failing operation. Restrict replacement to the relevant deployment conflict or restore the old registration if the retry fails.
  try {
    Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
  } catch {
    Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
    Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
  }

Two CI failures were both caused by the sparse-packaging additions:

- sparse-app job: ISCC failed to compile setup.iss ('BEGIN' expected on
  line 130). A Pascal { } block comment contained the text '{app}', and
  Inno's brace comments do not nest, so the '}' in '{app}' closed the
  comment early and the trailing prose was parsed as code. Reworded the
  comment to avoid the constant braces.

- build-and-package job: the MS Learn docs validation (port-mslearn-docs.ps1)
  failed because docs/guides/sparse.md had no '<!-- description: -->' marker
  distinct from its H1 (a hard error), and the ported page was not present in
  the generated toc.yml. Added the description marker and registered
  guides/sparse.md in the port script's title map and TOC tree.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
Copilot AI review requested due to automatic review settings August 2, 2026 21:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 71 changed files in this pull request and generated no new comments.

Suppressed comments (6)

src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs:128

  • Manifest-file packaging bypasses the sparse normalization used for folder inputs and then invokes MakeAppx with /nv. ParseAppxManifestAsync only proves that identity/application fields exist, so a sparse manifest with EntryPoint, legacy RuntimeBehavior="packagedClassicApp", or another deployment-invalid sparse combination can still produce a “successful” MSIX that Add-AppxPackage rejects. Apply the same sparse rewrite/semantic validation as the folder path before staging, and add a regression case for an invalid hand-authored sparse manifest.
    samples/sparse-app/installer/setup.iss:123
  • This catch removes the existing registration after every Add-AppxPackage failure, not only the same-version conflict described above. A bad signature, missing trust, locked file, or transient deployment error therefore deletes a working prior identity; if the retry fails, installer file rollback cannot restore that registration. Only unregister after positively identifying the expected same-version conflict, or fail while preserving the existing package.
    samples/sparse-app/sparse-app.csproj:22
  • Use the repository’s established recursive asset glob (Assets\**\*), as documented by IDotNetService.cs:126 and asserted in DotNetServiceTests.cs:1087. The bare trailing ** deviates from the generated project pattern and can fail to enumerate the asset files that sparse registration expects in publish output.
    src/winapp-CLI/WinApp.Cli/Commands/InitCommand.cs:297
  • The linked issue specifies that sparse init defaults to the executable’s directory, but this implementation defaults to ./sparse under the current directory. The new docs consistently describe ./sparse, so this appears intentional; update the PR/issue acceptance text or restore the specified default so the public contract is unambiguous.
    samples/sparse-app/installer/setup.iss:89
  • An exact Get-AppxPackage -Name match identifies only the package Identity Name, not the publisher/package family. If another package family uses the same Name, this pipeline unregisters it too, contradicting the comment that unrelated packages are protected. Filter by the expected Publisher/PackageFamilyName and remove one exact PackageFullName.
    docs/guides/sparse.md:212
  • The reusable installer script has the same destructive fallback as the sample: any add failure first removes the currently working registration, even when the failure is unrelated to a same-version conflict. Preserve the existing package unless the caught deployment error is specifically the conflict this retry is intended to resolve.
  try {
    Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
  } catch {
    Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
    Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
  }

Resolve conflicts in test-samples.yml (union of sparse-app and winui-app/
winui-solution/winui-unpackaged-app sample lists) and the auto-generated
agent files (keep embed-identity section plus main's updated run command
description).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
Copilot AI review requested due to automatic review settings August 2, 2026 21:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 72 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

samples/sparse-app/installer/setup.iss:123

  • The fallback unregisters the currently working package after any Add-AppxPackage failure, not only a same-version conflict. A bad/untrusted replacement package, locked files, or a transient deployment error therefore removes the existing registration; when the retry fails, Inno rolls back files but does not restore that registration, leaving the previously installed app without identity. Only remove on the specific conflict this fallback is intended to handle, and preserve/restore the prior registration if the retry fails.

Comment thread docs/guides/sparse.md
The register-sparse.ps1 docs helper and the sparse-app setup.iss inline
registration unregistered the existing package on ANY first Add-AppxPackage
failure, then retried. An untrusted or corrupt new .msix (or an unsupported OS)
would trip the catch, remove a working prior registration, then fail the retry
too — leaving the installed app with no identity.

Gate the unregister+retry on HRESULT 0x80073CFB (ERROR_PACKAGE_ALREADY_EXISTS,
the same-version-already-registered conflict Add-AppxPackage rejects) and
re-throw every other failure so a bad package can never strip existing identity.
Verified the parsing of 0x80073CFB is identical on Windows PowerShell 5.1 and
PowerShell 7, and validated both control-flow paths (conflict -> remove+retry;
untrusted -> abort without removing) with mocked cmdlets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
Copilot AI review requested due to automatic review settings August 2, 2026 21:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 72 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

samples/sparse-app/installer/setup.iss:144

  • RaiseException from this [Files] AfterInstall callback does not fail Setup. I compiled and ran the installer with an untrusted package certificate: RegisterParams returned exit code 1, but Inno logged the exception as a suppressed “Expression error,” continued creating shortcuts/uninstall metadata, exited 0, and left the app installed with no SparseAppSample package registered. This turns a registration failure into an apparently successful identity-less install. Move registration to an Inno execution path whose failure propagates to Setup, and explicitly clean up copied files/registration on failure rather than relying on automatic rollback.

Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs Outdated
The direct-file sparse pack path staged the input manifest with File.Copy,
bypassing the sparse corrections folder packing applies. A manifest from an
older template or hand-edit could ship with RuntimeBehavior=packagedClassicApp,
a missing ProcessorArchitecture, or a MinVersion below the 10.0.19041.0 that
AllowExternalContent requires.

Add MsixService.NormalizeSparseIdentityManifest, applied when staging the
manifest: forces win32App/mediumIL for an .exe app, removes EntryPoint,
defaults a missing ProcessorArchitecture to neutral, and raises any
TargetDeviceFamily MinVersion below 10.0.19041.0. Corrections are surfaced as
status messages. No-op for a freshly generated (correct) manifest.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a
Copilot AI review requested due to automatic review settings August 2, 2026 23:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 72 changed files in this pull request and generated 1 comment.

Comment thread samples/sparse-app/installer/setup.iss Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 72 changed files in this pull request and generated 1 comment.

Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.cs
Folder inputs route sparse manifests through UpdateAppxManifestContentAsync,
but that rewrite did not raise TargetDeviceFamily/@MinVersion. A folder created
from an older sparse template kept 10.0.18362.0; because packing uses MakeAppx
/nv, the command could still produce and sign an MSIX deployment rejects, since
AllowExternalContent requires 10.0.19041.0.

Extract the MinVersion flooring from NormalizeSparseIdentityManifest into a
shared RaiseSparseTargetDeviceFamilyMinVersion helper and apply it in the
folder-packing sparse block, so both the manifest-file and folder paths enforce
the same 10.0.19041.0 floor. Corrections are surfaced as status messages. Add a
folder-path regression test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: de996470-3bd8-45b4-a88d-810d3799467a

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 72 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs:242

  • This does not actually prevent an invalid identity package from being produced. ParseAppxManifestAsync only checks that Identity.Name, Identity.Publisher, and Application.Id attributes exist; empty values, a missing Identity.Version, and missing required package sections such as Dependencies/TargetDeviceFamily still pass. Because packaging deliberately uses MakeAppx /nv, such a manifest can be packed and signed successfully but then fail during Add-AppxPackage. Please validate the complete deployment-required sparse manifest structure (and non-empty/schema-valid attribute values) before packaging.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 72 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/winapp-npm/src/cli-args.ts:94

  • This does not match the native parser for repeated occurrences. The built CLI resolves --sparse=false --sparse to false (the explicit value wins over the bare occurrence), while this returns true; two valued occurrences are rejected by native parsing rather than using the last value. In the npm wrapper, that mismatch selects the sparse fast path and can reject --add-js-bindings even though native init is in normal mode. Track explicit values separately and bypass wrapper hooks for combinations the native parser will reject.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 71 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

docs/guides/sparse.md:215

  • The documented retry removes the existing working registration before the replacement is known to succeed. If the second Add-AppxPackage fails, the installer receives an error but the old app has already lost package identity. Avoid unregistering for a same-version reinstall, or preserve and restore the prior registration when the retry fails.
    Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
    Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop

src/winapp-CLI/WinApp.Cli/Services/MsixService.SparsePackaging.cs:205

  • A missing or malformed MinVersion bypasses this normalization because Version.TryParse returns false. Since sparse packages are packed with MakeAppx /nv, the command can then report success (and even sign the output) for a package that deployment rejects. Treat an unparseable value like a below-floor value, or fail validation explicitly.

Comment on lines +123 to +124
'Get-AppxPackage -Name ''' + EscapePSLiteral('{#MyPackageName}') + ''' | Remove-AppxPackage -ErrorAction SilentlyContinue; ' +
'Add-AppxPackage -Path ''' + MsixPath + ''' -ExternalLocation ''' + EscapePSLiteral(AppDir) + ''' -ErrorAction Stop } ' +
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add sparse packaging support (winapp sparse command group)

4 participants