Release & Self-Update Strategy

Support lifetime and official artifact retention follow the Release Support Policy.

Rule change, 2026-09-07 (maintainer) — Module Adoption Policy, implemented by #3648, #3649, #3650 and #3651. This page describes the mechanism as it runs after those changes: a declared floor is advisory, a refused generation falls back to the previous one, a new build is adopted eagerly, and a platform roll is held only by a module that provably cannot load on the target.

The production model in one picture:

PR ──[merge preconditions]──▶ main ──[CI: build ALL images, tag by version]──▶ ACR
                                                                                 │
                                  each install polls ACR per Admin/UpdatePolicy ◀┘
                                  Stable (default) → newest clean release · Continuous + pattern → newest build the pattern admits · None → manual
                                                 │
                  AKS / local k3s: patch own deployment   MAUI: notify + relaunch

One central version, two channels (continuous build vs official release), and every install keeps itself up to date. The version mechanics live in Release Process & Versioning; where the images go is Deployment.


1. Merge preconditions

A PR may merge to main only when all four hold. CI enforces (a)+(b); branch protection + a reviewer enforce (c)+(d). The checklist is in .github/pull_request_template.md.

Precondition Enforced by
a Build is green with warnings-as-errors Live. dotnet-test.yml's build step runs dotnet build --no-restore -c Release -p:CIRun=true -warnaserror, and Directory.Build.props additionally sets TreatWarningsAsErrors=true centrally — so a compiler warning cannot merge. Doc-completeness warnings (CS1591/CS1573/CS1712) and the NU1510 restore advisory are suppressed centrally (NoWarn); NuGet vulnerability advisories (NU1901–NU1904) stay visible as warnings via WarningsNotAsErrors, so a newly-disclosed transitive CVE doesn't fail every unrelated PR. Doc-quality warnings (CS1574/CS0419/CS1570) are NOT suppressed — they fail the gate.
b Tests are green The sharded suite + the consolidated Test Results check (dotnet-test.yml).
c Reviewed (AI or human) A required approving review (branch protection). We don't care whether the reviewer is a person or an AI. .github/CODEOWNERS requests an owner.
d Review comments dealt with "Require conversation resolution before merging" (branch protection). We expect at least one comment or code change tied to the review.

