Skip to content

Fix --uploadTimeout defaulting to 0 when the option is omitted - #163

Merged
isourabh merged 2 commits into
microsoft:mainfrom
tanmen:fix/upload-timeout-default
Aug 25, 2026
Merged

Fix --uploadTimeout defaulting to 0 when the option is omitted#163
isourabh merged 2 commits into
microsoft:mainfrom
tanmen:fix/upload-timeout-default

Conversation

@tanmen

Copy link
Copy Markdown
Contributor

Fixes #162.

Problem

UploadTimeoutOption declares a CustomParser that returns 100 for the empty-token case, but no DefaultValueFactory. System.CommandLine only invokes CustomParser when the option is present on the command line, so omitting it entirely yields default(long) — zero.

That zero reaches AzureBlobManager.UploadFileAsync:

blobClientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(uploadTimeout);

TimeSpan.FromSeconds(0) cancels every request the instant it starts, so msstore publish fails at Uploading Bundle to Azure blob: 0% after six retries — on every invocation that does not pass --uploadTimeout explicitly.

Fix

Add DefaultValueFactory = _ => DefaultUploadTimeoutSeconds so the documented 100-second default applies when the option is absent.

Worth noting: the CustomParser's empty-token branch can never cover that case. The option takes exactly one argument, so writing --uploadTimeout without a value is a parse error, not an empty-token parse. One of the new tests pins that behaviour so the two paths stay distinguishable.

I also lifted the 100 / 100000 literals into named constants, so the default, the range check and the error message cannot drift apart. Happy to drop that part if you would rather keep the diff to the single line.

Tests

8 new cases in PublishCommandUnitTests:

  • the option omitted → 100 (this is the regression)
  • the option present without a value → parse error
  • 100, 300, 100000 → used as given
  • 99, 100001, not-a-number → parse error

Verified the regression test actually catches the bug: with the DefaultValueFactory line removed, PublishCommandUploadTimeoutShouldDefaultWhenOptionIsOmitted fails; with it, all 8 pass.

The suite has 10 pre-existing failures on my machine (MSBuild / WinUI / settings related). They are identical with and without this change — 138 tests / 10 failures before, 146 tests / 10 failures after.

Why this was hard to spot

The console shows only Error while uploading the application package. — the exception goes to logger.LogError(ex, ...) while ansiConsole.WriteLine gets a bare sentence, so it reads as a network or service problem. The 0:00:00 only appears with --verbose. It cost several release runs before that was visible.

A guard rejecting a non-positive uploadTimeout in AzureBlobManager.UploadFileAsync would make this self-explanatory if it ever regresses. I left it out to keep this focused, but happy to add it.

UploadTimeoutOption declared a CustomParser returning 100 for the empty-token
case, but no DefaultValueFactory. System.CommandLine only invokes CustomParser
when the option is present on the command line, so omitting it entirely yielded
default(long) - zero.

That zero reaches AzureBlobManager.UploadFileAsync and becomes

    blobClientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(0);

which cancels every request the instant it starts. `msstore publish` then fails
at "Uploading Bundle to Azure blob: 0%" after six retries, on every invocation
that does not pass --uploadTimeout explicitly.

Adding DefaultValueFactory makes the documented 100 second default apply when
the option is absent, which is the case the CustomParser could never cover: the
option takes exactly one argument, so writing it without a value is a parse
error rather than an empty-token parse. The new tests cover that too.

Also lifted the 100 / 100000 literals into named constants so the default, the
range check and the error message cannot drift apart.

Tests: 8 new cases in PublishCommandUnitTests. Verified that
PublishCommandUploadTimeoutShouldDefaultWhenOptionIsOmitted fails without the
DefaultValueFactory line and passes with it. The 10 pre-existing failures in the
suite (MSBuild / WinUI / settings) are unchanged by this commit.

Fixes microsoft#162
@tanmen

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

// applied when the option was left out altogether.
var parseResult = ParsePublish("publish", ".", "--uploadTimeout");

parseResult.Errors.Should().NotBeEmpty();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can you add asset on the error message containing some text ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This test means that

if (result.Tokens.Count == 0)
                    {
                        return DefaultUploadTimeoutSeconds;
                    }

is not reached so we should remove that is that code is never reachable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. This one now asserts the error is the parser's own missing-value error rather than just being non-empty. It only matches on the option name — the rest of that message comes from System.CommandLine and is localized, so asserting the full English string fails on a non-English machine (it came back in Japanese on mine). It also asserts the message is not the range error, which is what makes it evidence for the point below.

🤖 Addressed by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right — removed.

