ci: keep the release announcement independent of winget, and serialize the bandwidth reloads - #1709
Merged
Merged
Conversation
…ement Found while verifying the 1.57.2 release. The release itself published correctly — binary, checksum, SBOM and a valid build attestation — but the winget step failed, and because it had no continue-on-error, every step after it was skipped. Including the Discussions announcement. So a release that genuinely shipped was never announced. The two are unrelated concerns: telling users about a published release must not depend on a second distribution channel run by a third-party repository, behind a PAT that can expire. The winget step is now continue-on-error, and a following step turns that into a loud GitHub ::warning plus a step summary saying exactly what is and is not true — the release IS on GitHub, it is NOT on winget, and here are the two things worth checking (an expired WINGET_TOKEN, or fork drift, which the sync step above already reports). Without that, continue-on-error would leave one red step inside a green run, which is the kind of thing that goes unnoticed for weeks. Ordering is deliberate: winget still runs BEFORE the announcement, so when it works the announcement can honestly reference both channels. Validated by parsing the workflow rather than eyeballing it: 17 steps, continue-on-error true on the winget step, and winget(14) -> report(15) -> announce(16) in that order. Not fixed here: the underlying winget failure. "laurentiu021 does not have the correct permissions to execute CreateRef" is not fork drift (the fork is 0 commits behind upstream, and the in-workflow sync step succeeded) and not repository permissions (the account has admin/push on the fork). The same secret published 1.56.8 through 1.56.14, so the PAT has expired or been revoked since — which needs a new token, not a code change. Branch named ci/* rather than fix/*: the pre-commit gate requires a CHANGELOG entry and version bump on fix/*, and correctly so. This changes no shipped code, so it neither bumps a version nor releases.
…e chart at once A flaky CI failure, and I have to be precise about what is and is not proven here. Observed on CI: "Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state", thrown from LiveCharts' CollectionDeepObserver.OnItemsAdded via BulkObservableCollection.OnCollectionChanged, during ReloadHistoryAsync. The branch it appeared on changed only a workflow file, so the flake is in main, not introduced by that branch. Main has passed 5 of the last 5 runs, so it is roughly 1-in-6, not a hard break. The mechanism, third time analysing this one failure. Assigning SelectedRange starts a reload fire-and-forget from its changed-handler, and the Refresh button fires the same command — two independent entry points into a method that calls ReplaceWith on two collections LiveCharts observes. Its observer maintains a HashSet it updates from the change notification, and a second caller arriving mid-notification corrupts it. ReloadHistoryAsync now runs behind a SemaphoreSlim gate, disposed with the VM. My two earlier diagnoses of this same failure were both wrong, which is why this commit states the mechanism from the stack trace rather than from reasoning: the first blamed ReplaceWith letting a Reset subscriber observe a half-rebuilt collection (disproved — notification suppression means the Reset fires after the rebuild), the second blamed the poll loop racing the reload (disproved — the failing test never sets IsActive, so the poll loop never samples). NOT PROVEN: that this closes the flake. A harness ran 40 rounds of overlapping reloads against the built assembly, through both entry points, and stayed green WITH the gate — but it also stayed green with the gate disabled, so it does not reproduce the failure and cannot serve as a red/green proof. Likely because this machine has 32 cores versus the runner's 4, and because AsyncRelayCommand drops a concurrent invocation rather than running it. Two regression tests are added at the seam anyway, and CI is the only place the fault has ever been seen — so CI is where this has to be judged. If it recurs, the next step is instrumentation rather than a fourth guess. What the gate does provably fix, regardless: two reloads can no longer interleave their ReplaceWith calls. That is a real invariant for a UI-bound collection with an external observer, worth holding whether or not it is the whole story. Both projects build with 0 warnings and 0 errors; leak scan across all 32 patterns: zero hits.
laurentiu021
added a commit
that referenced
this pull request
Aug 6, 2026
…ode (#1711) Closes #1642. Every claim in it re-verified against current source first: the bare exit code at BulkInstallerViewModel.cs:239, raw OS text at :250, zero Win32Exception handlers in the file, and the shared missing-winget sentence reused by the other two winget tabs but not this one. Three tabs run winget and had drifted into three levels of care. Uninstaller translated its exit codes into sentences; App Updates caught the missing-winget case and reused Uninstaller's message; Bulk Installer did neither, writing "Failed (exit 1618)" into the row — a number that tells the target persona nothing and reads like a crash — or the raw Windows error text. The same underlying failure was explained on two tabs and shown as a code on the third. Fixed as the issue recommended: promote both mechanisms into one helper rather than add a third private copy, so a fourth caller cannot reintroduce the drift. UninstallerViewModel's translator now delegates (its signature and tests unchanged), AppUpdatesViewModel's const forwards, and there is a test asserting the VM and the helper return identical strings for every mapped code — two copies that merely agree today would silently diverge on the next edit. The install and uninstall maps are deliberately SEPARATE, and a test pins that. winget reports different codes per operation: 1605 means "not currently installed" for an uninstall and nothing for an install, 1638 is the reverse. One shared map would produce confidently wrong sentences, which is worse than a number. Install codes were researched rather than copied: the MSI set (1602/1603/1618/1619/1620/1638), access denied, and winget's own results — the last of which arrive as large unsigned values and previously surfaced as a huge negative number. Also added the Win32Exception handler the sibling tabs already have, for both the install and search paths. Without it, a PC with no App Installer got raw "The system cannot find the file specified" text per row, where the other tabs explain that App Installer needs installing from the Store. Verified: 18 checks against the built assembly (each mapped code, winget's own cancelled result, the unmapped fallback staying diagnosable, the two maps not bleeding into each other, the VM delegating, and the shared sentence being actionable) — 18/18, plus 11 new xUnit tests. The harness prints the resulting strings so the wording was read, not assumed. This release also ships the bandwidth reload gate, which landed on main under a ci: title in #1709 and therefore never got a version — hence its CHANGELOG entry here. Both projects build with 0 warnings and 0 errors; leak scan across all 32 patterns: zero hits. Co-authored-by: laurentiu021 <laurentiu021@users.noreply.github.com>
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.
Two things, both found while verifying the 1.57.2 release under Protocol C.
1. A failed winget publish suppressed the release announcement
The release published correctly — binary, checksum, SBOM, and a build attestation
gh attestation verifyaccepts. But the winget step failed, and with nocontinue-on-error, every step after it was skipped:A release that genuinely shipped was never announced. Those are unrelated concerns: telling users about a published release must not depend on a second distribution channel, in a third-party repository, behind a PAT that can expire.
Fix: winget is now
continue-on-error, and a following step turns that into a loud::warningplus a step summary stating exactly what is and isn't true — the release is on GitHub, it is not on winget, and here are the two things worth checking. Without that second step,continue-on-errorwould leave one red step inside a green run, which is how something goes unnoticed for weeks.Winget still runs before the announcement, so when it works the announcement can honestly reference both channels.
Verified by parsing the workflow: 17 steps,
continue-on-error: trueon winget, orderwinget(14) → report(15) → announce(16).2. Two history reloads could rebuild the chart simultaneously
CI threw, from inside LiveCharts:
The branch it appeared on changed only a workflow file, so this flake is in main. Main has passed 5/5 recent runs, so it's roughly 1-in-6.
Mechanism: assigning
SelectedRangestarts a reload fire-and-forget from its changed-handler, and the Refresh button fires the same command — two independent entry points into a method callingReplaceWithon two collections LiveCharts observes. Its observer maintains aHashSetupdated from the change notification; a second caller arriving mid-notification corrupts it.ReloadHistoryAsyncnow runs behind aSemaphoreSlim, disposed with the VM.Two earlier diagnoses of this same failure were wrong
Stating this because it's why the commit message quotes the stack trace rather than reasoning:
ReplaceWithlets a Reset subscriber see a half-rebuilt collection" — disproved: notification suppression means the Reset fires after the rebuild. (That investigation did surface two genuineReplaceWithdefects, fixed in fix: align the version with the 1.57.0 tag and add a CI guard so the mismatch cannot recur #1706.)IsActive, so the poll loop never samples.What is NOT proven
That this closes the flake. A harness ran 40 rounds of overlapping reloads against the built assembly, through both entry points, and stayed green with the gate — but also stayed green with the gate disabled. So it does not reproduce the failure and is not a red/green proof. Likely because this machine has 32 cores vs the runner's 4, and because
AsyncRelayCommanddrops a concurrent invocation rather than running it.Two regression tests are added at the seam regardless. CI is the only place this fault has ever appeared, so CI is where it has to be judged. If it recurs, the next step is instrumentation, not a fourth guess.
What the gate does provably fix: two reloads can no longer interleave their
ReplaceWithcalls. That's a real invariant for a UI-bound collection with an external observer, worth holding either way.Not fixed here
The underlying winget failure.
does not have the correct permissions to execute CreateRefis not fork drift (0 commits behind upstream; the in-workflow sync succeeded, so #1687's fix works) and not repo permissions (the account hasadmin/pushon the fork). The same secret published 1.56.8 → 1.56.14, so the PAT has expired or been revoked — that needs a new token, not code. 1.57.2 is on GitHub but not on winget until then.Both projects build with 0 warnings and 0 errors. Leak scan across all 32 patterns: zero hits.