Skip to content

fix(self-update): a check that finds nothing must SAY so, and a dead event channel must not freeze an install (#2553, #2494) - #2657

Merged
rbuergi merged 2 commits into
mainfrom
fix/self-update-cluster
Aug 29, 2026
Merged

rbuergi merged 2 commits into
mainfrom
fix/self-update-cluster

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Fixes the two halves of the same defect: a self-update check could complete having reported nothing, and the check could stop happening at all without reporting that either. An install three builds behind and an install perfectly up to date produced identical evidence — healthy pods, no errors, an Updates tab reading "No newer version detected yet" — which is why memex sat 7 h and 3 builds behind while every signal said it was fine.

Refs #2553, #2494. Neither issue is closed by this PR: both carry a deployment half that is not mine to do (below).

#2553 — every check reports exactly one verdict

RunOnce returned IObservable<Unit>, so completing with no emission was a legitimate result. Two paths did exactly that, and they are the two most common outcomes a healthy install has:

.Where(content => content.Policy != UpdatePolicyKind.None)   // ← policy None: whole check discarded
.Where(target => !string.IsNullOrEmpty(target))              // ← nothing newer: whole check discarded
  • RunOnce now returns a SelfUpdateVerdict. Every branch names its outcome, so "we asked and the answer was no" is a different observation from "nothing ever asked".
  • The postcondition is asserted, not hoped for — following the framework-mvid.txt / _complete precedent. DefaultIfEmpty(SelfUpdateVerdict.NoOutcome()) means a pipeline that somehow produces nothing reports that, at Warning, naming itself as a defect in this service. A future filter cannot re-create the silence.
  • The verdict is logged AND stamped on Admin/UpdatePolicy (LastCheckedAt / LastCheckVerdict / LastCheckTrigger). Both, and the reason is the measured one: every line this service had to say was LogInformation, and the portal image's appsettings.json caps its logger prefix at Warning with no entry for this category (see "Deployment half" below). A log-only fix would have shipped straight back into the same void.
  • The Updates tab stops asserting a fact it never established. StatusMarkdown returned ui.updateNoNewerVersion whenever LatestAvailableTag was empty — which is also the state of an install that has never checked. Those are now two different sentences.

The bookkeeping write cannot feed itself: the trigger reads policy.DistinctUntilChanged(c => c.Policy) and the source is DistinctUntilChanged((Policy, RequireCiGreen)), so a write touching only these three fields emits nothing downstream — the #223 write-storm shape is structurally excluded rather than avoided by convention.

#2494 — a bounded safety net, and the first report that can name a dead channel

SelfUpdate:SafetyNetCheckInterval (default 01:00:00, 00:00:00 disables), rendered by the chart and declared in values.aks.yaml.

This is not the removed poll returning. A poll drives the update; this bounds how long a broken driver can hide. It passes through MinRollInterval like any other check, so it can only ever shorten discovery — never change the roll cadence. Events stay the fast path and still decide the latency.

The justification is the failure mode, not the latency: every event reaches an install across a chain nothing re-verifies — a GitHub webhook registration, a WebhookInbox allowlist slot, an HMAC secret that must match byte-for-byte — and every joint of it fails silently (an unlisted target answers 404 exactly like a wrong URL; a mismatched secret still answers 2xx and drops the delivery).

And the diagnosis half: a safety-net check that finds a release on an install that has seen no build-completion event at all warns and names the chain to check. All three clauses matter — "no events yet" alone is routine on an install whose modules rarely build, and warning on it would train people to ignore the line that means something. ABuildCompletionDrivenCheck_NeverWarnsAboutTheEventChannel is the control.

Tests — run against unfixed code FIRST

SelfUpdateCheckVerdictTest (6 tests) asserts the delivery: a log line that reached a logger, or a field that landed on the node. Never a return value — asserting the thing that already lied is worthless.

Against origin/main (ecef6dbd9) in a clean worktree, with only the two edits needed to compile there (SafetyNetCheckInterval and the SelfUpdateTrigger enum do not exist yet; the node-stamp test was dropped for the same reason):

ACheckThatFindsNothingNewer_SaysSoAnyway                                  [FAIL] 30s TimeoutException
APolicyOfNone_IsADecisionThatReportsItself                                [FAIL] 30s TimeoutException
WithNoBuildEventAtAll_TheInstallStillChecksAgain                          [FAIL] 30s TimeoutException
ASafetyNetCheckThatFindsAReleaseNobodyAnnounced_WarnsAboutTheEventChannel [FAIL] 30s TimeoutException
ABuildCompletionDrivenCheck_NeverWarnsAboutTheEventChannel                [FAIL] 30s TimeoutException

Five of five time out waiting for a verdict line that pre-fix code never emits — which is the issue, reproduced.

On this branch:

SelfUpdateCheckVerdictTest           6/6 pass
All *SelfUpdate* (Hosting.Monolith) 28/28 pass
Memex.Portal.Shared.Test           426/426 pass   (+ SelfUpdateVerdictTest, 12 pure tests)
MeshWeaver.Documentation.Test       97/97 pass
deploy/aks/scripts/check-values-are-read.sh — 120 keys, all read

ComboVerdictRecordingTest.StatusMarkdown_NoTagAtAll_SaysNothingDetected was replaced, not adjusted: it pinned the claim that has no evidence behind it. It is now two tests, one per fact.

🚨 The deployment half — not in this PR, and not mine to do

  1. Memex.Portal.Shared.SelfUpdate has no log-level entry. MeshWeaver.Plugins/src/Memex.Portal.Distributed/appsettings.json sets "Memex": "Warning" and explicitly surfaces the sibling Memex.Portal.Shared.ShippedReleaseSeedHostedService at Information — but nothing for this service. That is the direct cause of memex self-update is silent: 3 builds behind, roll floor satisfied 5h ago, and ZERO SelfUpdate log lines in 6.7h #2553's "ZERO SelfUpdate log lines": the mechanism was working and reporting into a void. The node stamp above makes the verdict readable without it; the entry is still worth adding, in that repo.
  2. memex lists zero webhook targets (WebhookInbox__Targets__0/1 both "", per 🚨 Prod cannot roll: CD's announcement 404s and self-update is EVENT-DRIVEN (no poll) — PLATFORM_WEBHOOK_URL ≠ memex WebhookInbox:Targets #2494's last comment). This PR bounds the damage; it does not fix the channel.

🤖 Generated with Claude Code

…event channel must not freeze an install (#2553, #2494)

Two issues, one defect seen from both ends: a self-update check could complete
having reported NOTHING, and the check could stop happening at all without
reporting that either. An install three builds behind and an install perfectly
up to date produced identical evidence — healthy pods, no errors, an Updates tab
reading "No newer version detected yet" — so nobody could tell them apart.

#2553 — every check now reports exactly one verdict

* RunOnce returns a SelfUpdateVerdict, not Unit. The two paths that used to be
  bare Rx `Where` clauses — a policy of None, and "no candidate is newer" —
  were the two most silent paths in the service; they now name their outcome.
* DefaultIfEmpty(NoOutcome) is the postcondition asserted rather than hoped for:
  a pipeline that somehow produces nothing reports THAT, at Warning, naming
  itself as a defect in this service.
* The verdict is both logged AND stamped on Admin/UpdatePolicy
  (LastCheckedAt / LastCheckVerdict / LastCheckTrigger). Both, because a log
  line depends on a per-category log level a deployment may simply not have set
  — which is exactly what happened: every line this service had to say was
  LogInformation, and the portal image caps its logger prefix at Warning.
* The Updates tab stops asserting a fact it never established. "No newer version
  detected yet" is now said only by an install that actually completed a check;
  one that has not says so instead.

#2494 — a bounded safety net, and the first report that can name a dead channel

* SelfUpdate:SafetyNetCheckInterval (default 1h, 0 disables), rendered by the
  chart. NOT the removed poll: it passes through MinRollInterval like any other
  check, so it can only shorten DISCOVERY, never change the roll cadence.
  Events stay the fast path; this bounds how long a broken driver can hide.
* A safety-net check that finds a release on an install that has seen NO
  build-completion event warns and names the chain to check. All three clauses
  matter: "no events yet" alone is routine and warning on it would train people
  to ignore the line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR fixes self-update observability and liveness by ensuring every self-update check yields an explicit verdict, persisting that verdict durably, and adding a bounded safety-net interval so a dead build-event channel cannot stall updates indefinitely.

Changes:

  • Introduces SelfUpdateVerdict/SelfUpdateOutcome/SelfUpdateTrigger and updates SelfUpdateHostedService to always emit, log, and record exactly one verdict per check (including “no newer release” and “updates disabled”).
  • Adds SafetyNetCheckInterval to periodically re-check when events never arrive, plus warning diagnostics when the safety net finds a newer release but no build events were ever observed.
  • Updates UI copy and localization to distinguish “never checked” from “checked and up to date,” and adds/updates tests plus deployment config keys/docs.

Reviewed changes

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

Show a summary per file
File Description
test/MeshWeaver.Hosting.Monolith.Test/SelfUpdateCheckVerdictTest.cs Adds integration tests asserting per-check verdict delivery via logs/node stamps and safety-net behavior.
test/Memex.Portal.Shared.Test/SelfUpdateVerdictTest.cs Adds unit tests for verdict factories and key predicates (e.g., FoundNewerRelease).
test/Memex.Portal.Shared.Test/ComboVerdictRecordingTest.cs Updates UI rendering tests to separate “never checked” vs “checked and no newer.”
src/MeshWeaver.Messaging.Hub/Localization/strings.en.json Adds ui.updateNeverChecked string.
src/MeshWeaver.Messaging.Hub/Localization/strings.de.json Adds German translation for ui.updateNeverChecked.
src/MeshWeaver.Hosting/SelfUpdate/SelfUpdateOptions.cs Adds SafetyNetCheckInterval option and documentation.
memex/Memex.Portal.Shared/Settings/UpdatePolicySettingsTab.cs Changes status text logic for empty tag to account for “never checked.”
memex/Memex.Portal.Shared/SelfUpdate/UpdatePolicyNodeType.cs Persists LastCheckedAt / LastCheckVerdict / LastCheckTrigger on policy node.
memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateVerdict.cs Adds verdict/outcome/trigger types and message factories.
memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateHostedService.cs Refactors pipeline to produce verdicts, add safety-net ticks, and record/report outcomes.
deploy/helm/templates/memex-portal/config.yaml Wires SelfUpdate__SafetyNetCheckInterval with a safe default.
deploy/aks/values.aks.yaml Documents and sets default SelfUpdate__SafetyNetCheckInterval.
deploy/aks/SELF-UPDATE.md Updates operational documentation for event-driven checks + safety net + verdict stamping.
.github/workflows/main-cd.yml Updates CD “healed” issue comment to reflect event/safety-net pickup.
Suppressed comments (1)

memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateHostedService.cs:1

  • .IgnoreElements().Select(_ => SelfUpdateVerdict.NoOutcome()) is intentionally unreachable and is only being used to coerce the observable’s generic type before Concat. That makes the pipeline harder to reason about (and invites copy/paste of a no-op mapping). A clearer option is to use an operator expressly for type coercion while emitting nothing (e.g., cast/empty transformation) so the sequencing intent ('run RecordAvailable, then GateThenApply') is obvious without relying on unreachable code.
using Microsoft.Extensions.Configuration;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +179 to 184
return content.LastCheckedAt is { } checkedAt
? localize("ui.updateNoNewerVersion", [])
+ " " + localize("ui.updateCheckedAt",
[DisplayTimeExtensions.ToDisplayTime(checkedAt, zoneId).ToString("yyyy-MM-dd HH:mm")])
: localize("ui.updateNeverChecked", []);

Comment on lines +48 to +55
/// <summary>
/// How many build-completion events this process has observed. The ONLY evidence that the event
/// channel is alive, and it exists solely to make the dead-channel report in
/// <see cref="ReportCheck"/> possible — nothing else can distinguish "nothing published" from
/// "something published and nothing told us". Mutable instance state, read with
/// <see cref="Volatile"/> because the watch and the check run on different pool threads.
/// </summary>
private int _buildEventsSeen;
BuildCompletionTicks(),
policy.DistinctUntilChanged(content => content.Policy).Skip(1).Select(_ => -3L)))
Observable.Return(SelfUpdateTrigger.Startup),
BuildCompletionTicks().Do(_ => Interlocked.Increment(ref _buildEventsSeen)),
Comment thread deploy/aks/values.aks.yaml Outdated
# and every joint of it fails SILENTLY. An install whose event channel is dead
# is indistinguishable from one that is up to date: healthy pods, no errors, a
# check that simply never runs. memex sat three builds behind for 7 h exactly
# so. SelfUpdate__SafetyNetCheckInterval (default 1h; empty or 0 disables)
Same trap the MinRollInterval comment three lines up already documents: the key
binds to a TimeSpan, so an empty string is a FormatException in the
configuration binder, not "inert". Say 00:00:00.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

