feat(operator): hosting-migrate — a Roll's database migration runs as its own Job before the image moves - #5148
Conversation
… its own Job before the image moves A Roll was `kubectl set image` and nothing else, so a target expecting a newer db_version died on DbVersionGate behind old pods answering 200 (memex-cloud 2026-09-19, 8411 → 8955). helm renders a migration Job only on upgrade and the self-updater mints its own; the control lane's Roll — the routine path for every reporting instance — had neither (Systemorph/Memex#458, #460; Doc/Architecture/SelfUpdateSchemaWall option b1). hosting-migrate --namespace <ns> --release <release> --image <…/memex-migration:tag> reads the migration Job the RELEASE rendered (helm get manifest → kubectl client dry run → JSON), moves only the containers running the migration image (rehearsal + run) to the target, runs it as Job memex-migration-roll-<tag> outside the portal, and exits 0 only when the Job reports succeeded. Idempotent per tag (succeeded → reported; failed → deleted and re-run; running → waited on). Forbidden is REFUSED, never absent; a release with no migration Job, a failed Job, a vanished Job or one past its own activeDeadlineSeconds (+grace) is a non-zero exit naming the Job with its log tail. Pure bash + jq + kubectl + helm: the operator image has no awk and no python. 13 new cases in run-tests.sh (stubs/migrate, fixtures/migrate): 537/537 on macOS bash 5.3 and in ubuntu:24.04; check-stderr-discarded and check-rbac-coverage clean (pods/log and batch/jobs already granted). The Roll plan calling this is the Plugins half, opened as a draft until an operator image carrying this command is on hosting-operator:main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
New documentation and script header comments hard-code a decision date/attribution that should be removed to avoid policy text rotting over time.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
What changed in this PR
This PR adds an AKS operator-side step (hosting-migrate) to run a release-rendered database migration Job (as a standalone Job) before a Roll moves the portal image, and documents the operator/control-plane gap this closes.
Changes:
- Add
deploy/aks/operator/bin/hosting-migrateto render the release’s migration Job viahelm get manifest, retarget only the migration containers to the target migration image, run it asmemex-migration-roll-<tag>, and gate success onstatus.succeeded >= 1. - Add a focused bash test harness with helm/kubectl stubs + fixtures to cover success, failure, forbidden, missing-Job, and budget-expiry scenarios.
- Update
SelfUpdateSchemaWall.mdto describe the new operator-side command as the missing half of theRollplan.
| File | Description |
|---|---|
| src/MeshWeaver.Documentation/Data/Architecture/SelfUpdateSchemaWall.md | Documents hosting-migrate as the operator-side mechanism to ensure migration precedes image move in a Roll plan. |
| deploy/aks/operator/bin/hosting-migrate | New operator command to run the chart-rendered migration Job out-of-band and block image movement until it succeeds. |
| deploy/aks/operator/test/run-tests.sh | Adds end-to-end stubbed tests for hosting-migrate behavior and failure modes. |
| deploy/aks/operator/test/stubs/migrate/kubectl | Kubectl stub to simulate Job lifecycle and capture created Job JSON for assertions. |
| deploy/aks/operator/test/stubs/migrate/helm | Helm stub to provide a manifest for helm get manifest and simulate missing release behavior. |
| deploy/aks/operator/test/fixtures/migrate/rendered.json | Fixture JSON representing the parsed/“rendered” Job used to validate retargeting and carry-over fields. |
| deploy/aks/operator/test/fixtures/migrate/manifest.yaml | Fixture manifest placeholder used by the helm stub. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| operator route has the same ordering the in-pod route has had since the seam landed. Until it does, | ||
| a `Roll` across a schema bump is `Roll` + `Reconcile`, in that order, by hand. | ||
|
|
||
| **The operator half is `hosting-migrate`** (`deploy/aks/operator/bin/`, 2026-09-21): it reads the |
There was a problem hiding this comment.
Agreed — that date is a policy marker, not evidence: it is the day the rule "a schema change ships as a new image, and every roll runs its migration first, outside the portal, as its own Job before the image moves" came into force, and it would have to change if the decision changed. That is policy policy-not-prose: the date moves into the register, the prose cites the id.
Fix pushed after this reply: a new register row roll-migrates-first in Doc/Architecture/PolicyNotProse carries the value, the in-force date and who set it; the flagged line now cites roll-migrates-first and links to the register instead of carrying the date. Nothing is deleted — the information moved.
Test Results (shard 0) 1 files 1 suites 2m 19s ⏱️ Results for commit 0937690. ♻️ This comment has been updated with latest results. |
Test Results (shard 3)443 tests 443 ✅ 56s ⏱️ Results for commit 0937690. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)1 590 tests 1 590 ✅ 3m 1s ⏱️ Results for commit 0937690. ♻️ This comment has been updated with latest results. |
Test Results (shard 4) 3 files 3 suites 5m 54s ⏱️ Results for commit 0937690. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)723 tests 531 ✅ 5m 41s ⏱️ Results for commit 0937690. ♻️ This comment has been updated with latest results. |
Test Results (shard 5) 5 files 5 suites 15m 42s ⏱️ Results for commit 0937690. ♻️ This comment has been updated with latest results. |
Test Results 17 files 17 suites 33m 35s ⏱️ Results for commit 0937690. ♻️ This comment has been updated with latest results. |
…hosting-migrate not-yet-wired
Two findings on this branch, both answered here.
Copilot (thread 4065102977): the doc line naming `hosting-migrate` carried the decision date. That
is a policy marker — policy `policy-not-prose` — so the date moves into the register as row
`roll-migrates-first` (value, in-force date, who set it) and the SelfUpdateSchemaWall line cites
the id and links to the register. Nothing deleted, the information moved.
`Operator scripts (shellcheck + behaviour)`: `hosting-migrate` shipped without a plan entry and
without a not-yet-wired declaration, which the gate refuses by design ("a shipped command nobody
claims is untested and unreachable"). It is the `hosting-db-release` shape exactly: the script is
behaviour-tested against test/stubs/migrate, and the Roll plan step that emits it is draft
Plugins#2219, held until an operator image carrying the command is on hosting-operator:main. So it
is declared not-yet-wired naming that PR; the line moves up into the plan list in the change that
merges the plan step. The tracked-file floor moves to the REAL count (31: 28 commands + run.sh +
_common.sh + _audit.jq) — it was already one behind on main.
Verified locally: the gate's plan/not-wired step passes on this tree; shellcheck clean; the
workflow timeout guard reports 0 violations; MeshWeaver.Documentation.Test builds Release
-warnaserror with 0 Error(s) and DocumentationLinkIntegrityTest + ArchitectureTopicMapTest +
NoConflictMarkersGuard pass (6 of 6, trx).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Why
A
RollHosting/InstanceActionwas onlykubectl set image. When the target image expects a newerdb_version, the new pod dies on DbVersionGate while the old pods keep answering 200. Measured on memex-cloud 2026-09-19, rolling3.0.0-ci.8411→8955: CrashLoopBackOff, and no migration Job in the namespace.Two paths already run the migration as a separate Job:
helm upgrade(memex-migration-<revision>) and the in-pod self-updater (memex-migration-su-<tag>). The control lane'sRollhas neither, and it is the routine path: an instance that reports to a control instance hands its updates over instead of patching itself.Maintainer rule (2026-09-21, Systemorph/Memex#460): a schema change ships as a new image, and every roll runs its migration first, outside the portal, as a separate Job. Context: Systemorph/Memex#458. This is option (b1) in
Doc/Architecture/SelfUpdateSchemaWall.What
hosting-migrate --namespace <ns> --release <release> --image <registry>/…/memex-migration:<tag>helm get manifest, then akubectl create --dry-run=client -o jsonto get JSON, then the Job labelledapp.kubernetes.io/component=memex-migration. Everything the chart puts on that Job is kept: wait-for-postgres, the rehearsal init container,activeDeadlineSeconds, envFrom (config, secrets and any Key Vault synced Secret) and the pull Secret.--image.memex-migration-roll-<tag>and waits. Exit 0 only when the Job reportssucceeded ≥ 1.Behaviour in each case:
--imagenames the portal imageIt uses only bash, jq, kubectl and helm, because the operator image (Azure Linux 3) has no awk and no python.
Tests
13 new cases in
run-tests.sh(stubs/migrate,fixtures/migrate). 537 passed, 0 failed on macOS bash 5.3 and inubuntu:24.04.check-stderr-discardedandcheck-rbac-coverageare clean;pods/logandbatch/jobsare already granted.What else is needed
Rollplan calls this beforeset image. That PR is opened as a draft until an operator image with this command is onhosting-operator:main.🤖 Generated with Claude Code