Skip to content

feat(operator): hosting-migrate — a Roll's database migration runs as its own Job before the image moves - #5148

Merged
meshweaver-cloud[bot] merged 2 commits into
mainfrom
feat/hosting-migrate-roll-job
Sep 22, 2026
Merged

meshweaver-cloud[bot] merged 2 commits into
mainfrom
feat/hosting-migrate-roll-job

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Why

A Roll Hosting/InstanceAction was only kubectl set image. When the target image expects a newer db_version, the new pod dies on DbVersionGate while the old pods keep answering 200. Measured on memex-cloud 2026-09-19, rolling 3.0.0-ci.84118955: 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's Roll has 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>

  1. Reads the migration Job the release itself rendered: helm get manifest, then a kubectl create --dry-run=client -o json to get JSON, then the Job labelled app.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.
  2. Moves only the containers that run the migration image (rehearsal and run) to --image.
  3. Runs it as memex-migration-roll-<tag> and waits. Exit 0 only when the Job reports succeeded ≥ 1.

Behaviour in each case:

Situation Result
This tag's Job already succeeded Reported, not run again
This tag's Job failed before Deleted and run again
This tag's Job is still running Waited on
Forbidden on the Job read REFUSED, not treated as absent; nothing is created
Release renders no migration Job Refusal; the image must not move without one
Failed Job, vanished Job, or Job past its own budget + grace Non-zero exit naming the Job, with its log tail
--image names the portal image Refused

It 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 in ubuntu:24.04. check-stderr-discarded and check-rbac-coverage are clean; pods/log and batch/jobs are already granted.

What else is needed

  • Plugins: the Roll plan calls this before set image. That PR is opened as a draft until an operator image with this command is on hosting-operator:main.
  • Once published, pearl rolls through it (Memex#458).
  • Merging this changes nothing that runs until the Plugins plan calls it. Nothing to recycle.

🤖 Generated with Claude Code

… 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>
Copilot AI lite review requested due to automatic review settings September 21, 2026 18:16

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.

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 Low severity

Open (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-migrate to render the release’s migration Job via helm get manifest, retarget only the migration containers to the target migration image, run it as memex-migration-roll-<tag>, and gate success on status.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.md to describe the new operator-side command as the missing half of the Roll plan.
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files    1 suites   2m 19s ⏱️
318 tests 318 ✅ 0 💤 0 ❌
322 runs  322 ✅ 0 💤 0 ❌

Results for commit 0937690.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

443 tests   443 ✅  56s ⏱️
  3 suites    0 💤
  3 files      0 ❌

Results for commit 0937690.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 590 tests   1 590 ✅  3m 1s ⏱️
    2 suites      0 💤
    2 files        0 ❌

Results for commit 0937690.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files      3 suites   5m 54s ⏱️
1 938 tests 1 938 ✅ 0 💤 0 ❌
1 939 runs  1 939 ✅ 0 💤 0 ❌

Results for commit 0937690.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

723 tests   531 ✅  5m 41s ⏱️
  3 suites  192 💤
  3 files      0 ❌

Results for commit 0937690.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    5 files      5 suites   15m 42s ⏱️
3 906 tests 3 904 ✅ 2 💤 0 ❌
3 910 runs  3 908 ✅ 2 💤 0 ❌

Results for commit 0937690.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Test Results

   17 files     17 suites   33m 35s ⏱️
8 918 tests 8 724 ✅ 194 💤 0 ❌
8 927 runs  8 733 ✅ 194 💤 0 ❌

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>
@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit e0c8be2 Sep 22, 2026
37 checks passed
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