493 tests  +3   489 ✅ +3   4m 7s ⏱️ -34s
  6 suites ±0     4 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit 2827487. ± Comparison against base commit ecef6db.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

907 tests  ±0   907 ✅ ±0   5m 48s ⏱️ +16s
  6 suites ±0     0 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit 2827487. ± Comparison against base commit ecef6db.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

931 tests   - 26   931 ✅  - 26   4m 50s ⏱️ -19s
  6 suites ± 0     0 💤 ± 0 
  6 files   ± 0     0 ❌ ± 0 

Results for commit 2827487. ± Comparison against base commit ecef6db.

This pull request removes 84 and adds 58 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.SelfUpdateRollFloorTest ‑ AFreshRoll_DefersTheNextOne
MeshWeaver.Hosting.Monolith.Test.SelfUpdateRollFloorTest ‑ AZeroFloor_NeverHolds
MeshWeaver.Hosting.Monolith.Test.SelfUpdateRollFloorTest ‑ AnOldStampRollsImmediately_TheCrashRecoveryCase
MeshWeaver.Hosting.Monolith.Test.SelfUpdateRollFloorTest ‑ NeverRolled_IsNotHeld
MeshWeaver.Hosting.Monolith.Test.SilentReadNackTest ‑ AnsweredByTheOwner_AcceptsEveryOwnerTerminal_AndRefusesRoutings
MeshWeaver.Hosting.Monolith.Test.SilentReadNackTest ‑ GetMeshNode_WhenOwnerIsDisposedMidRead_RecoversOnTheReProbe
MeshWeaver.Hosting.Monolith.Test.SilentReadNackTest ‑ LiveOwner_WithASilentSource_IsNotNacked
MeshWeaver.Hosting.Monolith.Test.SilentReadNackTest ‑ OwnerDisposedWithReadOutstanding_IsNacked_NotLeftHanging
MeshWeaver.Hosting.Monolith.Test.SourceSnapshotEstablishmentTest ‑ EveryOtherCompileFailure_StillStampsError
MeshWeaver.Hosting.Monolith.Test.SourceSnapshotEstablishmentTest ‑ FailedProbe_DoesNotVeto_AHealthyCachedAnswerThatArrivesLater
…
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ Failed_availability_write_never_blocks_the_roll_forward
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ MidLife_fault_reestablishes_silently_and_never_reapplies_the_default_policy
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ Poller_survives_faulted_first_read_and_processes_a_policy_change
MeshWeaver.Hosting.Monolith.Test.ShutdownRoutingRejectClassificationTest ‑ RoutingRejectWhileMeshIsShuttingDown_IsTransientShuttingDown_NotTerminalFailed
MeshWeaver.Hosting.Monolith.Test.SourceDiscoveryUnavailableTest ‑ StarvedSourceDiscovery_IsNotACompileVerdict_WhileARealCompileErrorStillIs
MeshWeaver.Hosting.Monolith.Test.SpaceCreateAtomicityTest ‑ FailedCreatorGrant_LeavesNoOrphanGrantEither
MeshWeaver.Hosting.Monolith.Test.SpaceCreateAtomicityTest ‑ FailedCreatorGrant_RollsBackTheRow_AndTheCreateStaysRetryable
MeshWeaver.Hosting.Monolith.Test.SpaceEditableTitleTest ‑ SpaceOverview_RendersClickToEditTitle_ForEditor
MeshWeaver.Hosting.Monolith.Test.SpaceWelcomeTest ‑ Space_HasBodyProperty_ForUserEditableMarkdown
MeshWeaver.Hosting.Monolith.Test.SpaceWelcomeTest ‑ WelcomeMarkdown_InvitesUserToPersonalizeAndChat
…

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 065 tests  +45   1 065 ✅ +45   6m 44s ⏱️ +19s
    6 suites ± 0       0 💤 ± 0 
    6 files   ± 0       0 ❌ ± 0 

