diff --git a/.claude/workflows/docs-accuracy-audit.js b/.claude/workflows/docs-accuracy-audit.js index 6a85f6c3f0..1ff672f723 100644 --- a/.claude/workflows/docs-accuracy-audit.js +++ b/.claude/workflows/docs-accuracy-audit.js @@ -210,6 +210,7 @@ const ALL_HANDWRITTEN = [ "content/docs/ui/setup-app.mdx", "content/docs/ui/translations.mdx", "content/docs/ui/views.mdx", + "content/docs/upgrading.mdx", ] // diff --git a/content/docs/meta.json b/content/docs/meta.json index 968b3e7a1b..6da91d755b 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -13,6 +13,7 @@ "permissions", "ai", "api", + "upgrading", "---Platform---", "deployment", "plugins", diff --git a/content/docs/upgrading.mdx b/content/docs/upgrading.mdx new file mode 100644 index 0000000000..71b4db4c36 --- /dev/null +++ b/content/docs/upgrading.mdx @@ -0,0 +1,206 @@ +--- +title: Upgrading +description: ObjectStack upgrades come in two halves that run on separate clocks — the platform runtime and your metadata app. Which one you are doing, what each one moves, and where the per-major checklists live. +--- + +There are **two upgrades** on this platform, and they run on separate clocks: + +| | Platform runtime | Metadata app | +| :--- | :--- | :--- | +| Ships as | a Docker image (or the `os` CLI on a host) | a compiled artifact, `dist/objectstack.json` | +| Versioned by | our release train (`17.0.0`) | your own catalog | +| Whose cadence | ours | yours | +| The upgrade action | move the image tag, restart | `os migrate meta --from 16`, then rebuild and ship | +| Touches your metadata? | no | yes — it rewrites your source | + +Most upgrade questions are really the question *which of these am I doing*. You +can do either one alone. A platform move does not rewrite your app, and +republishing your app does not move the runtime. + + +**The short version.** Moving the runtime forward one major keeps working with +metadata authored against the previous major — the loader converts it as it +reads. Falling **two** majors behind is what breaks, because the conversion +window is one major wide. See [when a platform move forces an app +move](#when-a-platform-move-forces-an-app-move). + + +## The platform runtime + +The platform ships as a single version-locked train: every `@objectstack/*` +package shares one version number, and that number is the platform version. + +### Moving the tag + +The official image is `ghcr.io/objectstack-ai/objectstack`, and its tags mirror +`@objectstack/cli` versions (`17.0.0`, `17.0`, `17`, `latest`). **Pin the exact +version in production** and move it deliberately: + +```bash +# docker-compose.yml, or your orchestrator's manifest +image: ghcr.io/objectstack-ai/objectstack:17.0.0 +``` + +On a host running the artifact directly under systemd, the same move is a file +swap: replace the artifact and restart the service. Roll back by restoring the +previous artifact. + +See [Self-hosting](/docs/deployment/self-hosting) for both deployment shapes in +full. + +### What the boot does when the database disagrees + +Moving the runtime can leave the physical database shaped for the previous +version. What happens next depends on which deployment shape you run. + +**The standing production policy is hands-off.** Under `NODE_ENV=production`, +automatic reconciliation is ignored and every divergence is *warned* about, on +the assumption that an operator runs `os migrate` deliberately: + +```bash +os migrate plan # how the database has drifted, categorised safe / needs-confirm / destructive +os migrate apply # applies the loosening changes +os migrate apply --allow-destructive # the narrowing ones, once you have read the plan +``` + +**The artifact-pinned boot is stricter**, because there is nobody at a terminal: +a container simply comes up carrying a different artifact than the one that +shaped the database. On that path the boot applies safe and needs-confirm drift +itself, and **refuses to start on destructive drift**, printing every change and +the command that resolves it. Nothing is skipped silently — "shrug and serve" is +the state that gate exists to delete. The refusal happens before the HTTP port +binds, so a refused boot is a boot that never served traffic. + +### When a platform move forces an app move + +Each package declares the protocol major it was authored against, and the +runtime checks that handshake **first**, before it loads anything: + +```ts +manifest: { + // ... + engines: { protocol: '^17' }, +} +``` + +The load path then converts old metadata shapes to the canonical current shape +as it reads them, emitting a deprecation notice per conversion. **That window is +exactly one major wide.** Metadata authored against the previous major loads and +runs; a shape retired one major further back is *rejected*, and the rejection +carries the fix — the old spelling, the new spelling, and the command that +rewrites your sources. + +The practical consequence: + +- **One major behind** — it runs. You will see conversion notices in the boot + log. Migrate at your convenience. +- **Two or more majors behind** — the load rejects the retired shapes. The + metadata half is no longer optional, and it is the next section. + +## The metadata app + +Your app is versioned by you, upgraded by you, and shipped as a compiled +artifact. Upgrading it across one or more protocol majors is one command +followed by your normal build. + +### One command covers every major you skipped + +```bash +os migrate meta --from 15 # the major your metadata was authored against +``` + +`--from` is the major **you** wrote against, not the one you are going to. The +command replays every step between that major and this runtime's, in order, in a +single pass — so upgrading across skipped majors does **not** mean running it +once per major, and does not mean reading several release checklists and merging +them by hand: + +| You ran | Steps it replays | +| :--- | :--- | +| `os migrate meta --from 16` | 17 | +| `os migrate meta --from 15` | 16, 17 | +| `os migrate meta --from 14` | 15, 16, 17 | +| `os migrate meta --from 12` | 13, 14, 15, 16, 17 | + +The chain reaches back to **protocol 10** — that floor is a release-policy +decision, not an accident of what still exists. Below it the command refuses +with a message naming the floor rather than half-migrating you. + +Useful flags: + +| Flag | What it does | +| :--- | :--- | +| `--step` | Report each major's hop separately, so a failure bisects to the exact major | +| `--out migrated.stack.json` | Write the migrated stack instead of only reporting it | +| `--to 16` | Stop at an intermediate major instead of this runtime's | +| `--json` | Machine-readable output, for CI or an agent | + +It rewrites **your source** and reads no database. (Its sibling, +`os migrate meta --stored`, does the opposite — it rewrites one deployment's +stored metadata rows and reads no config. The two are mutually exclusive.) + +### What it does not do — read the output + + +`os migrate meta` applies the **mechanical** rewrites and never guesses at the +rest. Changes that cannot be converted losslessly are reported as structured +to-dos for you to resolve — they are not applied, and a clean-looking run can +still carry dozens of them. The command's output is the work list, not a receipt. + + +That split is the reason the per-major checklists below still matter: renames +and retired keys the tool handles; decisions it cannot make for you stay yours. +The run also ends by naming the per-deployment **data** migrations that remain +(`os migrate files-to-references`, `os migrate value-shapes`, and friends) — +scoped to the field classes your metadata actually declares. It reads no +database, so that list is what is left to consider, never what this deployment +has already done. + +### The loop + +```bash +os migrate meta --from 15 # 1. rewrite sources; read the to-dos + # 2. resolve them, and the checklist items below +os validate # 3. the gate — schema, CEL predicates, widget bindings +os build # 4. compile to dist/objectstack.json + # 5. ship the artifact; restart +os migrate plan # 6. reconcile the database to the new metadata +``` + +Step 3 is the real gate: `os validate` runs the same checks as `os build` but +writes no artifact, which makes it the fast inner loop while you work through +the to-dos. Full command reference in the [CLI +documentation](/docs/deployment/cli). + +## Per-major specifics + +Everything above is the mechanism. The *contents* of a given major — which keys +were renamed, which defaults changed, which grant you now have to declare +explicitly — live on that major's release page, written for app authors and +compiled at release time. + +Read the checklist for **every major you are crossing**, not only the one you +are landing on: + +| Release | Upgrade checklist | +| :--- | :--- | +| v17.0.0 | [Upgrade checklist](/docs/releases/v17#upgrade-checklist) | +| v16.0.0 | [Upgrade checklist](/docs/releases/v16#upgrade-checklist) | +| v15.0.0 | [Upgrade checklist](/docs/releases/v15#upgrade-checklist) | +| v14.0.0 | [Upgrade checklist](/docs/releases/v14#upgrade-checklist) | +| v13.0.0 | [Upgrade checklist](/docs/releases/v13#upgrade-checklist) | +| v12.0.0 | [Upgrade checklist](/docs/releases/v12#upgrade-checklist) | +| v9.0.0 | [Upgrade checklist](/docs/releases/v9#upgrade-checklist) | + +Curated notes for v10 and v11 were never backfilled; consult the per-package +`CHANGELOG.md` files for those two. The [release notes +overview](/docs/releases) summarizes what each major changed. + +## Not this page + +**Upgrading an installed package** — a template app or a third-party package +already installed into a deployment — is a different operation with its own +lifecycle (pre-check, plan, snapshot, execute, validate, commit or roll back). +It is neither of the two halves above: it moves *someone else's* metadata inside +your deployment. See the [package upgrade +protocol](/docs/references/kernel/package-upgrade).