Branch-protection settings to configure once (GitHub → Settings → Branches → main; gh can't set these): require the Build and Test Results status checks; require ≥1 approving review; require review from Code Owners; require conversation resolution before merging.


2. Versions: current-build vs official

Two shapes, and no others: X.Y.Z-ci.<n> for every continuous build, clean X.Y.Z for the release. No rc, no preview, no beta, no labelled line, ever — the scheme, the mint-versus-read split and the ordering consequences are stated once, authoritatively, in Release Process & Versioning §1; this page describes what the two channels do with them.

The one number is PlatformVersion in Directory.Build.propstoday 3.0.0, the next release. Every build derives its version from it (details):

🚨 Candidates are ranked by the CD run number, not by the version string (#3542). <n> is the ordering key: a 3.1.0-ci.7841 published before 3.0.0-ci.7977 loses to it, and so does a retired 3.0.0-rc9.ci.7824 — SemVer compares pre-release identifiers as text, which is how a mislabelled line outranked every sealed set for ever and rolled both AKS portals three days backwards on 2026-09-07. An official release carries no run number of its own (it is a promotion of a sealed set), so it is ranked by version — which is what keeps a Stable install running a continuous build able to reach it. Full reference: Self-Update Target Selection.

No pre-release label on the core beyond -ci.<n>, and no rc line — retired 2026-09-05, settled 2026-09-07. -ci.<n> is a channel marker, never a version; the release is a promotion of a sealed continuous set, not a rebuild; and nothing else may ever be minted. The bump to 3.1.0 still happens the day 3.0.0 is tagged — the lane opens that pull request. Full rule: Release Process & Versioning §1.

Cutting an official release and starting the next line

  1. Cut the official release: commit the notes page Doc/ReleaseNotes/3_0_0, then push an annotated v3.0.0 tag on a promoted, sealed commit. release.yml retags that commit's set with the clean version in ACR, mirrors it to GHCR, records the release marker and publishes the GitHub Release. Stable installs pick it up.
  2. Start the next line: merge the pull request the lane opened — PlatformVersion 3.0.03.1.0. Continuous builds are now 3.1.0-ci.<n>, above the release.

3. CI produces ALL images, tagged by version, on ACR

Both channels publish the full image set to meshweaver.azurecr.io, tagged by the version string — that tag is what each install compares.

Channel Trigger Version baked + image tag Workflow
Continuous green merge to main 3.0.0-ci.<run#> (+ short SHA + moving main) main-cd.yml
Official push an annotated v*.*.* tag on a promoted, sealed commit clean 3.0.0 — the continuous set retagged in ACR and mirrored to GHCR; nothing rebuilt release.yml

So the continuous build produces all images, the release names one of them, and a running install only has to list ACR tags and pick the best per its policy. (main-cd.yml still rolls the environments once as the bootstrap; steady-state updates are the self-updater below.)

Two properties of the continuous leg matter to a reader of tags:


4. The update policy — Admin/UpdatePolicy

A single mesh node, edited by platform admins under Settings → Updates (a dropdown and a pattern field bound straight to the node). Default Stable — clean releases only (maintainer, 2026-09-08: "by default we will not upgrade as long as no version without -ci… is labelled").

Policy pattern Behaviour
Stable (default) (optional; narrows to a line, e.g. 3.0.*) Roll only to the newest clean release (no build number).
Continuous required, e.g. 3.0.1-ci* Roll to the newest sealed continuous build the pattern admits — latest by CD run number, not by version string (why). 3.0.0-ci* never selects 3.0.1: following a line ends when its release is tagged. Without a pattern this is Stable, and the poller says so once at Warning.
None (ignored) Never auto-update. Apply updates manually (operator, or the admin tab's Apply available update now).

Fleet today (no clean release above 3.0.0 yet): memex and memex-cloud carry Continuous + 3.0.0-ci*, to be changed the day 3.0.1 is tagged. The record shapes and the semver rule are in Release Process & Versioning → "Which build an install takes".

The poller (SelfUpdateHostedService) reads this node live: changing the policy re-drives it immediately. It checks ACR a few times a day, records the latest tag it sees on the node (LatestAvailableTag, surfaced in the admin tab and to the MAUI notifier), and — where it can — applies the update.


5. How each install updates

Target What "update" does
AKS (memex portal) The portal patches its own Deployment image from inside the pod (Kubernetes API, projected service-account token). It rolls the portal AND migration deployments to the new tag together; k8s does the rolling update.
Local k3s on Mac Same Helm chart as AKS → same in-pod patch. (A version-specific tag pulls even under imagePullPolicy: IfNotPresent because the tag isn't cached. A pure local-build dev loop without ACR is effectively None.) See LocalColimaMac.
Monolith (non-k8s) No self-patch (no service-account token) → detect-only: records LatestAvailableTag for visibility; the operator updates the binary.
MAUI app Detect + notify. A sandboxed app can't replace its own binary, so on connecting to a remote mesh that runs a newer platform version it shows an in-app alert: update from the store and relaunch.

The availability gate — "newer" is not sufficient

Whichever target it is, a newer tag is not on its own a reason to roll. Before patching anything, the poller asks the release availability gate whether a module this deployment has landed provably cannot load on that release — the landed bytes linked against the surface the release published (the link gate, MeshWeaver#3651). If one cannot, the update is held: the poller stays on the current image, writes the refusal to Admin/UpdatePolicy (HeldTag, HeldReason, HeldAt), and the Updates tab reports it — the About tab shows ⏸️ Update held rather than ⬆️ Update available, because an install that has refused a build must not look like one that is about to take it.

What the gate reports without holding — a course with no prebuilt bake for that build, which compiles when the new build starts; a module whose loadability could not be measured, which the platform decides at start-up and keeps the previous version of if the new one cannot load — is written to the same node (AdvisoriesTag, Advisories) and shown on the tab beneath the available line. Only Modules:RequirePrebuilt turns the missing bake back into a hold.

A hold is not sticky. It is re-evaluated on every poll and every green-build event and clears itself the moment the missing artifact is published, so nothing has to be un-stuck by hand. The manual Apply update now button consults the same verdict — a gate the unattended path respects and the manual one does not is no gate at all.

🚨 "Cannot determine" is a hold too, and it says so distinctly: an unreadable artifact catalogue is an availability incident to fix, not an incompatible release to re-bake.

Postgres ("auto-update pg")


6. AKS prerequisites (for the in-pod patch + ACR polling)

The Helm chart (deploy/helm/templates/memex-portal/) ships these so the portal can update itself:

For ACR polling on AKS you must, once per environment, create a user-assigned managed identity, federate it to system:serviceaccount:<ns>:memex-portal-sa, grant it AcrPull on meshweaver.azurecr.io, and set selfUpdate.azureClientId (e.g. in values.aks.yaml / via Key Vault) to its client id. (Mirrors the existing pgbackrest-sa workload-identity wiring.) The in-cluster Deployment PATCH works without this; it only authenticates the tag-list call.


7. Operate & verify

The decision logic (which tag each policy picks; "is newer") is unit-pinned in VersionSelectTest (ordering, and the three-valued "does my own tag still exist" check) and SelfUpdateStrandRecoveryTest (the poller, against a real mesh); the enum dropdown in MeshNodeEditorFieldTest.


8. Cut a release — operator runbook

Three operator actions, two independent channels, and steady state is self-update: you push images (by merging or tagging) and installs roll themselves per Admin/UpdatePolicy — you do not kubectl set image by hand. Forcing a specific tag on one instance is a Roll Hosting/InstanceAction on the control instance (OperatingFromThePortal); the manual AKS runbook is what the operator runs for it, and the break-glass path when the control plane itself cannot act.

Step Action What ships Who rolls to it
a Merge to main (preconditions §1 green) main-cd.yml builds the multi-arch image set (amd64 + arm64), tags it 3.0.0-ci.<run#> (+ short SHA + moving main), pushes to ACR, bakes and seals Continuous installs whose pattern admits the tag (dev/test; the fleet's 3.0.0-ci* today)
b Push the annotated tag v3.0.0 on a promoted, sealed commit release.yml retags that set 3.0.0 in ACR, mirrors it to GHCR, records _releases/3.0.0, publishes the GitHub Release Stable installs — the default (prod)
c Merge the bump the lane opened (PlatformVersion3.1.0) continuous builds become 3.1.0-ci.<n> opens the next development line
# (a) ship a continuous build — just merge; CI builds + pushes + seals the image set
git switch main && git pull

# (b) cut the official release — an immutable, ANNOTATED tag on a commit whose CD run sealed
#     (Doc/ReleaseNotes/3_0_0 must already be committed; the lane refuses a release without notes)
git tag -a v3.0.0 -m "MeshWeaver 3.0.0" <sha> && git push origin v3.0.0

# (c) open the next line — merge the pull request release.yml opened:
#     release: PlatformVersion 3.0.0 → 3.1.0

(a) can look shipped and produce no image. CD reacts to MeshWeaver Build and Test completing on a push to main — a hand-kicked workflow_dispatch of that workflow turns main green and CD still skips. Verify the IMAGE (.github/scripts/check-image-set.sh <short-sha>), and re-drive CD with gh workflow run main-cd.yml --ref main if it is missing (it also self-heals HEAD on a hourly schedule).

(b) depends on (a): a tag can only name a set that a merge already shipped and sealed — that is the whole point of a promotion. (c) follows (b) once per release line, opened by the lane and merged by you. The version mechanics behind each step are in Release Process & Versioning; §2 above covers the same cut from the version-scheme angle.


9. See also

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.