Results for commit 2827487. ± Comparison against base commit ecef6db.

This pull request removes 59 and adds 104 tests. Note that renamed tests count towards both.
Memex.Portal.Shared.Test.ComboVerdictRecordingTest ‑ StatusMarkdown_NoTagAtAll_SaysNothingDetected
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ Failed_availability_write_never_blocks_the_roll_forward
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ MidLife_fault_reestablishes_silently_and_never_reapplies_the_default_policy
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ Poller_survives_faulted_first_read_and_processes_a_policy_change
MeshWeaver.Hosting.Monolith.Test.ShutdownRoutingRejectClassificationTest ‑ RoutingRejectWhileMeshIsShuttingDown_IsTransientShuttingDown_NotTerminalFailed
MeshWeaver.Hosting.Monolith.Test.SourceDiscoveryUnavailableTest ‑ StarvedSourceDiscovery_IsNotACompileVerdict_WhileARealCompileErrorStillIs
MeshWeaver.Hosting.Monolith.Test.SpaceCreateAtomicityTest ‑ FailedCreatorGrant_LeavesNoOrphanGrantEither
MeshWeaver.Hosting.Monolith.Test.SpaceCreateAtomicityTest ‑ FailedCreatorGrant_RollsBackTheRow_AndTheCreateStaysRetryable
MeshWeaver.Hosting.Monolith.Test.SpaceEditableTitleTest ‑ SpaceOverview_RendersClickToEditTitle_ForEditor
MeshWeaver.Hosting.Monolith.Test.SpaceWelcomeTest ‑ Space_HasBodyProperty_ForUserEditableMarkdown
…
Memex.Portal.Shared.Test.ComboVerdictRecordingTest ‑ StatusMarkdown_NoTagAndNoCheckEverCompleted_SaysNoCheckHasRun
Memex.Portal.Shared.Test.ComboVerdictRecordingTest ‑ StatusMarkdown_NoTagButACheckDidRun_SaysNothingNewerAndWhen
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ AHoldWithNoReason_StillNamesTheTagAndTheHold
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ EveryOutcome_HasAFactoryThatProducesIt
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: Applied, expected: True)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: CheckFailed, expected: False)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: Deferred, expected: True)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: DetectOnly, expected: True)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: Held, expected: True)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: NoNewerRelease, expected: False)
…

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

   10 files  ±0     10 suites  ±0   5m 54s ⏱️ -1s
