Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deploy/helm/templates/memex-portal/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,10 @@ data:
# seed only; a record's own AutoUpdate flag stays the runtime authority thereafter.
# See Doc/Architecture/PluginUpdateOnGreenBuild.md.
PluginCatalog__AutoUpdateByDefault: "{{ .Values.config.memex_portal.PluginCatalog__AutoUpdateByDefault | default "true" }}"
# The per-PACKAGE default policy (Auto | Notify | None), seeded onto every fresh install record —
# separate from the platform's own image policy (Admin/UpdatePolicy) since 2026-09-14. Empty keeps
# the legacy AutoUpdateByDefault mapping (true → Auto, false → Notify).
PluginCatalog__DefaultUpdatePolicy: "{{ .Values.config.memex_portal.PluginCatalog__DefaultUpdatePolicy | default "" }}"
Comment on lines +903 to +906
# ---- Plugin catalog: which registry this install CONSUMES, and (on a registry install) which
# git repos it SERVES. Both were previously un-templated, so an env overlay that set them was
# silently dropped — the catalog then reads "not configured" with the values file looking right,
Expand Down
66 changes: 0 additions & 66 deletions memex/Memex.Portal.Shared/SelfUpdate/PlatformModuleUpdatePolicy.cs

This file was deleted.

10 changes: 4 additions & 6 deletions memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ public static TBuilder AddSelfUpdate<TBuilder>(this TBuilder builder, SelfUpdate
builder.AddUpdatePolicyType();
builder.ConfigureServices(services =>
{
// The module lane's unattended-landing gate (#1664) rides the SAME Admin/UpdatePolicy
// node this feature owns — Continuous (default) lands store-installed modules
// unattended, Stable/None decline. Registered here, beside the node type, so a host
// without self-update simply has no policy provider and the PluginCatalog default
// (allowed) applies. Platform-neutral (a storage read), so no browser guard.
services.AddSingleton<MeshWeaver.PluginCatalog.IModuleUpdatePolicy, PlatformModuleUpdatePolicy>();
// Admin/UpdatePolicy governs the PLATFORM IMAGE roll only. Until 2026-09-14 it also
// gated every module's unattended landing (PlatformModuleUpdatePolicy → the retired
// IModuleUpdatePolicy seam); each package now carries its own policy on its install
// record (PackageManifest.UpdatePolicy) — "separate for the platform and for each module".
// The deployment gate (#1754): "may this environment be rolled to that release?".
// Registered unconditionally — the SAME verdict has to be readable by all three paths
// that roll a version (the poller below, CD's post-promote assertion and a manual
Expand Down
13 changes: 12 additions & 1 deletion src/MeshWeaver.Deployment.Contract/DeploymentContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,20 @@ public record DeploymentContent
[Description("Image pull Secret name in the instance namespace — required when the image registry is not ACR")]
public string? ImagePullSecret { get; init; }

/// <summary>Self-update policy: Continuous (default), Stable, or None.</summary>
/// <summary>The PLATFORM image self-update policy: Continuous (default), Stable, or None. Governs the image roll only.</summary>
public string? UpdatePolicy { get; init; }

/// <summary>
/// The DEFAULT per-package (module) update policy this instance seeds onto every install
/// record it creates — <c>Auto</c> (track the registry unattended), <c>Notify</c> (remind,
/// a person clicks Update) or <c>None</c> (pinned). Renders as
/// <c>PluginCatalog__DefaultUpdatePolicy</c>. Separate from <see cref="UpdatePolicy"/> on
/// purpose: an instance may pin its platform and still track its modules, or the reverse.
/// A package's own record stays the runtime authority once installed.
/// </summary>
[Description("Default per-module update policy seeded at install: Auto, Notify or None")]
public string? ModuleUpdatePolicy { get; init; }

/// <summary>Who this deployment is for — a customer name, or "Systemorph" for our own.</summary>
public string? Owner { get; init; }

Expand Down
5 changes: 5 additions & 0 deletions src/MeshWeaver.Deployment.Contract/DeploymentPortalConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@ public static SortedDictionary<string, string> PortalConfig(DeploymentContent d,

Set("Deployment__Orleans__Clustering", OrleansClustering(d));
Set("SelfUpdate__MinRollInterval", string.IsNullOrWhiteSpace(d.MinRollInterval) ? DefaultMinRollInterval : d.MinRollInterval!.Trim());
// The per-PACKAGE default update policy (Auto | Notify | None) the instance seeds onto every
// install record — separate from the platform's own image policy (Admin/UpdatePolicy) since
// 2026-09-14. Absent renders nothing: the chart's default keeps the legacy AutoUpdateByDefault
// mapping (true → Auto).
Set("PluginCatalog__DefaultUpdatePolicy", string.IsNullOrWhiteSpace(d.ModuleUpdatePolicy) ? null : d.ModuleUpdatePolicy!.Trim());
SetBool("Modules__AutoRecycleOnStaleBuild", d.AutoRecycleOnStaleBuild);
foreach (var (slot, assembly) in ModuleSlots(d))
Set($"Modules__Required__{slot}", assembly);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ d with
/// <summary>Self-update policy name (<c>None</c>, <c>Continuous</c>, <c>Stable</c>).</summary>
public static DeploymentContent WithUpdatePolicy(this DeploymentContent d, string policy) => d with { UpdatePolicy = policy };

/// <summary>
/// The default update policy every package installed on this instance is SEEDED with —
/// <c>Auto</c>, <c>Notify</c> or <c>None</c> (<c>PluginCatalog:DefaultUpdatePolicy</c>); a package
/// re-stamped later keeps its own. Independent of <see cref="WithUpdatePolicy"/>, which moves the
/// platform IMAGE only.
/// </summary>
public static DeploymentContent WithModuleUpdatePolicy(this DeploymentContent d, string? policy) => d with { ModuleUpdatePolicy = policy };

/// <summary>Minimum interval between self-update rolls (a <see cref="TimeSpan"/> string, e.g. <c>01:00:00</c>).</summary>
public static DeploymentContent WithMinRollInterval(this DeploymentContent d, string? interval) => d with { MinRollInterval = interval };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ configures no container.
| `WithImage(repository, tag, pullSecret, migrationRepository)` | `ImageRepository`, `PinnedImageTag`, `ImagePullSecret`, `MigrationImageRepository` | `portal.image`, `portal.imagePullSecret`, `migration.image`, `selfUpdate.registry` | — |
| `WithPinnedImageTag(tag)` | `PinnedImageTag` | `portal.image` (the tag; the pin IS the roll) | — |
| `WithUpdatePolicy(policy)` | `UpdatePolicy` | — (the self-updater reads the record) | — |
| `WithModuleUpdatePolicy(policy)` | `ModuleUpdatePolicy` | — (rendered into the portal's config) | `PluginCatalog__DefaultUpdatePolicy` |
| `WithMinRollInterval(interval)` | `MinRollInterval` | `config.memex_portal.SelfUpdate__MinRollInterval` | `SelfUpdate__MinRollInterval` |
| `WithAutoRecycleOnStaleBuild(enabled)` | `AutoRecycleOnStaleBuild` | `config.memex_portal.Modules__AutoRecycleOnStaleBuild` | `Modules__AutoRecycleOnStaleBuild` |
| `WithPluginRepo(name, url, gitRef, isRegistrySource, secretName)` | `PluginRepos[].Name`, `PluginRepos[].Url`, `PluginRepos[].Ref`, `PluginRepos[].IsRegistrySource`, `PluginRepos[].SecretName` | `pluginCatalog.sources` (the operator's catalog file) | `PluginCatalog__*` (Aspire only) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ still describes the older mechanism, it says so in a banner that names the imple

## The three rules

> **Who may adopt unattended is a PER-PACKAGE choice** since 2026-09-14 — `updatePolicy` on the
> install record (`Auto` / `Notify` / `None`), seeded from `PluginCatalog:DefaultUpdatePolicy`, and
> independent of the platform's image policy on `Admin/UpdatePolicy`. The rules below say WHAT an
> installation runs once a version is adopted; [Plugin Update on Green Build](@/Doc/Architecture/PluginUpdateOnGreenBuild)
> says whether the adoption happens by itself.

**Maintainer clarification, 2026-09-09: compatibility follows the used API, not a platform pin.**
Keep a module usable across platform and dependency releases when the contracts it uses remain
compatible. A different version, build commit or MVID is not evidence of incompatibility. A
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,20 +378,42 @@ edit, so neither is evidence of equal content. It is the same reason
an unversioned partition instead of a version number, and why `ModuleLandingService.GenerationIdOf`
appends each file's bytes and not only its length.

## 🚨 Reminder by default; unattended on opt-in — seeded per deployment
## 🚨 One policy PER PACKAGE — separate from the platform's image policy (2026-09-14)

The **platform default is explicit opt-in**: a changed module raises a `Notification` satellite on
the install record (the bell surfaces it, the catalog card offers **Update**) and nothing installs
until a human acts. A record whose `AutoUpdate` flag is set installs the delta unattended instead.
> **Maintainer, 2026-09-14:** *"continuous update is set globally — it should be separate for the
> platform and for each module."* Until that day `Admin/UpdatePolicy` — the record that decides
> whether the platform IMAGE rolls — also gated every module's unattended landing
> (`PlatformModuleUpdatePolicy`, the retired `IModuleUpdatePolicy` seam): a deployment that pinned
> its image to a clean release silently stopped every module from tracking its registry, and one
> that followed continuous builds could not pin a single module. The two are now independent.

The opt-in is **stamped at install time from the deployment's policy**:
`PluginCatalog:AutoUpdateByDefault` (default `false`) seeds every fresh install record. A
deployment that wants plugins tracking their repos continuously sets it `true` — **our Helm
deployments do**, so a plugin repo's green build reaches those portals with nobody clicking
anything — while an installation that configures nothing stays review-first. Install-time seed
only: the record's own flag is the runtime authority thereafter, in both directions — an update
re-stamp carries it forward, and flipping the deployment default later changes nothing for
already-installed packages.
**Every install record carries its own `updatePolicy`** — `PackageManifest.UpdatePolicy`, one of:

| `updatePolicy` | Content (this page's lane) | Compiled module (the registry's bundle lane) |
|---|---|---|
| **`Auto`** | the delta installs unattended | the newer bundle lands unattended (restart-as-activation) |
| **`Notify`** | a `Notification` satellite on the record, told once per candidate; the catalog card offers **Update** | declined with the reason on the log; the card's **Update** lands it |
| **`None`** (pinned) | nothing — no install, no reminder | nothing |

Both lanes read the SAME value (`PackageManifest.EffectiveUpdatePolicy`), so a package never
lands one half without the other. **The platform's own policy on `Admin/UpdatePolicy`
(Continuous / Stable / None + the version pattern) governs the image roll and nothing else.**

**The platform default is `Notify`** — explicit opt-in. The policy is **stamped at install time
from the deployment's default**, `PluginCatalog:DefaultUpdatePolicy` (`Auto` | `Notify` | `None`;
unset → the legacy `PluginCatalog:AutoUpdateByDefault` decides, `true` → `Auto`), seeds every
fresh install record. **Our Helm deployments seed `Auto`**, so a plugin repo's green build reaches
those portals with nobody clicking anything, while an installation that configures nothing stays
review-first. Install-time seed only: the record's own policy is the runtime authority
thereafter — an update re-stamp carries it forward (`PackageInstaller.SeedUpdatePolicy`), and
flipping the deployment default later changes nothing for already-installed packages.

**A global administrator changes ONE package's policy on its catalog card** (Auto · Notify ·
Pinned, the current one accented — `PackageInstaller.SetUpdatePolicy`, a System write authorized by
the card's admin gate) — and nowhere else: the record is System-owned, so a `patch` over MCP is
refused by design. A record written before this field existed (`updatePolicy` absent) reads its
legacy `autoUpdate` flag as `Auto` / `Notify`; the flag is kept consistent on every write for
older readers.

An opted-in, unattended update is still fenced three ways, none of which depend on a human being
present:
Expand Down Expand Up @@ -474,12 +496,15 @@ is what associates a repository with a catalog, and the same value resolves the
a `workflow_run` payload carries the same `repository` object a `push` does. A catalog whose source
is a **local path** never matches a webhook, by construction.

### 3. Opting in to unattended updates
### 3. Choosing the per-package policy

Per deployment: `PluginCatalog:AutoUpdateByDefault=true` seeds every FUTURE install record opted
in (the Helm chart sets this for our portals). Per package: set `AutoUpdate` on an installed
package's record. Both are edits to the record's own flag — the deployment key is only the
install-time seed.
Per deployment: `PluginCatalog:DefaultUpdatePolicy` = `Auto` | `Notify` | `None` seeds every FUTURE
install record (the Helm chart's `PluginCatalog__DefaultUpdatePolicy`; a Hosting/Deployment record's
`moduleUpdatePolicy` renders it). Legacy: `PluginCatalog:AutoUpdateByDefault=true` still reads as
`Auto` when the new key is unset. Per package: the catalog card's **Updates** row (global
administrators) — `Auto` · `Notify` · `Pinned`. The deployment key is only the install-time seed;
the record's own `updatePolicy` is the authority. The platform's image policy on
`Admin/UpdatePolicy` is a different knob and does not touch modules.

### 4. Receiving the module-published broadcast (consumers, #3650)

Expand Down
7 changes: 6 additions & 1 deletion src/MeshWeaver.Messaging.Hub/Localization/strings.de.json
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@
"ui.mdLoadingPrivacy": "_Datenschutzerklärung wird geladen…_",
"ui.mdLoadingRepoSettings": "_Repository-Einstellungen werden geladen…_",
"ui.mdLoadingUpdatePolicy": "_Update-Richtlinie wird geladen…_",
"ui.mdUpdatePolicyIntro": "Die Plattform aktualisiert sich selbst gemäss der Strategie unten. **Stable** (Standard) wechselt nur auf saubere Releases wie `3.0.1`. **Continuous** wechselt auf die Continuous-Builds, die das **Versionsmuster** zulässt — zum Beispiel `3.0.1-ci*` — und verhält sich ohne Muster wie Stable. **None** schaltet die automatische Aktualisierung aus. **Nur auf CI-geprüfte (grüne) Builds aktualisieren** (standardmässig an) hält die Installation auf Builds, die CI bestanden haben — ausschalten, um auch ungeprüfte Edge-Builds zu akzeptieren. In der Flotte übergibt das Portal ein erkanntes Release an die Kontrollinstanz, die es ausrollt (ein Release jenseits des gepinnten wartet dort auf eine Freigabe); eine eigenständige Kubernetes-Installation patcht weiterhin ihr eigenes Deployment; sonst wird die neueste Version für ein manuelles Update angezeigt.",
"ui.mdUpdatePolicyIntro": "**Diese Richtlinie betrifft nur das Plattform-Image.** Jedes installierte Paket trägt seine eigene Update-Richtlinie (Automatisch · Benachrichtigen · Fixiert) auf seiner Katalogkarte. Die Plattform aktualisiert sich selbst gemäss der Strategie unten. **Stable** (Standard) wechselt nur auf saubere Releases wie `3.0.1`. **Continuous** wechselt auf die Continuous-Builds, die das **Versionsmuster** zulässt — zum Beispiel `3.0.1-ci*` — und verhält sich ohne Muster wie Stable. **None** schaltet die automatische Aktualisierung aus. **Nur auf CI-geprüfte (grüne) Builds aktualisieren** (standardmässig an) hält die Installation auf Builds, die CI bestanden haben — ausschalten, um auch ungeprüfte Edge-Builds zu akzeptieren. In der Flotte übergibt das Portal ein erkanntes Release an die Kontrollinstanz, die es ausrollt (ein Release jenseits des gepinnten wartet dort auf eine Freigabe); eine eigenständige Kubernetes-Installation patcht weiterhin ihr eigenes Deployment; sonst wird die neueste Version für ein manuelles Update angezeigt.",
"ui.mdNoCommits": "_Keine Commits._",
"ui.mdNoDocumentData": "_Keine Dokumentdaten._",
"ui.mdNoFileChanges": "_Keine Dateiänderungen._",
Expand Down Expand Up @@ -855,6 +855,11 @@
"ui.catalogRegistry": "Registry",
"ui.catalogInstall": "Installieren",
"ui.catalogUpdateTo": "Auf v{0} aktualisieren",
"ui.catalogUpdatePolicy": "Updates",
"ui.catalogUpdatePolicyAuto": "Automatisch",
"ui.catalogUpdatePolicyNotify": "Benachrichtigen",
"ui.catalogUpdatePolicyNone": "Fixiert",
"ui.catalogUpdatePolicyHint": "Wie dieses Paket Updates erhält — unabhängig von der Update-Richtlinie der Plattform.",
"ui.catalogInstalledVersion": "✓ Installiert v{0}",
"ui.releases": "Releases",
"ui.yourProgress": "Dein Fortschritt",
Expand Down
Loading
Loading