NuGet Package Retirement
"which nuget packages do we want to release at all? should we discontinue all but the aspire adapter? β¦ we retire most nuget packages completely β¦ we leave the dotnet new package and the aspire integration" β maintainer, 2026-09-07
Two packages survive. Everything else ever published under the MeshWeaver prefix is retired and
unlisted on nuget.org.
1. The two survivors
| Package | Project | What it is |
|---|---|---|
MeshWeaver.Aspire.Hosting.Memex |
memex/aspire/Memex.Aspire.Hosting (this repository) |
The Aspire hosting integration. builder.AddMemex() wires Postgres (pgvector), the one-shot migration and the portal from published GHCR images, and publishes to Docker Compose, Kubernetes/Helm or Azure Container Apps. |
MeshWeaver.MemexTemplate |
tools/MeshWeaver.MemexTemplate.Pack (MeshWeaver.Plugins) |
The dotnet new template pack that writes a portal solution. |
Both survive for the same reason: they are entry points, not platform bytes. One writes the solution a newcomer starts from; the other is the AppHost-side integration that names the images. Neither is loaded into a portal, neither carries a framework assembly, and neither participates in the NodeType bake identity.
π¨ Always pass both survivors to the sweep's keep input. The subtraction alone protects only
what THIS tree packs, and one survivor is in another repository β invisible to the derivation by
construction. That gap has already cost one wrong unlist: an earlier sweep run from this repository
could not see the template's project and retired it.
π The template is a survivor that has not shipped yet
MeshWeaver.MemexTemplate last published 3.0.0-rc7; all seven of its versions read
listed: false, so a versionless dotnet new install MeshWeaver.MemexTemplate cannot resolve it.
Two causes, both now cleared, and one piece of wiring that is still not built.
Cause 1 β mechanical, fixed. Its pack target never passed the generator the platform checkout
it requires, so dotnet pack on it could not succeed at all.
Cause 2 β the GUI, decided 2026-09-10. Measured 2026-09-07:
| Fact | Consequence |
|---|---|
generate-memex-template.cs refused to generate without --with-gui β the two shipped hosts referenced Memex.Portal.Gui unconditionally |
there was no GUI-less template to publish |
Memex.Portal.Gui lives only in MeshWeaver.Plugins, which is private. Core holds zero .razor files and no MeshWeaver.Blazor* project |
publishing the template WITH it publishes private source |
The published 3.0.0-rc7 package contains no Memex.Portal.Gui |
shipping it with the GUI is a NEW exposure, not a restoration |
| A nupkg cannot be recalled β unlisting hides a version from search but it stays downloadable by exact version for ever | that exposure would be irreversible |
There was a standing decision to include the GUI (2026-08-26), but its stated premise β "the UI
is public in core today, the move is what would make it private" β expired when the move
happened. #3653 was therefore decided the
other way: make the GUI optional in the hosts, landed by
MeshWeaver.Plugins#1591. Both hosts
reference Memex.Portal.Gui from a conditioned ItemGroup and compile without it; the generator
drops an optional project it is not shipping and refuses by name if the reference is unconditional;
dotnet pack on the template now succeeds with no --with-gui and no Memex.Portal.Gui in the
nupkg. Nothing private ships, and the first row above is history. The scaffolded solution is a
headless mesh host β no Blazor shell β which the generated README states rather than promising a UI
the package cannot contain. Full reasoning: Hosting/OptionalPortalGui in MeshWeaver.Plugins.
What is left is this lane's wiring, and it is a real task. publish-packages.yml publishes the
Aspire integration only; the template's step is absent rather than written-and-disabled (a step
that exists but never runs is the skip-trapdoor this repository forbids). Building it needs a
Plugins checkout with a minted App token, -p:Version=, -p:MeshWeaverRoot=, and an answer to
which Plugins commit a core tag pairs with β see that workflow's header.
Everything else was a library, and a MeshWeaver library package has had no consumer for months:
in-mesh source compiles against the platform image, module bundles carry their own closures, and
satellite repositories build inside mw-plugin-test
(Release Process Β§5,
Plugin Packaging,
Module Build Architecture). Publishing them meant
offering consumers packages nobody builds against and nobody patches.
2. The ground rule: startup dependencies are Aspire options, never packages
"everything which has a startup-dependency should be configurable via aspire plugin" β maintainer, 2026-09-07
This is what keeps the survivor count at two. The question it settles is the obvious objection β surely feature configuration needs packages? which database driver to load, which plugins to operate it? It does not, and one fact decides it:
The AppHost is not the portal. The adapter runs in the orchestration process; the portal is a
prebuilt image pulled from GHCR. A PackageReference a customer adds to their AppHost lands in the
AppHost's output, where the portal container can never see it. A NuGet package therefore cannot
add a driver to a running portal even in principle β so a fan-out of MeshWeaver.Hosting.<driver>
packages would buy nothing and cost a version matrix that must agree with the image tag.
What does work is in place. The Deployment record (DeploymentContent, the assembly
MeshWeaver.Deployment.Contract, bundled INSIDE the Aspire package) is the single config surface β
the ONE input Aspire and the Helm chart both render from β and every field on it maps to a portal
config key emitted as container environment:
builder.AddMemex("memex")
.WithStorageLayout(s => s with { Backend = "Filesystem" }) // β Deployment__Backend
.WithOrleansClustering("AdoNet") // β Deployment__Orleans__Clustering
.WithPluginRepo("plugins", "https://github.com/Systemorph/MeshWeaver.Plugins", gitRef: "main")
.PreInstall("MeshWeaver.Plugins/Hosting") // β PluginCatalog__* (Aspire) / the catalog file (Helm)
.WithRequiredModule("MeshWeaver.Hosting.Postgres") // β Modules__Required__0
.WithAi(a => a.Anthropic(models, enabled: false)); // β Features__Ai__Providers__Anthropic
Driver selection is done this way β the storage layout picks the backend, OrleansClustering
picks Localhost, AdoNet or Azure Tables β as config strings the image interprets at startup,
not as assemblies the customer resolves. Selecting which plugins the image loads at startup is the
same shape: a field on the record (pluginRepos, preInstall, requiredModules), a config key on
the container, and the plugin fetched at runtime from the plugin catalog as a module bundle. Adding
a startup-time capability means adding a field to the record (and its fluent method), never
adding a package. The full surface and the parity table are
ConfiguringAnInstanceFromAspire.
β Settled, 2026-09-08 β #3646. Plugin selection from the AppHost is the record's
WithPluginRepo/PreInstall/WithRequiredModule. Generating the Helm chart from Aspire is retired: the duplication was the adapter's own second copy of the record (MemexOptions), not the chart, and the chart's operational contract (the migration Job, the Key Vault classes, KEDA, the operator, the registry, the gates) is not expressible in Aspire's publisher. Aspire emits a record, never a chart; the chart is a renderer of the same record.
3. What unlisting does β and what it does not
dotnet nuget delete unlists on nuget.org; it does not erase. This is what makes the retirement
safe to automate at all:
| β Existing pins keep resolving | A PackageReference at an exact version still restores. |
| β Reversible | A listing can be restored from the nuget.org UI, and publishing a new version relists the id. |
| β Gone from search | The package no longer appears in nuget.org search or dotnet add package completion. |
| β Gone from latest-version resolution | A floating range or a versionless dotnet tool install -g no longer finds it. |
This is measured, not assumed, three ways:
- A direct restore.
MeshWeaver.AI 2.5.0readslisted: falseon nuget.org. A project referencing exactly that version, against nuget.org as the only source, restores in about three seconds. That is the property the whole retirement rests on, verified against the live registry rather than inferred from NuGet's documentation. - An earlier sweep already did this.
MeshWeaver.AI,MeshWeaver.Blazor,MeshWeaver.Hosting.Blazor,MeshWeaver.ImportandMeshWeaver.Chartingare already unlisted.Systemorph/Memexpins about thirty-five of them at3.0.0-rc2, restores from nuget.org and nothing else, and itsBuild (Release)has stayed green throughout β for weeks. - Every downstream consumer pins exactly. Six repositories in the organisation reference
MeshWeaver.*byPackageReference, and every one of them fixes the version through central package management β nothing floats:
| Repository | Pinned at |
|---|---|
Systemorph/Memex |
3.0.0-rc2 |
Systemorph/ILS |
3.0.0-preview1 |
Systemorph/CreditReRate |
2.5.0 |
Systemorph/PartnerRe.Aviation |
2.4.0 |
Systemorph/PartnerRe.PropertyFac |
2.4.0 |
Systemorph/Solar |
1.0.0 / 1.0.1 |
π¨ A floating range is what unlisting would break β
Version="2.*", a versionlessdotnet tool install -g, or aPackageReferencewith no version at all under a feed that supplies one. Before retiring any further id, check for those rather than for the presence of a reference: a reference is not the risk, an unpinned one is. The check that matters is an organisation-wide code search forPackageReference Include="MeshWeaver, followed by reading each hit'sDirectory.Packages.props.
The two consumption paths that unlisting does end are both deliberate:
dotnet tool install -g MeshWeaver.Cli(thememexCLI) and-g MeshWeaver.Compiler.Cli(mw-compiler). No lane in this repository or any satellite installed either: CI reaches those verbs through themw-plugin-testimage, andMeshWeaver.Plugin.Buildis built from source at the pinned platform ref on purpose, because it encodes how a given version compiles.- Anything resolving
MeshWeaver.*by a floating version. Nothing in the fleet does.
4. How the retirement is enforced
Retirement is derived, never a hand-kept list β so it stays correct as further modules move out.
Half one β nothing packs unless it says so. Directory.Build.props sets
<IsPackable>false</IsPackable> at the repository root, and exactly one project opts back in. That
is a default rather than a ban: it exists so that adding a project cannot silently add a package.
Half two β the sweep. scripts/orphaned-nuget-packages.py computes
orphaned = (published under the MeshWeaver prefix) MINUS (what this tree packs) MINUS (--keep)
and .github/workflows/unlist-orphaned-packages.yml runs it. It is report-only unless apply
is checked and confirm reads UNLIST β because the derivation is a subtraction, so any bug in
"what this tree packs" fails toward unlisting live packages, and report-only is what caught such a
bug once computing "all 90".
π¨
IsPackableis inherited, and the sweep reads csproj TEXT. The script does not evaluate MSBuild, so a repository-level default is invisible to it unless it is taught the props chain β and it is:directory_default()resolves the chain the way MSBuild does (an explicit declaration wins; a props file that neither declares nor imports the one above ends the chain at MSBuild's own default,true). Getting this wrong in the other direction is the dangerous one β believing a project does not pack is what unlists a live package β so the resolution deliberately errs toward packable, and four self-tests pin both directions.
Half three β the set is asserted. PackableProjectReadmeTest fails if the effectively-packable
set is anything other than the one expected project. A project that quietly regains packability
would otherwise do two invisible things at once: enter the release's pack glob, and spare its
retired package from the sweep.
Publishing the survivors
.github/workflows/publish-packages.yml packs and pushes both, on the release tag v*.*.*.
- Why the tag and not every merge. nuget.org orders by SemVer, and the continuous
-ci.<n>stream does not sort the way the fleet's self-updater ranks it. A continuous stream on nuget.org would reproduce the rc line's defect exactly βrc9sat aboverc13for a whole run β in front of consumers whose only question is "what is the current one". - Why pack rather than promote. A release is a promotion of images because those bytes already
exist in a registry; no nupkg is ever built by
main-cd, so there is nothing to retag. Packing at the tag is safe only because neither survivor is a platform assembly. If either grows a dependency on a MeshWeaver framework assembly, this lane is wrong. - The template needs both trees, and neither alone is enough. Its generator copies six
projects β four from MeshWeaver.Plugins, two from MeshWeaver β plus three assets only core has
(
samples/Graph/Data/User,samples/Graph/Data/ACME, and theDirectory.Packages.propsthe generated template's pins are read from). So it takes<portalRoot> --core <meshweaverRoot>and can run in neither repository alone.projectsToCopyingenerate-memex-template.csis the authority on the split; it moves as projects move, so read it rather than a remembered ratio. The lane checks out MeshWeaver.Plugins with a minted GitHub App token β never a stored PAT. Its predecessor,publish-github.yml, carriedsecrets.GH_PATand is deleted. - The publish is verified.
dotnet nuget pushexiting 0 reports that the call was made, not the resulting state; the lane polls nuget.org until both versions readlisted: true.
π¨ A core tag does not name a MeshWeaver.Plugins commit, and no pairing artefact exists to resolve one. The lane takes the plugins default branch and records the resolved sha in the job summary so the pairing is observable after the fact. When a pairing artefact appears, read it there instead.
5. What was retired
Forty-three package ids. Thirty-nine stopped at 3.0.0-rc13, three at rc8
(MeshWeaver.Fixture, MeshWeaver.Hosting.Monolith.TestBase,
MeshWeaver.Hosting.Orleans.TestBase) and one at rc7
(MeshWeaver.Markdown.Collaboration) β nothing has been published since the rc line closed.
π¨
v3-flatcontainer/index.jsonsorts versions as STRINGS, not as versions.rc10β¦rc13therefore sort BEFORErc9, so reading the tail of that list reportsrc9as the newest and is wrong by four releases. It fooled this page's first draft. Order with a SemVer comparison, or read the registration blob, whenever "the latest version" matters. The largest by downloads wereMeshWeaver.ShortGuid,MeshWeaver.Utils,MeshWeaver.Domain,MeshWeaver.Reflection,MeshWeaver.ServiceProvider,MeshWeaver.Messaging.ContractandMeshWeaver.Messaging.Hub; the set also included the three dotnet tools (MeshWeaver.Cli,MeshWeaver.Compiler.Cli,MeshWeaver.ThumbnailGenerator) andMeshWeaver.Reactive.Assertions, whose only out-of-repo consumer references it by project against$(MeshWeaverRoot).
β Applied and verified (2026-09-08) β and why it took several runs
The retirement is complete. Apply run
34173649521 (2026-09-08
00:32Z) skipped 29 versions that were already unlisted, unlisted the rest, and ended with
VERIFIED on attempt 4: every version now reads as unlisted β the verification reads the
registration blobs, not search. Re-dispatching the same lane with apply is the standing
re-verification: it skips everything already unlisted and re-verifies, so it is safe to run at any
time and costs almost no quota. What follows is the record of why one pass was never enough.
The retirement was applied in batches, not in one pass. Measured on the first apply run (2026-09-07, 760 versions across the 43 ids):
| Elapsed | Deletes accepted | Refused 403 (Quota Exceeded) |
|---|---|---|
| minute 1 | 98 | 0 |
| minute 2 | 141 | 0 |
| minute 3 | 70 | 73 |
| minute 4 | 0 | 158 |
| minute 5 | 0 | 155 |
| minute 6 | 31 | 12 |
| minute 8 | 22 | 0 |
So roughly 300 deletes land, then the endpoint blocks for ~2.5 minutes, then the budget
refills β and the server names the wait itself (retry after: 145-160s). That first run
unlisted 362 versions, verified, and spent 398 round trips rediscovering the same 403.
Three consequences are built into orphaned-nuget-packages.py rather than remembered:
- Already-unlisted versions are skipped before any delete is called. Deleting an unlisted version is a no-op that still costs a quota slot, so without this a resumed run spends its entire budget redoing finished work. This is what makes the job completable at all.
- The server's retry hint is honoured, once, then the run stops. Waiting the time nuget.org names is using the API correctly; hammering keeps the quota pinned and teaches the log nothing.
- A quota wall exits
2β RESUMABLE β and names exactly what remains. It is neither a success nor a failure, and the workflow summary says so in those words. π¨ Re-running the same dispatch is the whole recovery procedure; there is nothing to edit between runs.
π¨ Read the end state from the registration blobs, never from search. nuget.org's search index lags the blobs by a wide margin β mid-retirement it reported 25 listed
MeshWeaver.*ids while the per-versionlistedflags told a different and more advanced story.is_listed()in the script reads the blob, which is the same source the verification step trusts.
π¨
Memex.Merlin.*belongs to a different company. It surfaces in a nuget.org search for "MeshWeaver" because the search is full-text. The sweep only ever considers ids that are exactlyMeshWeaveror beginMeshWeaver., and that filter is the reason β do not relax it.
6. Ownership β what still has to move to Systemorph
Unlisting does not touch ownership, and nuget.org exposes no API for owner management β every step below is a web action by the current owner, so this section is the checklist. Measured 2026-09-08 09:4xZ against the nuget.org search service and profiles:
| Fact | Consequence |
|---|---|
MeshWeaver.Aspire.Hosting.Memex β owners: rbuergi only, 12 listed versions |
the surviving package is personally owned |
MeshWeaver.MemexTemplate and every retired id β fully unlisted, ownership unchanged |
still rbuergi's; an owner can re-list any version |
verified: false on the survivor |
the MeshWeaver.* ID prefix is not reserved β anyone can publish a new MeshWeaver.X |
nuget.org profile Systemorph exists |
the organisation account to transfer to is there |
publish-packages.yml and unlist-orphaned-packages.yml authenticate by Trusted Publishing β GitHub OIDC exchanged by NuGet/login for a short-lived key, under a nuget.org policy per workflow file β with the policy owner in vars.NUGET_TRUSTED_USER |
no stored NuGet secret; the policy today is the owner's, not the organisation's |
The transfer, in the order that keeps publishing working throughout:
- Add the organisation as owner of each id β survivors first, then the retired ids, so nothing
under the prefix stays personally owned (a retired version stays downloadable by exact version
for ever, and re-listing is an owner's act). nuget.org β package β Manage owners β add
Systemorph; the organisation accepts the invitation. - Reserve the
MeshWeaver.*ID prefix forSystemorph(nuget.org's ID-prefix reservation request). Until then theverifiedbadge stays off and the prefix is open. - Move the Trusted Publishing policies to the organisation β one per workflow file
(
publish-packages.yml,unlist-orphaned-packages.yml; repositorySystemorph/MeshWeaver; environment blank, since neither job declares one) β and setvars.NUGET_TRUSTED_USERtoSystemorph. The publish preflight performs the exchange, so a policy that does not match goes red there, named. Then deletesecrets.NUGET_PAT: nothing reads it any more. - Remove
rbuergias owner only after step 3 has published once from the organisation's key.
7. See also
- Release Process & Versioning β the one version number, the two channels, and Β§5 on why NuGet stopped being a delivery vehicle.
- Plugin Packaging β how plugins actually ship: bundles, not packages.
- Module Build Architecture β the image as compiler and reference set.
- Deployment β where the built images go.