I checked it against System.CommandLine 2.0.10 rather than inferring it from the test, and the branch is dead for two independent reasons:

  1. Arity. ArgumentArity.Default only returns ZeroOrOne for a non-boolean, non-collection argument when parent is Command. An option's argument has the Option as its parent, so it falls through to ExactlyOne — a value-less --uploadTimeout is rejected by the parser before CustomParser runs.
  2. Omitted option. In ArgumentResult.ValidateAndConvert, the Argument.HasDefaultValue && Parent.UseDefaultValueFor(this) check returns the DefaultValueFactory value and returns before Argument.ConvertArguments (the CustomParser) is reached. So omitting the option never enters the parser either.

Confirmed empirically too: with the branch gone, result.Tokens.Single() would throw on an empty token list, and the omitted-option test passes rather than throwing.

I kept ShouldRequireAValueWhenTheOptionIsPresent, since it is the thing that demonstrates the branch is unreachable — and left a comment on the option noting that a "no tokens" branch has to come back if the arity is ever relaxed to ZeroOrOne.

🤖 Addressed by Claude Code

{
var parseResult = ParsePublish("publish", ".", "--uploadTimeout", seconds);

parseResult.Errors.Should().NotBeEmpty();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here, can you add asset on the error message containing some text?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. This now asserts the exact message: Invalid seconds value. The value must be between 100 and 100000. It is built from MinUploadTimeoutSeconds/MaxUploadTimeoutSeconds rather than hard-coded, so the test follows the constants if the range is ever changed. Both constants are now internal for that reason.

🤖 Addressed by Claude Code

Comment thread MSStore.CLI/Commands/PublishCommand.cs Outdated
Comment on lines 115 to 118
if (result.Tokens.Count == 0)
{
return 100;
return DefaultUploadTimeoutSeconds;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is this code reachable ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, it is not reachable — removed in 799a67a.

The option's arity is ExactlyOne (ArgumentArity.Default only relaxes to ZeroOrOne when the argument's parent is a Command, which is not the case for an option's argument), so --uploadTimeout with no value is a parse error before CustomParser runs. And when the option is omitted entirely, ArgumentResult.ValidateAndConvert returns the DefaultValueFactory value before it ever reaches ConvertArguments. Either way the parser is never called with zero tokens.

Note this branch was also not what made the omitted case work — that is exactly the bug this PR fixes: without a DefaultValueFactory the omitted case never reached this code at all and yielded default(long) = 0. Verified by removing the factory locally and watching the omitted-option test fail with found 0L.

🤖 Addressed by Claude Code

… branch

Assert the actual parse error text in the two --uploadTimeout tests instead of
only checking that Errors is non-empty. The range test compares against a string
built from MinUploadTimeoutSeconds/MaxUploadTimeoutSeconds so it follows the
constants if the range changes; those two are now internal for that reason. The
missing-value test only asserts the option name, because that message comes from
System.CommandLine and is localized.

Remove the CustomParser's "no tokens" branch. It is unreachable for two
independent reasons: the option's arity is ExactlyOne, so a value-less
--uploadTimeout is rejected before the parser runs, and ArgumentResult returns
the DefaultValueFactory value before ConvertArguments when the option is
omitted. A comment records that the branch must come back if the arity is ever
relaxed to ZeroOrOne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — I reproduced the bug and validated the fix locally.

Reverting PublishCommand.cs to the merge-base makes PublishCommandUploadTimeoutShouldDefaultWhenOptionIsOmitted fail with found 0L, and it passes on HEAD, so the regression test genuinely pins the bug. Full suite on net10.0: 146 total, 136 passed, 10 skipped (OS-conditional), 0 failed. Release build with TreatWarningsAsErrors is clean.

The root-cause analysis is accurate — System.CommandLine 2.0.10 doesn't invoke CustomParser for an absent option, so the old Tokens.Count == 0 branch was dead and default(long) flowed into BlobClientOptions.Retry.NetworkTimeout. Removing that branch is the right call, and the comment explaining why it's gone (plus the arity caveat) is worth keeping. Nice bonus that UploadTimeoutOption is a shared static, so msstore init --publish gets the fix too.

Two non-blocking notes:

1. Sibling options still carry the same dead branch. PackageRolloutPercentageOption and InputDirectoryOption both still have unreachable Tokens.Count == 0 branches. They're harmless today because default(T) is null for both, and downstream treats packageRolloutPercentage == null as "no rollout" (IStorePackagedAPIExtensions.cs:487). Mostly flagging it so a later cleanup doesn't "fix" them by mirroring this PR — adding DefaultValueFactory = _ => 100f to the rollout option would silently force 100% rollout on every publish.

2. GetRequiredValue would be more idiomatic now. With a DefaultValueFactory in place, PublishCommand.cs:170 and InitCommand.cs:168 could use GetRequiredValue the way NoCommitOption already does. GetValue works correctly, so this is purely a consistency nit.

@isourabh
isourabh merged commit ffb1aaf into microsoft:main Aug 25, 2026
10 checks passed
ififi2017 pushed a commit to ififi2017/Off-Work-Countdown that referenced this pull request Sep 2, 2026
This reverts PR #68. The explicit --uploadTimeout was a workaround for
microsoft/msstore-cli#162, where an omitted --uploadTimeout resolved to 0
and cancelled every Azure blob request immediately.

That bug is fixed by microsoft/msstore-cli#163 and shipped in msstore-cli
v0.4.2 (2026-09-02). The action installs 'latest', so the workaround is
no longer needed and the documented 100s default applies again.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a33ffb27-8072-43c0-beb7-a7bc7e8e2229
Shane Weaver (shanebweaver) pushed a commit to shanebweaver/CaptureTool that referenced this pull request Sep 4, 2026
This reverts PR #478. The explicit --uploadTimeout 300 worked around
microsoft/msstore-cli#162, where an omitted --uploadTimeout resolved to 0
and cancelled every Azure blob request immediately.

Fixed upstream by microsoft/msstore-cli#163 and released in msstore-cli
v0.4.2 (2026-09-02). This workflow installs the CLI via
microsoft/microsoft-store-apppublisher@v1.4 with the default version
'latest', so it now picks up the fix and the documented 100s default
applies again.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a33ffb27-8072-43c0-beb7-a7bc7e8e2229
James Montemagno (jamesmontemagno) pushed a commit to jamesmontemagno/tiny-clips that referenced this pull request Sep 4, 2026
This reverts PR #321. The explicit --uploadTimeout 900 worked around
microsoft/msstore-cli#162, where an omitted --uploadTimeout resolved to 0
and cancelled every Azure blob request the instant it started.

Fixed upstream by microsoft/msstore-cli#163 and released in msstore-cli
v0.4.2 (2026-09-02). This workflow installs the CLI via
microsoft/microsoft-store-apppublisher@v1.4 with the default version
'latest', so it now picks up the fix and the documented 100s default
applies when the option is omitted.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a33ffb27-8072-43c0-beb7-a7bc7e8e2229
Jeremy [KK7GWY] (ten9876) pushed a commit to aethersdr/AetherSDR that referenced this pull request Sep 6, 2026
The pin sat on `v0.4.1`, the last release carrying
[microsoft/msstore-cli#162](microsoft/msstore-cli#162).
That is fixed by
[#163](microsoft/msstore-cli#163) and released
in **v0.4.2** on 2026-09-02 — the exact condition
`docs/WINDOWS-STORE-MSIX.md` set for advancing the pin:

> advance that pin only after validating a released version containing
microsoft/msstore-cli#163.

### This is deliberately not a revert of #5345

#5345 was mostly Qt symbol packaging — `stage-debug-symbols.ps1`,
`check-symbol-package.ps1`, the `.appxsym` topology work. Only one of
its five commits (`ae57707`) was the timeout workaround, and only the
pin and its surrounding prose change here.

### `-UploadTimeoutSeconds 300` stays — and it matters here

This one is not just habit. `AzureBlobManager` calls
`blobClient.UploadAsync` **without setting `StorageTransferOptions`**,
so the SDK's default 256 MiB `InitialTransferSize` applies and a
`.msixupload` under that size is sent as a **single PUT**.
`Retry.NetworkTimeout` therefore has to cover the entire transfer, not
one chunk.

Your published `.msixupload` is **~197 MB** (v26.9.1), and #5345 adds
symbols on top. At the CLI's restored 100 s default that would demand a
sustained **~2 MB/s for the whole request** — genuinely tight, and it
would fail in exactly the same `Uploading Bundle to Azure blob: 0%`
shape as #162 did, which would be a miserable thing to re-diagnose.

So the 300 s value is kept and re-documented as a deliberate sizing
decision rather than a bug workaround.

### Net effect

- `version: v0.4.1` → `v0.4.2`
- `docs/WINDOWS-STORE-MSIX.md`: the timeout paragraph rewritten, plus
the v0.4.1 mention in the numbered flow
- `publish-store.ps1`: `.PARAMETER UploadTimeoutSeconds` help rewritten

No behaviour change beyond the version bump. The note about keeping
verbose logging off in public Actions logs is preserved verbatim.

Sent as part of a sweep across the repos that referenced
msstore-cli#162.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a33ffb27-8072-43c0-beb7-a7bc7e8e2229
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.

--uploadTimeout defaults to 0 when omitted, so every blob upload fails

3 participants