Skip to content

feat(PluginCatalog): one update policy per package — separate from the platform's image policy - #4310

Merged
meshweaver-cloud[bot] merged 3 commits into
mainfrom
feat/per-module-update-policy
Sep 14, 2026
Merged

meshweaver-cloud[bot] merged 3 commits into
mainfrom
feat/per-module-update-policy

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Maintainer, 2026-09-14: "continuous update is set globally; it should be separate for the platform and for each module."

Until now Admin/UpdatePolicy — the record that decides whether the platform image rolls — also gated every module's unattended landing (PlatformModuleUpdatePolicy → IModuleUpdatePolicy). Pinning the image silently stopped every module from tracking its registry; following continuous builds could not pin a single module.

What changes

  • PackageManifest.UpdatePolicy = Auto | Notify | None (nullable — the zero member must survive WhenWritingDefault), with EffectiveUpdatePolicy reading a legacy record's autoUpdate flag as Auto / Notify. Both lanes branch on it: the content reconcile (PackageUpdateReconciler: Auto applies, Notify reminds once, None is silent) and the module lane (RegistryUpdateReconciler.PolicyDecline → PluginBundleClient.AdoptModule), so a package never lands one half without the other.
  • PluginCatalog:DefaultUpdatePolicy seeds fresh install records (PackageInstaller.SeedUpdatePolicy; AutoUpdateByDefault stays as the legacy mapping, true → Auto); every re-stamp carries the record's own policy forward.
  • Per-package control on the catalog card (global administrators): Auto · Notify · Pinned — PackageInstaller.SetUpdatePolicy, a System write authorized by the card's admin gate. Strings en/de.
  • Retired: IModuleUpdatePolicy and the portal's PlatformModuleUpdatePolicy. Admin/UpdatePolicy governs the image roll only; its settings-tab intro says so.
  • DeploymentContent.ModuleUpdatePolicy → PluginCatalog__DefaultUpdatePolicy in the chart (config.yaml; empty keeps the legacy mapping).
  • Docs: Doc/Architecture/PluginUpdateOnGreenBuild (the per-package section and §3), ModuleAdoptionPolicy (a banner).

Verified

dotnet build -c Release -warnaserror on MeshWeaver.PluginCatalog and Memex.Portal.Shared: 0 warnings, 0 errors. The catalog's tests live in MeshWeaver.Plugins (src/MeshWeaver.PluginCatalog.Test) — the pinned cases for SeedUpdatePolicy, PolicyDecline and the three-way Decide follow in the Plugins PR once this set seals, together with HelmValues rendering moduleUpdatePolicy.

🤖 Generated with Claude Code

Mirror-sync: tracked on Systemorph/MeshWeaver.Plugins#1858 — npm run sync:i18n -- --ref <merged core sha> in MeshWeaver.Plugins once this lands (5 keys added, ui.mdUpdatePolicyIntro reworded).

…e platform's image policy

Maintainer, 2026-09-14: "continuous update is set globally; it should be
separate for the platform and for each module." Until now Admin/UpdatePolicy —
the record that decides whether the platform IMAGE rolls — also gated every
module's unattended landing (PlatformModuleUpdatePolicy → IModuleUpdatePolicy),
so pinning the image silently stopped every module from tracking its registry,
and following continuous builds could not pin one module.