3 773 tests ±0  3 581 ✅ ±0  192 💤 ±0  0 ❌ ±0 
4 405 runs  ±0  4 213 ✅ ±0  192 💤 ±0  0 ❌ ±0 

Results for commit 2827487. ± Comparison against base commit ecef6db.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 597 tests  +12   1 596 ✅ +12   6m 17s ⏱️ -47s
    8 suites ± 0       1 💤 ± 0 
    8 files   ± 0       0 ❌ ± 0 

Results for commit 2827487. ± Comparison against base commit ecef6db.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   42 files  ± 0     42 suites  ±0   33m 41s ⏱️ - 1m 8s
8 766 tests +34  8 569 ✅ +34  197 💤 ±0  0 ❌ ±0 
9 398 runs  +34  9 201 ✅ +34  197 💤 ±0  0 ❌ ±0 

Results for commit 2827487. ± Comparison against base commit ecef6db.

This pull request removes 1 and adds 35 tests. Note that renamed tests count towards both.
Memex.Portal.Shared.Test.ComboVerdictRecordingTest ‑ StatusMarkdown_NoTagAtAll_SaysNothingDetected
Memex.Portal.Shared.Test.ComboVerdictRecordingTest ‑ StatusMarkdown_NoTagAndNoCheckEverCompleted_SaysNoCheckHasRun
Memex.Portal.Shared.Test.ComboVerdictRecordingTest ‑ StatusMarkdown_NoTagButACheckDidRun_SaysNothingNewerAndWhen
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ AHoldWithNoReason_StillNamesTheTagAndTheHold
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ EveryOutcome_HasAFactoryThatProducesIt
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: Applied, expected: True)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: CheckFailed, expected: False)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: Deferred, expected: True)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: DetectOnly, expected: True)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: Held, expected: True)
Memex.Portal.Shared.Test.SelfUpdateVerdictTest ‑ FoundNewerRelease_IsTrueExactlyWhenAReleaseWasWaiting(outcome: NoNewerRelease, expected: False)
…

@rbuergi
rbuergi merged commit 12b1fed into main Aug 29, 2026
23 checks passed
@rbuergi
rbuergi deleted the fix/self-update-cluster branch October 10, 2026 13:11
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