Skip to content

Add MSIXVC2 upload support to PackageUploader.exe via MakePkg.exe - #135

Open
Jason Williams (WilliamsJason) wants to merge 10 commits into
mainfrom
jaswill-microsoft-msixvc2-cli-upload
Open

Add MSIXVC2 upload support to PackageUploader.exe via MakePkg.exe#135
Jason Williams (WilliamsJason) wants to merge 10 commits into
mainfrom
jaswill-microsoft-msixvc2-cli-upload

Conversation

@WilliamsJason

@WilliamsJason Jason Williams (WilliamsJason) commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

UploadXvcPackage now detects MSIXVC2 packages and delegates the upload to the MSIXVC2-capable MakePkg.exe, translating the operation config into MakePkg.exe command-line parameters. This is CHANGE 2 of a two-part GDK release work item.

There is no new verb and no new config switch — detection is driven off the package file itself, so an existing UploadXvcPackage config "just works" for an MSIXVC2 package. The legacy XVC1/MSIXVC1 upload path is untouched.

Rebased onto CHANGE 1 (#134)

This PR is rebased onto merged main and now consumes CHANGE 1's resolver directly. The placeholder is gone: Msixvc2CapabilityPlaceholder is deleted and both TODO(GDK-release) markers are removed.

src/PackageUploader.Application/Tools/IMsixvc2UploadToolProvider.cs remains as the narrow abstraction this PR consumes (IsAvailable, ExecutablePath), and Msixvc2ToolResolverAdapter implements it over PackageUploader.ClientApi.Tools.IMsixvc2ToolResolver. As designed, the swap was a single registration line in HostExtensions.ConfigureServices.

The adapter resolves with no path hints — pure self-discovery. The UI passes already-resolved paths because it has its own file pickers; the CLI has no such input. It honors the provider contract that the compiler cannot express here, this project having nullable reference types off:

  • Resolve() returning null maps to IsAvailable == false and a null ExecutablePath, so "no capable tool" stays the clean, actionable error the operation already reports.
  • Resolution happens exactly once and both members are served from that one result. The resolver deliberately does not cache and re-probes by launching a candidate executable on every call, so a two-call adapter would probe repeatedly per upload and could disagree with itself between reads. UploadMsixvc2PackageAsync reads the members three times, so this is not hypothetical. It is registered scoped, not singleton, so each operation gets a fresh resolution rather than one cached for the life of the process.
  • Nothing escapes as an exception. The resolver is documented as never throwing; the catch is defense in depth, degrading to unavailable.

Verified against the real GDK, not only mocks. The legacy MakePkg.exe in the GDK bin directory fails the uploadsource probe, the resolver falls back to the makepkg2.exe shipped alongside it, and a live upload to branch JaswillTest completed and applied its availability date:

MakePkg.exe uploadsource probe: not supported (exit code 2) for C:\Program Files (x86)\Microsoft GDK\bin\MakePkg.exe.
makepkg2.exe uploadsource probe: supported (exit code 0) for C:\Program Files (x86)\Microsoft GDK\bin\makepkg2.exe.
Running C:\Program Files (x86)\Microsoft GDK\bin\makepkg2.exe upload /pd "..." /branch "JaswillTest" ...

The probe pair appears exactly once in that run, confirming the single resolution end to end.

Circular-dependency guard (two independent barriers)

MakePkg.exe shells back out to PackageUploader.exe for XVC1 uploads — the legacy makepkg.exe upload /? help says so in its own words: "Uploads a specified package to Partner Center via the PackageUploader tool." Delegating unconditionally would create infinite process recursion between the two executables.

Barrier 1 — package format detection. UploadXvcPackageOperation.ProcessAsync only delegates when the package is detected as MSIXVC2. This is driven off the package file, not a config flag, so it cannot be bypassed by configuration. Covered by NonMsixvc2Package_NeverShellsOut and MissingPackageFile_NeverShellsOut.

Barrier 2 — delegation breadcrumb. PackageFormatDetector.IsLikelyMsixvc2Package is a heuristic: its fallback check scans the trailing bytes for the 4-byte ZIP EOCD signature, which an encrypted XVC1 tail can contain by chance. A false positive there would be unbounded. So Msixvc2ProcessRunner stamps PACKAGEUPLOADER_MSIXVC2_DELEGATED=1 onto every MakePkg.exe child process, and Msixvc2DelegationGuard refuses to delegate when that variable is already present in our own environment. Any MakePkg.exe that shells back to us inherits the stamp, so the cycle breaks after exactly one hop regardless of what the heuristic decides — it logs a warning and falls through to the normal upload path. Tested in both directions.

MakePkg.exe argument mapping

upload /pd "<directory of packageFilePath>"
       [/branch "<branchFriendlyName>" | /flight "<flightName>"]
       [/market "<marketGroupName>"]
       /storeid "<bigId>"
       /auth <method>
       [/tenantid "..."] [/clientid "..."]
       [/clientsecret "..." | /certthumbprint "..." [/certstore "..."] [/certlocation "..."]]
       [/resourceid "..."]

Every flag is now grounded in the verbatim help output of a real MSIXVC2-capable binarymakepkg2.exe upload /?, version 2604.405.14000.0, from C:\PackagingTest\Release\Tools\ — cross-checked against the two existing UI argument builders. The earlier "needs verification" section is resolved and removed.

Flag Grounding
/pd In makepkg2.exe upload /? and in PackageUploadViewModel.BuildMsixvc2UploadArguments() — the already-built-package scenario, which is what the CLI has. /d is the pack-from-a-loose-content-folder form.
no /msixvc2 /msixvc2 appears only in the /d (pack-and-upload) form. The /pd form in PackageUploadViewModel does not emit it, so neither do we.
/branch, /flight, /market, /storeid Help output + both UI builders. Emitted in the same order as the UI.
/auth, /tenantid, /clientid, /clientsecret, /certthumbprint, /certstore, /certlocation, /resourceid Help output, verbatim.
no /uploadsource See below — deliberately dropped.

Correction: /uploadsource has been dropped

The previous revision emitted /uploadsource PackageUploader. That was flagged as unverified, and the flag check found it to be wrong. /uploadsource does exist, but probing its enum shows it accepts only makepkg2 and XGPM. There is no value representing PackageUploader, so the flag is now omitted entirely and the tool's own default is used. IMsixvc2UploadToolProvider.SupportsUploadSource and the probe behind it are removed.

Caveat: the MSIXVC2-capable binary available for verification is makepkg2.exe, not the renamed MakePkg.exe that ships with the upcoming GDK. The legacy makepkg.exe on the same machine is the pre-merge tool (it has /tenantid but no /auth, and returns exit 2 for supports uploadsource). Any statement about the renamed tool's surface is inferred from makepkg2's.

Authentication — non-interactive/CI auth is supported

The previous revision hard-failed every --Authentication value except CacheableBrowser, which would have made MSIXVC2 upload impossible from any unattended pipeline. That was based on the UI being the only grounding available; the real help output shows the tool accepts a full credential surface:

/auth <method>   Available methods: Default, Browser, CacheableBrowser, AzureCli, ManagedIdentity,
                 ManagedIdentityFederated, Environment, AzurePipelines, ClientSecret,
                 ClientCertificate. Default is CacheableBrowser.

So PackageUploader now forwards the configured identity rather than rejecting it.

--Authentication Forwarded as Note
AppSecret /auth ClientSecret PackageUploader's legacy name for the same AAD application-secret flow. AzureApplicationSecretAccessTokenProvider (MSAL confidential client) and ClientSecretCredentialAccessTokenProvider (Azure.Identity.ClientSecretCredential) take the same TenantId/ClientId/ClientSecret triple — a straight alias, not a behavior change.
AppCert /auth ClientCertificate Store-based certificate.
Default, Browser, CacheableBrowser, AzureCli, ManagedIdentity, ManagedIdentityFederated, Environment, AzurePipelines, ClientSecret, ClientCertificate Same name Verbatim from the help output.

The --TenantId hard-fail is also removed — /tenantid is a real flag.

Two configurations are still rejected, because the tool genuinely has no equivalent:

  • Certificate file path (ClientCertificateAuthInfo:CertificatePath). The tool selects a certificate from a Windows store by thumbprint (/certthumbprint, /certstore, /certlocation) and exposes no flag naming a certificate file. Error message points at importing to a store and using AppCert.
  • Certificate subject (AadAuthInfo:CertificateSubject). No subject flag exists; resolving the subject ourselves and forwarding a thumbprint would be guesswork about which certificate the user meant.

Security note: /clientsecret puts the secret on the child's command line, where anything that can read the process table can see it. PackageUploader redacts /clientsecret and /certpassword from its own logs, and the README recommends Environment, AzurePipelines, ManagedIdentity, or ManagedIdentityFederated on shared build agents.

Config option handling

Warn-and-ignore the harmless, hard-fail the meaningful:

Option Behavior on the MSIXVC2 path
gameAssets — all paths resolve to the same directory as packageFilePath Warn and continue. MakePkg.exe picks the assets up from the package folder.
gameAssets — any path resolves elsewhere Hard-fail, naming the offending path and the expected directory. Silently dropping an asset the user deliberately placed elsewhere would change the outcome.
minutesToWaitForProcessing Always warn and ignore. The default is 30 and an explicit 30 is indistinguishable, so treating it as an error would be unreliable.
deltaUpload Warn and ignore.
availabilityDate / preDownloadDate Supported, applied exactly as on the XVC1 path — see below.
productId without bigId Resolved to the Big ID via GetProductAsync. Only fails if the lookup genuinely can't produce one.
--Authentication / --TenantId Forwarded. See the authentication section above.

gameAssets is no longer [Required] on the attribute; the requirement is now enforced inside UploadXvcPackageOperationConfig.Validate() only when the package is not MSIXVC2, so XVC1 validation is unchanged. Both branches are tested.

How availabilityDate / preDownloadDate are applied

An earlier revision of this PR hard-failed these two options, on the premise that SetXvcConfigurationAsync needs the specific GamePackage that was just uploaded and that MakePkg.exe does not report that identity back. That premise was wrong, and it was the last true adoption blocker for MSIXVC2 on the CLI, so it was worth checking against the real tool rather than inferring.

A live upload shows MakePkg emitting Package Id is <guid> at info level — no /v required — early in the run, before the content transfer. And SetXvcConfigurationAsync only ever reads GamePackage.Id. That one line is the entire missing input.

  • Msixvc2ProcessRunner now returns Msixvc2ProcessResult(ExitCode, UploadedPackageId) instead of a bare exit code.
  • The capture is deliberately strict: an exact "Package Id is " marker plus Guid.TryParseExact(..., "D", ...). MakePkg prints several other ... is <guid> lines (Xfus Id, Draft Instance Id, CV, ingest job), so a looser marker would silently date the wrong package. Two conflicting ids in one run resolve to null rather than a guess.
  • The operation does not trust the reported id. It looks it up via GetGamePackagesAsync for the target branch and market group. That lookup is required regardless — GamePackageResource.Id is internal init, so the Application cannot fabricate a GamePackage — but it doubles as proof the package belongs where the dates are being written. If the output format ever drifts, this fails loudly instead of quietly mis-dating.
  • The date condition mirrors XVC1 exactly (is not null, not ?.IsEnabled == true), so a disabled date still calls through and clears a previously-set value.

Verified end to end: a real CLI upload to branch JaswillTest captured a760372c-8c3b-4d15-bdef-57449a7cb4a6 and set the configuration against it.

One caveat, called out in the README as a > [!NOTE]: because the dates are a post-upload step, a failure there does not undo the upload.

Shared MSIXVC2 detection

IsLikelyMsixvc2Package moves out of PackageUploader.UI/Model/Xvc/XvcFile.cs into PackageUploader.ClientApi/Packaging/PackageFormatDetector.cs — ClientApi being the only project referenced by both the UI and the Application. XvcFile.IsLikelyMsixvc2Package now delegates to it, so there is a single source of truth. Behavior is identical apart from an added null/whitespace path guard.

Child process handling

Msixvc2ProcessRunner mirrors the UI: UseShellExecute = false, CreateNoWindow = true, stdout/stderr redirected and streamed through the existing ILogger so console users see progress, the operation's CancellationToken is honored (the child process tree is killed on cancel), the child's exit code is propagated as the operation's success/failure, and PACKAGEUPLOADER_MSIXVC2_DELEGATED=1 is stamped on the child environment.

Tests

src/PackageUploader.Application.Test/Tools/:

  • MSIXVC2 + capability available → shells out with the exact expected argument string (branch and flight forms).
  • MSIXVC2 + capability unavailable → clear error, no shell-out.
  • Non-MSIXVC2 package → never shells out (the circular-dependency guard), and takes the legacy ingestion path with identical calls and arguments.
  • Missing package file → never shells out.
  • Loop-breaker, both directions: sentinel absent → delegates; sentinel present → never shells out, logs a warning, and falls through to the legacy path. Plus direct unit tests of Msixvc2DelegationGuard against the environment variable.
  • Auth: AppSecret/auth ClientSecret with tenant/client/secret forwarded; AppCert/auth ClientCertificate with thumbprint/store/location; AzurePipelines forwards the method with no credential flags; ManagedIdentityFederated forwards /resourceid; missing secret, missing thumbprint, certificate file path, and certificate subject each throw with an actionable message.
  • Asserts /uploadsource is absent from the built arguments.
  • Availability/pre-download dates: applied after a successful upload, skipped when unset, and hard-failing when MakePkg reported no usable id or the id is not in the target market group.
  • Msixvc2ProcessRunnerTest drives a real child process to cover id capture, absence, non-GUID rejection, conflicting ids, and that other ... is <guid> lines are not mistaken for the package id.
  • deltaUpload → warns and still shells out.
  • Non-zero child exit code → operation fails.
  • Cancellation → the operation token reaches the runner and the operation reports cancelled.
  • productId-only config → resolves and passes the Big ID to /storeid; unresolvable → clear error.
  • Asserts /msixvc2 is absent from the built arguments.

src/PackageUploader.ClientApi.Test/Packaging/PackageFormatDetectorTest.cs ports the detector coverage.

Validation

dotnet build src\PackageUploader.sln
  Build succeeded. 0 Warning(s) 0 Error(s)

dotnet test src\PackageUploader.Application.Test\PackageUploader.Application.Test.csproj
  Passed! total: 103, failed: 0

dotnet test src\PackageUploader.ClientApi.Test\PackageUploader.ClientApi.Test.csproj
  Passed! total: 77, failed: 0

dotnet test src\PackageUploader.UI.Test\PackageUploader.UI.Test.csproj
  Passed! total: 357, failed: 0

@WilliamsJason
Jason Williams (WilliamsJason) marked this pull request as draft August 12, 2026 20:40
UploadXvcPackage now detects MSIXVC2 packages and delegates the upload to the
MSIXVC2-capable MakePkg.exe, translating the operation config into MakePkg.exe
command-line parameters. The legacy XVC1/MSIXVC1 upload path is unchanged.

Delegation is gated strictly on positive MSIXVC2 package detection, because
MakePkg.exe shells back out to PackageUploader.exe for XVC1 uploads and an
unconditional delegation would create infinite process recursion.

MSIXVC2 detection moves from PackageUploader.UI into
PackageUploader.ClientApi/Packaging/PackageFormatDetector so the CLI and UI
share one source of truth; XvcFile.IsLikelyMsixvc2Package now delegates to it.

The MakePkg.exe capability check is a clearly marked placeholder in
PackageUploader.Application/Tools that must be swapped for the shared
IMsixvc2ToolResolver before merge.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
PackageFormatDetector.IsLikelyMsixvc2Package is a heuristic: its fallback
check scans the trailing bytes of the package for the 4-byte ZIP EOCD
signature, which an encrypted XVC1 tail can contain by chance. Because
MakePkg.exe shells back out to PackageUploader.exe for XVC1 uploads, a false
positive there is unbounded rather than merely wrong.

Stamp PACKAGEUPLOADER_MSIXVC2_DELEGATED=1 onto every MakePkg.exe child
process, and refuse to delegate when that variable is already present in our
own environment. Any MakePkg.exe that shells back to us inherits the stamp,
so the cycle breaks after exactly one hop no matter what the format
heuristic decides. Format detection remains the primary guard.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The builder previously rejected every --Authentication value except
CacheableBrowser, which made MSIXVC2 upload impossible from any
non-interactive pipeline - PackageUploader.exe's primary use case.

Grounded against the verbatim help output of the MSIXVC2-capable packaging
tool (makepkg2.exe upload /?, version 2604.405.14000.0):

  /auth accepts Default, Browser, CacheableBrowser, AzureCli,
  ManagedIdentity, ManagedIdentityFederated, Environment, AzurePipelines,
  ClientSecret and ClientCertificate, alongside /tenantid, /clientid,
  /clientsecret, /certthumbprint, /certstore, /certlocation and /resourceid.

So forward the configured identity instead of rejecting it. AppSecret and
AppCert map onto ClientSecret and ClientCertificate, which are the same AAD
application flows under the tool's names; the other ten map verbatim. The
/tenantid hard-fail is likewise removed, since the flag exists.

Two configurations are still rejected, because the tool genuinely has no
equivalent: a certificate FILE path (it selects certificates from a Windows
store by thumbprint only) and a certificate SUBJECT.

Also drop /uploadsource entirely. The flag exists but its enum accepts only
'makepkg2' and 'XGPM' - there is no value representing PackageUploader, so
the previously emitted value was invalid. The tool's own default is used.

Redact /clientsecret and /certpassword from the logged argument string, and
document in the README that a secret passed this way is visible in the
process table, recommending the credential-free methods on shared agents.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Delegating the upload means a client secret has to be handed to MakePkg.exe
on its command line, where it is visible in the process table for the
lifetime of the child. MakePkg.exe offers no out-of-band credential path, so
redacting our own logs does not address it. Raise the guidance from a
trailing note to a prominent warning callout, name the credential-free
methods to prefer on shared agents, and reference it from the config table.

Also record in the argument builder's docs that the mapping was verified
against makepkg2.exe rather than the renamed MakePkg.exe from the merged
GDK, so whoever revisits this knows /auth is the first thing to re-check if
the merged tool diverges.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Microsoft.Xbox.Packaging.Tools.makepkg2 ships on an internal-only feed, so
pointing an external customer at it tells them to install something they
cannot obtain. The GDK is the complete answer on its own: an installed GDK
ships both makepkg.exe and makepkg2.exe side by side in <GDKInstallPath>\bin,
so no NuGet package is required.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This branch introduced the repository's only six `#nullable enable`
directives. The repo does use nullable reference types, but always
project-wide via `<Nullable>enable</Nullable>` in the .csproj, never by
file-level directive. Drop the directives so the branch matches.

Five of the six were in projects where NRT is off, so their `?`
annotations were load-bearing and are stripped alongside the directive.
The sixth (Msixvc2UploadArgumentBuilderTest.cs) is in a project that
already enables NRT project-wide, so its directive was a no-op and only
the line is removed.

Annotation-only: no null check, guard, or fail-fast is altered, and
`?.`, `!`, and Nullable<T> value types such as DateTime? are untouched.

Because the compiler can no longer express it, the null contract that
the annotations carried is now stated explicitly in XML docs, most
importantly on IMsixvc2UploadToolProvider: ExecutablePath is null or
empty whenever IsAvailable is false, neither member may throw when no
tool is available, and both must report a single shared resolution.
That contract is what the post-rebase adapter over IMsixvc2ToolResolver
has to honor.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The environment stamp only covers cycles that PackageUploader.exe itself
starts. When MakePkg.exe is the entry point it invokes PackageUploader
with nothing stamped, so that first hop is unguarded: a false-positive
MSIXVC2 detection there delegates straight back to MakePkg.

The stamp still bounds that case, because the MakePkg we spawn inherits
it and the PackageUploader beneath that one sees it, so the cycle closes
after two hops rather than running away. It stops bounding it only if
MakePkg ever sanitizes the child environment, which is outside our
control. This adds a third, independent signal that does not rely on
environment inheritance and closes the same case after one hop.

MakePkg only hands PackageUploader XVC1/MSIXVC1 packages, so a MakePkg
parent contradicts an MSIXVC2 detection, and the parent is the more
trustworthy of the two. Both barriers therefore fall through to the
normal XVC1 upload instead of failing: a false-positive XVC1 package
uploads correctly, and a genuine MSIXVC2 package fails, which is the
right outcome for one that cannot be delegated. Failing outright would
have broken the false-positive case, which is the likelier one.

The lookup is seamed behind IParentProcessProvider so the guard is
testable without a real MakePkg parent. The Windows implementation reads
the parent id from the current process via NtQueryInformationProcess,
using the pseudo-handle so it needs no extra rights, and reading the
buffer field-by-field so the path stays blittable under PublishAot. It
returns null on every failure, guards against process id reuse via start
times, and never throws. Non-Windows reports the parent as unknown.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CodeQL flagged the logged argument string as clear-text storage of
sensitive information. The flow was real: the command line was built
with the client secret in it, then scrubbed with a regex just before
logging, so the secret genuinely existed in the value handed to the
logger and the regex was the only thing standing between it and the log
file.

Scrubbing after the fact was the weak part, not just the taint path. The
pattern had to stay in sync with the exact spelling, spacing and quoting
the builder emits, and it matched only a quoted value after whitespace.
Any change to how the flag is rendered, or a newly added credential
flag, would have silently started leaking. It also already covered
/certpassword, which the builder never emits, which is a good sign the
pattern and the builder were maintained independently.

The builder now returns both forms. The redacted one is built from a
context whose secret has been substituted, so it is produced from
credential-free inputs and never contains the credential at all. It
cannot drift from the executable form because both come from the same
code path, and returning them together makes logging the wrong one hard
to do by accident.

Verified the new coverage fails when the fix is reverted: pointing the
log line back at the executable command line fails
Msixvc2WithClientSecret_PassesSecretToProcessButNeverLogsIt, which
asserts the secret reaches the process and appears in no log entry at
any level.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The MSIXVC2 path previously hard-failed when availabilityDate or
preDownloadDate was configured, on the stated premise that MakePkg.exe
does not report back the identity of the package it created. That
premise was wrong. A live upload against a real MSIXVC2 package shows
MakePkg emitting "Package Id is <guid>" at info level (no /v needed),
early in the run and before the content transfer.

SetXvcConfigurationAsync only ever reads GamePackage.Id, so that one
line is the entire missing input. Msixvc2ProcessRunner now captures it
and returns Msixvc2ProcessResult instead of a bare exit code, and
UploadXvcPackageOperation applies the dates exactly as the XVC1 path
does.

The capture is deliberately strict: an exact "Package Id is " marker
plus Guid.TryParseExact with the "D" format. MakePkg prints several
other "... is <guid>" lines (Xfus Id, Draft Instance Id, CV, ingest
job), so a looser marker would silently date the wrong package. Two
conflicting ids in one run resolve to null rather than a guess.

Rather than trust the reported id, the operation looks it up through
GetGamePackagesAsync for the target branch and market group. That is
required regardless, since GamePackageResource.Id is internal init and
the id cannot be turned into a GamePackage locally, but it doubles as
proof the package belongs where the dates are being written. If the
output format ever drifts, this fails loudly instead of quietly
mis-dating.

Verified end to end: a real CLI upload to branch JaswillTest captured
a760372c-8c3b-4d15-bdef-57449a7cb4a6 and set the configuration against
it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CHANGE 1 has landed, so IMsixvc2UploadToolProvider now sits over
PackageUploader.ClientApi.Tools.IMsixvc2ToolResolver instead of the
always-available placeholder. Msixvc2CapabilityPlaceholder is deleted
and both TODO(GDK-release) markers are gone.

The adapter resolves with no path hints, i.e. pure self-discovery. The
UI passes already-resolved paths because it has its own file pickers;
the CLI has no such input.

It honors the provider contract that the compiler cannot express here,
this project having nullable reference types off:

- Resolve() returning null maps to IsAvailable false and a null
  ExecutablePath, so "no capable tool" stays the clean, actionable
  error UploadXvcPackageOperation already reports.
- Resolution happens exactly once and both members are served from
  that one result. The resolver deliberately does not cache and
  re-probes by launching a candidate executable on every call, so a
  two-call adapter would probe repeatedly per upload and could
  disagree with itself between reads. UploadMsixvc2PackageAsync reads
  the members three times, so this is not hypothetical.
- Nothing escapes as an exception. The resolver is documented as never
  throwing; the catch is defense in depth, degrading to unavailable.

Registered scoped rather than singleton so each operation gets a fresh
resolution instead of one cached for the life of the process.

Verified against the real GDK rather than only mocks: the legacy
MakePkg.exe in the GDK bin directory fails the uploadsource probe, the
resolver falls back to makepkg2.exe alongside it, and a live upload to
branch JaswillTest completed and applied its availability date. The
probe pair appears exactly once in that run, confirming the single
resolution end to end.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@WilliamsJason
Jason Williams (WilliamsJason) force-pushed the jaswill-microsoft-msixvc2-cli-upload branch from 554b7d2 to 3469b28 Compare August 17, 2026 17:55
@WilliamsJason
Jason Williams (WilliamsJason) marked this pull request as ready for review August 17, 2026 22:16
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.

2 participants