- PackageManifest.UpdatePolicy (Auto | Notify | None, nullable — the zero
  member must survive WhenWritingDefault) with EffectiveUpdatePolicy reading a
  legacy record's autoUpdate flag as Auto / Notify. BOTH lanes branch on it:
  the content reconcile (PackageUpdateReconciler: Auto applies, Notify reminds
  once, None is silent) and the module lane (RegistryUpdateReconciler passes
  the record's PolicyDecline into PluginBundleClient.AdoptModule).
- PluginCatalogOptions.DefaultUpdatePolicy seeds fresh records
  (PackageInstaller.SeedUpdatePolicy; AutoUpdateByDefault stays as the legacy
  mapping); every re-stamp carries the record's own policy forward.
- PackageInstaller.SetUpdatePolicy — the catalog card's per-package control for
  global administrators (Auto · Notify · Pinned), a System write authorized by
  the card's admin gate; strings en/de.
- IModuleUpdatePolicy and the portal's PlatformModuleUpdatePolicy are retired;
  Admin/UpdatePolicy governs the image roll only (the settings tab says so).
- DeploymentContent.ModuleUpdatePolicy → PluginCatalog__DefaultUpdatePolicy in
  the chart; Doc/Architecture/PluginUpdateOnGreenBuild + ModuleAdoptionPolicy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 14, 2026 12:13
…__DefaultUpdatePolicy

The per-package default the instance seeds onto every install record, from the
Hosting/Deployment record's moduleUpdatePolicy; absent renders nothing so the
chart's default keeps the legacy AutoUpdateByDefault mapping.

Co-Authored-By: Claude Fable 5.1 <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.

🟡 Changes recommended

Unresolved deployment wiring, policy-update integrity, and unattended-policy enforcement issues block approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Separates per-package plugin update policies from the platform image policy, supporting Auto, Notify, and None behavior across content and module updates.

Changes:

  • Adds policy modeling, legacy compatibility, defaults, and reconciliation behavior.
  • Adds administrator controls, localization, and deployment configuration.
  • Retires the platform-wide module policy seam and updates documentation.
File summaries
File Summary
src/MeshWeaver.PluginCatalog/RegistryUpdateReconciler.cs Applies package policy to module reconciliation.
src/MeshWeaver.PluginCatalog/PluginUpdateWatcher.cs Uses effective package policy.
src/MeshWeaver.PluginCatalog/PluginCatalogOptions.cs Adds default policy configuration.
src/MeshWeaver.PluginCatalog/PluginBundleClient.cs Supports policy-based module adoption decisions.
src/MeshWeaver.PluginCatalog/PackageUpdateReconciler.cs Applies content update policy decisions.
src/MeshWeaver.PluginCatalog/PackageInstaller.cs Seeds and edits package policies.
src/MeshWeaver.PluginCatalog/Package.cs Defines package policy and legacy mapping.
src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs Removes the retired policy interface.
src/MeshWeaver.PluginCatalog/CatalogLayoutAreas.cs Adds administrator policy controls.
src/MeshWeaver.Messaging.Hub/Localization/strings.en.json Adds English UI strings.
src/MeshWeaver.Messaging.Hub/Localization/strings.de.json Adds German UI strings.
src/MeshWeaver.Documentation/Data/Architecture/PluginUpdateOnGreenBuild.md Documents per-package policies.
src/MeshWeaver.Documentation/Data/Architecture/ModuleAdoptionPolicy.md Updates adoption-policy guidance.
src/MeshWeaver.Deployment.Contract/DeploymentContent.cs Adds deployment-level policy input.
memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateConfiguration.cs Removes platform/module coupling.
memex/Memex.Portal.Shared/SelfUpdate/PlatformModuleUpdatePolicy.cs Removes the retired implementation.
deploy/helm/templates/memex-portal/config.yaml Adds the policy configuration key.
Review details

Suppressed comments (8)

deploy/helm/templates/memex-portal/config.yaml:906

  • DefaultUpdatePolicy is a nullable enum, but this template always emits an empty string when unset. The .NET configuration binder sees a present invalid enum value rather than an absent/null setting, so a default deployment can fail binding instead of falling back to AutoUpdateByDefault. Omit this key when no policy is configured (or emit only a valid enum name).
  PluginCatalog__DefaultUpdatePolicy: "{{ .Values.config.memex_portal.PluginCatalog__DefaultUpdatePolicy | default "" }}"

src/MeshWeaver.Deployment.Contract/DeploymentContent.cs:79

  • The new public deployment property adds an English [Description], but the contract-label appendix in ConfiguringAnInstanceFromAspire.md is the documented source of German labels for this assembly and has no ModuleUpdatePolicy row. Add the German label there (and the in-mesh translation in the paired Plugins change) so this setting is not English-only.
    [Description("Default per-module update policy seeded at install: Auto, Notify or None")]
    public string? ModuleUpdatePolicy { get; init; }

src/MeshWeaver.Documentation/Data/Architecture/ModuleAdoptionPolicy.md:24

  • This migration banner updates one page, but src/MeshWeaver.Documentation/Data/Architecture/Modules.md:924-936 still documents Admin/UpdatePolicy and IModuleUpdatePolicy as the active module gate. After this deletion that page tells operators the opposite behavior and points at a removed type; update it in the same documentation change.
> **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.

src/MeshWeaver.Documentation/Data/Architecture/PluginUpdateOnGreenBuild.md:388

  • This is a user-visible feature, but no Data/WhatsNew/2026/... entry is included. The portal's What's New feed is built from those nodes and the release convention requires a Category: Feature entry for a new user-facing capability; add the entry (or explicitly pair it with the dependent change) before considering the feature complete.
## 🚨 One policy PER PACKAGE — separate from the platform's image policy (2026-09-14)

> **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.

src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs:26

  • This new SkipPolicy contract conflicts with the shipped Doc/Architecture/Modules.md, which still says Admin/UpdatePolicy and IModuleUpdatePolicy gate module landing. Since this change removes that interface, update that documentation page in the same change or operators will follow the old platform-coupled behavior.
    /// <summary>The package's own update policy declines unattended landing (Notify / None).</summary>
    SkipPolicy,

src/MeshWeaver.PluginCatalog/PackageInstaller.cs:577

  • This success log is emitted before the write is subscribed and completed, so a rejected or failed write still records "update policy set". Move the LogInformation call into the successful write pipeline (for example, after Take(1) with Do) so the operational record reflects the actual outcome.
                logger?.LogInformation(
                    "Package {Id}: update policy set to {Policy} (was {Previous}).",
                    packageId, policy, manifest.EffectiveUpdatePolicy);

src/MeshWeaver.PluginCatalog/PackageInstaller.cs:572

  • None has no representation in the legacy AutoUpdate boolean. Writing AutoUpdate = false makes an older reconciler take its Notify branch, and clearing NotifiedModuleVersion here means a mixed-version rollout or downgrade can still raise a reminder for a package that was explicitly pinned. Either preserve the pinned state with a compatible marker/upgrade ordering, or document and test this intentional loss of down-level semantics instead of calling the flag consistent for older readers.
                        UpdatePolicy = policy,
                        AutoUpdate = policy == PackageUpdatePolicy.Auto,
                        // A new policy is a new question for the reminder path: forget what was
                        // told under the old one, so a switch back to Notify reminds again.
                        NotifiedModuleVersion = null,

src/MeshWeaver.PluginCatalog/PackageUpdateReconciler.cs:178

  • The new three-way policy decision is not covered by the tests in this change: the existing reconciler fixtures seed only the legacy AutoUpdate flag. Add real-mesh coverage for Auto applying, Notify producing one reminder, None staying silent, and the module lane declining on the same policy; otherwise content and module policy parity can regress independently.
        return policy switch
        {
            PackageUpdatePolicy.Auto => Apply(hub, meshService, accessService, source, sourceRef, pkg, record,
                recordPath, provenance, detail, logger),
            PackageUpdatePolicy.Notify => Notify(
  • Files reviewed: 18/18 changed files
  • Comments generated: 14
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +903 to +906
# 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 "" }}"
/// 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; }
Comment on lines +564 to +568
var updated = node with
{
Content = manifest with
{
UpdatePolicy = policy,
Comment on lines 350 to +352
public IObservable<int> AdoptModule(
string pluginId, string moduleName, string? packagePath = null, bool unattended = false)
string pluginId, string moduleName, string? packagePath = null, bool unattended = false,
string? policyDecline = null)
Comment on lines +885 to +888
PackageInstaller.SetUpdatePolicy(host.Hub, packageId, policy, logger)
.Subscribe(
_ => { },
ex => logger?.LogWarning(ex, "Setting update policy {Policy} on {Id} failed.", policy, packageId));
Comment on lines +405 to +410
/// <summary>
/// THE per-package update policy — <see cref="PackageUpdatePolicy.Auto"/> /
/// <see cref="PackageUpdatePolicy.Notify"/> / <see cref="PackageUpdatePolicy.None"/> — the
/// sole runtime authority for this package's unattended updates, content AND module, and
/// independent of the platform's own image policy (<c>Admin/UpdatePolicy</c>). Seeded at
/// install time from the deployment's <see cref="PluginCatalogOptions.DefaultUpdatePolicy"/>
// Internal for the BuildCompletionSubscriptionTest pin (InternalsVisibleTo).
internal static bool SeedAutoUpdate(PackageManifest? existingRecord, PluginCatalogOptions? options) =>
existingRecord?.AutoUpdate ?? options?.AutoUpdateByDefault ?? false;
SeedUpdatePolicy(existingRecord, options) == PackageUpdatePolicy.Auto;
Comment on lines +167 to +176
var policy = record.EffectiveUpdatePolicy;
logger?.LogInformation(
"Package update: {Id} has an update ({Old} → {New}; {Detail}); autoUpdate={Auto}. {Provenance}",
pkg.Id, record.ModuleVersion, pkg.ModuleVersion, detail, record.AutoUpdate, provenance);
"Package update: {Id} has an update ({Old} → {New}; {Detail}); updatePolicy={Policy}. {Provenance}",
pkg.Id, record.ModuleVersion, pkg.ModuleVersion, detail, policy, provenance);

return record.AutoUpdate
? Apply(hub, meshService, accessService, source, sourceRef, pkg, record, recordPath,
provenance, detail, logger)
: Notify(
// The PACKAGE's own policy decides — never the platform's image policy (Admin/UpdatePolicy),
// which governs the image roll alone since 2026-09-14.
return policy switch
{
PackageUpdatePolicy.Auto => Apply(hub, meshService, accessService, source, sourceRef, pkg, record,
Comment on lines +61 to +69
/// <summary>
/// The deployment's DEFAULT per-package update policy (<c>PluginCatalog:DefaultUpdatePolicy</c>
/// = <c>Auto</c> | <c>Notify</c> | <c>None</c>), consulted ONLY at install time: a fresh install
/// record is seeded with it (<c>PackageInstaller.SeedUpdatePolicy</c>). Unset → the legacy
/// <see cref="AutoUpdateByDefault"/> decides (<c>true</c> → Auto, else Notify). Separate from
/// the platform's own image policy on <c>Admin/UpdatePolicy</c> by design: a deployment may
/// pin its platform and still track every module, or the reverse.
/// </summary>
public PackageUpdatePolicy? DefaultUpdatePolicy { get; set; }
Comment on lines +297 to +298
internal static bool ShouldAutoApply(PackageManifest record) =>
record.EffectiveUpdatePolicy == PackageUpdatePolicy.Auto;
@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

262 tests   262 ✅  1m 55s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit 8b0a74e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 472 tests   1 472 ✅  2m 48s ⏱️
    2 suites      0 💤
    2 files        0 ❌

Results for commit 8b0a74e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

910 tests   910 ✅  3m 26s ⏱️
  2 suites    0 💤
  2 files      0 ❌

Results for commit 8b0a74e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 846 tests   1 654 ✅  4m 32s ⏱️
    4 suites    192 💤
    4 files        0 ❌

Results for commit 8b0a74e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files      6 suites   5m 36s ⏱️
2 183 tests 2 183 ✅ 0 💤 0 ❌
2 187 runs  2 187 ✅ 0 💤 0 ❌

Results for commit 8b0a74e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 092 tests   1 090 ✅  6m 19s ⏱️
    3 suites      2 💤
    3 files        0 ❌

Results for commit 8b0a74e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   18 files     18 suites   24m 40s ⏱️
7 765 tests 7 571 ✅ 194 💤 0 ❌
7 769 runs  7 575 ✅ 194 💤 0 ❌

Results for commit 8b0a74e.

♻️ This comment has been updated with latest results.

…; WithModuleUpdatePolicy + its parity row

Three gates on the first run, none about the change's substance:

- Public surface / cross-repo pair: the retired IModuleUpdatePolicy was DELETED,
  and a removed public type is binary-breaking for a module compiled against an
  earlier platform (#2370). It stays as a public, [Obsolete] interface with its
  one member — nothing registers or consults it — so no TypeRef is orphaned and
  no counterpart declaration is owed.
- RendererParityTest: DeploymentContent.ModuleUpdatePolicy had no fluent
  transform and no parity-table row. WithModuleUpdatePolicy(policy) is added
  beside WithUpdatePolicy (the image policy), the ConfiguringAnInstanceFromAspire
  table gains its row (config key PluginCatalog__DefaultUpdatePolicy), and the
  fully-populated fixture sets it so the emitted-key check can see it —
  Deployment.Contract.Test 15/15.
- i18n mirror sync: the handover is declared in the PR body
  (Mirror-sync: Systemorph/MeshWeaver.Plugins#1858).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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