Add MSIXVC2 upload support to PackageUploader.exe via MakePkg.exe - #135
Open
Jason Williams (WilliamsJason) wants to merge 10 commits into
Open
Add MSIXVC2 upload support to PackageUploader.exe via MakePkg.exe#135Jason Williams (WilliamsJason) wants to merge 10 commits into
Jason Williams (WilliamsJason) wants to merge 10 commits into
Conversation
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>
Jason Williams (WilliamsJason)
force-pushed
the
jaswill-microsoft-msixvc2-cli-upload
branch
from
August 17, 2026 17:55
554b7d2 to
3469b28
Compare
Jason Williams (WilliamsJason)
marked this pull request as ready for review
August 17, 2026 22:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
UploadXvcPackagenow detects MSIXVC2 packages and delegates the upload to the MSIXVC2-capableMakePkg.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
UploadXvcPackageconfig "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
mainand now consumes CHANGE 1's resolver directly. The placeholder is gone:Msixvc2CapabilityPlaceholderis deleted and bothTODO(GDK-release)markers are removed.src/PackageUploader.Application/Tools/IMsixvc2UploadToolProvider.csremains as the narrow abstraction this PR consumes (IsAvailable,ExecutablePath), andMsixvc2ToolResolverAdapterimplements it overPackageUploader.ClientApi.Tools.IMsixvc2ToolResolver. As designed, the swap was a single registration line inHostExtensions.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 toIsAvailable == falseand a nullExecutablePath, so "no capable tool" stays the clean, actionable error the operation already reports.UploadMsixvc2PackageAsyncreads 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.Verified against the real GDK, not only mocks. The legacy
MakePkg.exein the GDKbindirectory fails theuploadsourceprobe, the resolver falls back to themakepkg2.exeshipped alongside it, and a live upload to branchJaswillTestcompleted and applied its availability date: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.ProcessAsynconly 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 byNonMsixvc2Package_NeverShellsOutandMissingPackageFile_NeverShellsOut.Barrier 2 — delegation breadcrumb.
PackageFormatDetector.IsLikelyMsixvc2Packageis 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. SoMsixvc2ProcessRunnerstampsPACKAGEUPLOADER_MSIXVC2_DELEGATED=1onto every MakePkg.exe child process, andMsixvc2DelegationGuardrefuses 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
Every flag is now grounded in the verbatim help output of a real MSIXVC2-capable binary —
makepkg2.exe upload /?, version 2604.405.14000.0, fromC:\PackagingTest\Release\Tools\— cross-checked against the two existing UI argument builders. The earlier "needs verification" section is resolved and removed./pdmakepkg2.exe upload /?and inPackageUploadViewModel.BuildMsixvc2UploadArguments()— the already-built-package scenario, which is what the CLI has./dis the pack-from-a-loose-content-folder form./msixvc2/msixvc2appears only in the/d(pack-and-upload) form. The/pdform inPackageUploadViewModeldoes not emit it, so neither do we./branch,/flight,/market,/storeid/auth,/tenantid,/clientid,/clientsecret,/certthumbprint,/certstore,/certlocation,/resourceid/uploadsourceCorrection:
/uploadsourcehas been droppedThe previous revision emitted
/uploadsource PackageUploader. That was flagged as unverified, and the flag check found it to be wrong./uploadsourcedoes exist, but probing its enum shows it accepts onlymakepkg2andXGPM. There is no value representing PackageUploader, so the flag is now omitted entirely and the tool's own default is used.IMsixvc2UploadToolProvider.SupportsUploadSourceand the probe behind it are removed.Authentication — non-interactive/CI auth is supported
The previous revision hard-failed every
--Authenticationvalue exceptCacheableBrowser, 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:So PackageUploader now forwards the configured identity rather than rejecting it.
--AuthenticationAppSecret/auth ClientSecretAzureApplicationSecretAccessTokenProvider(MSAL confidential client) andClientSecretCredentialAccessTokenProvider(Azure.Identity.ClientSecretCredential) take the same TenantId/ClientId/ClientSecret triple — a straight alias, not a behavior change.AppCert/auth ClientCertificateDefault,Browser,CacheableBrowser,AzureCli,ManagedIdentity,ManagedIdentityFederated,Environment,AzurePipelines,ClientSecret,ClientCertificateThe
--TenantIdhard-fail is also removed —/tenantidis a real flag.Two configurations are still rejected, because the tool genuinely has no equivalent:
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 usingAppCert.AadAuthInfo:CertificateSubject). No subject flag exists; resolving the subject ourselves and forwarding a thumbprint would be guesswork about which certificate the user meant.Config option handling
Warn-and-ignore the harmless, hard-fail the meaningful:
gameAssets— all paths resolve to the same directory aspackageFilePathgameAssets— any path resolves elsewhereminutesToWaitForProcessingdeltaUploadavailabilityDate/preDownloadDateproductIdwithoutbigIdGetProductAsync. Only fails if the lookup genuinely can't produce one.--Authentication/--TenantIdgameAssetsis no longer[Required]on the attribute; the requirement is now enforced insideUploadXvcPackageOperationConfig.Validate()only when the package is not MSIXVC2, so XVC1 validation is unchanged. Both branches are tested.How
availabilityDate/preDownloadDateare appliedAn earlier revision of this PR hard-failed these two options, on the premise that
SetXvcConfigurationAsyncneeds the specificGamePackagethat 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/vrequired — early in the run, before the content transfer. AndSetXvcConfigurationAsynconly ever readsGamePackage.Id. That one line is the entire missing input.Msixvc2ProcessRunnernow returnsMsixvc2ProcessResult(ExitCode, UploadedPackageId)instead of a bare exit code."Package Id is "marker plusGuid.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 tonullrather than a guess.GetGamePackagesAsyncfor the target branch and market group. That lookup is required regardless —GamePackageResource.Idisinternal init, so the Application cannot fabricate aGamePackage— 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.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
JaswillTestcaptureda760372c-8c3b-4d15-bdef-57449a7cb4a6and 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
IsLikelyMsixvc2Packagemoves out ofPackageUploader.UI/Model/Xvc/XvcFile.csintoPackageUploader.ClientApi/Packaging/PackageFormatDetector.cs— ClientApi being the only project referenced by both the UI and the Application.XvcFile.IsLikelyMsixvc2Packagenow 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
Msixvc2ProcessRunnermirrors the UI:UseShellExecute = false,CreateNoWindow = true, stdout/stderr redirected and streamed through the existingILoggerso console users see progress, the operation'sCancellationTokenis honored (the child process tree is killed on cancel), the child's exit code is propagated as the operation's success/failure, andPACKAGEUPLOADER_MSIXVC2_DELEGATED=1is stamped on the child environment.Tests
src/PackageUploader.Application.Test/Tools/:Msixvc2DelegationGuardagainst the environment variable.AppSecret→/auth ClientSecretwith tenant/client/secret forwarded;AppCert→/auth ClientCertificatewith thumbprint/store/location;AzurePipelinesforwards the method with no credential flags;ManagedIdentityFederatedforwards/resourceid; missing secret, missing thumbprint, certificate file path, and certificate subject each throw with an actionable message./uploadsourceis absent from the built arguments.Msixvc2ProcessRunnerTestdrives 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.productId-only config → resolves and passes the Big ID to/storeid; unresolvable → clear error./msixvc2is absent from the built arguments.src/PackageUploader.ClientApi.Test/Packaging/PackageFormatDetectorTest.csports the detector coverage.Validation