diff --git a/content/docs/releases/v18/18-0.mdx b/content/docs/releases/v18/18-0.mdx new file mode 100644 index 00000000000..b201189509e --- /dev/null +++ b/content/docs/releases/v18/18-0.mdx @@ -0,0 +1,709 @@ +--- +title: 18.0.0 +description: "The v18 GA release, published 2026-10-20: organization ownership is total (ADR-0131), so a database 17.x created takes the ownership ceremony before 18.0.0 serves it; protocol 18; a flow reads one expression dialect per slot; the security catalog is metadata; API keys expire; the CORS default admits no cross-origin caller. With its upgrade checklist, walked from 17.7.0." +--- + +## Highlights — 18.0.0 + +- **A database 17.x created is upgraded by a ceremony before 18.0.0 serves it.** + ADR-0131 makes organization ownership total: no NULL `organization_id`. + `os serve` 18.0.0 starts on a database it created itself, or on one that + `os migrate organization-ownership` certified, and refuses any other before it + listens (`dd29bbe`, `12d1476`). The ceremony's plan reads only, and `--apply` + runs only the plan you reviewed (`f9b4217`). ⚠️ **Read [step 2 of the upgrade + checklist](#2-upgrade-the-database) before the first 18.0.0 boot of a 17.x + database.** +- **Protocol 18.** `PROTOCOL_VERSION` is `18.0.0`: the handshake admits + `engines.protocol: '^18'` and refuses `'^17'` (`4e9fe9f`). The ADR-0087 + conversions whose one-major window ran out leave the load path, so a retired + spelling is refused with its rename instead of being rewritten (`5fd2c76`, + `c5dd7fc`, `bc0538a`, `105902f`, `8693110`, `d176b0a`, `f606ba2`, `f901746`). +- **A flow reads one expression dialect per slot** (ADR-0032). A value slot takes + a CEL value envelope and refuses the single-brace `{…}` template; a text slot + takes `{{ }}` holes; a CEL root the flow does not bind is refused at + `os validate`; `$` names belong to the engine; and every builtin node's + `config` is held to its executor's contract (`0e9e7b7`, `2b61f2d`, `7958bdf`, + `5fb1746`, `2f70c22`). +- **Positions, permission sets and capabilities are metadata.** The four catalog + row objects retire (`sys_position`, `sys_permission_set`, + `sys_position_permission_set`, `sys_capability`), a position names its sets in + `permissionSets`, a grant names its set by name, and a name has one holder per + deployment (ADR-0131 D3/D4; `66949b7`, `bfc15d2`, `6f21216`, `fe98cc6`). + Declared sharing rules, webhooks and email templates are read from the registry + and are read-only (`5e97907`, `191e043`, `7de0265`). +- **Page components are judged as errors.** An undeclared prop, a value its row + refuses or a missing binding fails `os validate`, `os build`, `os lint` and the + metadata save door, where 17.7.0 printed an advisory (`4f68ba1`, `32fa194`, + `d0c09e4`). An element binds its data through `dataSource` only (`317cddd`). +- **Credentials and origins close by default.** A key created without an expiry + lives 90 days, a key unused for 90 days is refused, and a key that existed + with no expiry has one year from the upgrade (`1d79556`). The CORS default + admits no cross-origin caller, and `'*'` with credentials fails the boot + (`f7546a4`). The inbound `api` hook verifies only the timestamped signature + (`5ce6773`). +- **The AI skills ship with the packages,** as `@objectstack/skills`, on the same + version as every other `@objectstack/*` package. + +--- + +## Release facts, as measured + +Read with `npm view` and `npm pack` on 2026-10-20, after the publish: + +| | Measured | +| :--- | :--- | +| `latest` | `18.0.0`, on every `@objectstack/*` package read (`spec`, `cli`, `runtime`, `core`, `objectql`, `skills`, `organizations`) and on `create-objectstack` | +| `next` | `18.0.0-next.5`, which is **older** than `latest`. ⛔ Never install `@next` for 18: it resolves a prerelease that 18.0.0 supersedes | +| `rc` | `17.0.0-rc.6` (`@objectstack/skills` and `@objectstack/organizations` carry no `rc` tag) | +| Published | `@objectstack/cli@18.0.0` at 2026-10-20 12:44 UTC | +| Prereleases | `18.0.0-next.0` … `next.3` and `next.5`. `next.4` was never published, although the CHANGELOG carries a section for it | + +Install 18 by its exact version (`18.0.0`) or a caret range (`^18.0.0`); both +resolve `latest`. Moving the stale `next` tag is a release act, outside these +notes. + +### How to read the 18.0.0 CHANGELOG + +The `## 18.0.0` section of each package's `CHANGELOG.md` is the whole v18 line: +every changeset of every `18.0.0-next.N`, in landing order. In +`@objectstack/spec` it holds 341 entries; across the 53 packages whose +CHANGELOG has an `18.0.0` section it holds 1,115 distinct entries, 340 of them +marked breaking or narrowing. Read it with two cautions: + +- **A later entry can undo an earlier one.** In the `CHANGELOG.md` the published + `@objectstack/cli@18.0.0` tarball carries — the copy `node_modules` holds + after you install 18.0.0 — `ae6bd1a` says "18.0 supports fresh `single` + installs only — `os migrate organization-ownership` refuses, and `os serve` + refuses a walled posture at boot"; `12d1476` and `fc34d00`, later in the same + section, open the ceremony and lift the walled refusal, and the 18.0.0 code + does what the later entries say. The repository's + `packages/cli/CHANGELOG.md` no longer reads that way: #24912 amended the + released `ae6bd1a` entry in place (it now reads "the 18.0 pre-release + refusals of `os migrate organization-ownership` and of a walled posture at + boot, both lifted in 18.0.0") and `fc34d00`'s headline ("… supported on fresh + installs and on databases the ownership ceremony upgraded"); the 18.0.0 + tarball keeps the old text. `record:json` is declared by `97b8e7a` and + retired by `030a7b8`; it shipped only in `18.0.0-next.3`. +- **The refusal is the authority.** Every refusal this page quotes was read off + the published 18.0.0 packages, not off the CHANGELOG: where the two differ, + the package's own message says what the package does. + +The checklist below is curated from that record, not copied from it: it keeps +the changes an application must edit for, and leaves out the ones that move only +the platform's internals, its own tooling and the Console's code (the Console's +changes are in the `@objectstack/console` CHANGELOG). + +--- + +## Upgrade checklist + +⚠️ One checklist per release, for the release you are landing on **and** every +release you cross to get there: a 17.4.0 app reads the +[17.5.0](/docs/releases/v17/17-5#upgrade-checklist), +[17.6.0](/docs/releases/v17/17-6#upgrade-checklist) and +[17.7.0](/docs/releases/v17/17-7#upgrade-checklist) lists before this one. On an +`18.0.0-next.N` prerelease, read the CHANGELOG entries after your prerelease +instead: this list is written for 17.x. + +### 18.0.0 + + +**How far this list has been walked.** On 2026-10-20, from 17.7.0, twice: an +app scaffolded with `create-objectstack@17.7.0` (the `blank` template, pnpm, its +skills installed by `skills add`) was booted once on 17.7.0 with +`objectstack start`, so that it created a SQLite database holding one admin and +three records (the second time also an API key and a comment with an emoji +reaction), and was then moved to 18.0.0 from npm. The first pass drafted steps +1 and 2; the second, on a fresh scaffold, followed them as written, and both +printed the numbers quoted below. Each line says how it was measured: + +- **Measured in the walk** — the step was run, and the quoted text is what 18.0.0 + printed. +- **Probed on 18.0.0** — the walk's app carries none of it, so a probe stack was + refused by the installed 18.0.0 packages (`defineStack`, or `os validate` + exiting `1`), and the quoted text is that refusal. +- **Read off the 18.0.0 tarball** — the code or the message was found in the + published package (`npm pack @objectstack/PKG@18.0.0`), and nothing was run. + +⛔ The walk was one small app on SQLite, under the `single` posture, plus one +upgrade of the same app's database created under `group` (step 4) and one of an +`os dev` database (step 2). `isolated` databases, Postgres, MySQL and Turso were +not walked, and no upgrade of a production application was run for this page. + + +#### 1. Move the pins + +- **Move every `@objectstack/*` dependency to 18.0.0 together**, + `devDependencies` included, and regenerate the lockfile — + [Moving the dependency pins](/docs/upgrading#moving-the-dependency-pins). The + walk moved the scaffold's seven `^17.7.0` ranges (`@objectstack/cli` among + them) to `^18.0.0`; `pnpm install` moved all seven to 18.0.0 and printed no + peer-dependency warning. + *Measured in the walk.* +- **Move the protocol declarations to 18.** `engines.protocol` in your + `defineStack` manifest becomes `'^18'`, and `specVersion` in + `objectstack.manifest.json` becomes `'^18.0.0'` — what + `create-objectstack@18.0.0` stamps. `os migrate meta --from 17 --write` + rewrites `engines.protocol` for you (it printed + `objectstack.config.ts:38 manifest.engines.protocol: '^17' → '^18'`); + `specVersion` is a hand edit, and neither `os validate` nor the boot reads it. + Left on `'^17'`, `os validate` and `os build` still pass with a warning, and + the boot refuses before it serves (exit `1`): + + ```text + ⚠ Installed @objectstack/spec is v18.0.0 but this app declares engines.protocol '^17', which targets protocol v17. + ✗ package 'com.example.v17app' targets protocol ^17 (engines.protocol) but this runtime is protocol 18.0.0. This is a major-version break. Run: objectstack migrate meta --from 17 + ``` + + *Measured in the walk.* +- **Read `os migrate meta --from 17` as a list to search, not a list to do.** On + the walk's app — one object, two fields, no flow, page or permission set — it + printed `406 manual change(s) require your judgment`: every protocol-18 manual + entry it could not prove absent from the stack. Step 3 below is the curated + version for an application. *Measured in the walk.* +- **Move the AI skills.** From v18 the catalog is the `@objectstack/skills` + package. A 17.x project has none: its skills were installed with + `skills add` from the repository. Remove those, add the package at your + `@objectstack/spec` version with **your project's package manager**, and copy + it into your agent's directory: + + ```bash + npx skills remove objectstack-ai objectstack-api objectstack-automation \ + objectstack-data objectstack-formula objectstack-i18n objectstack-platform \ + objectstack-pm-dispatch objectstack-query objectstack-ui objectstack-upgrade -y + pnpm add -D "@objectstack/skills@$(node -p "require('@objectstack/spec/package.json').version")" + # npm: npm install --save-dev "@objectstack/skills@…" · yarn: yarn add -D "@objectstack/skills@…" + npx skills@1.7.2 experimental_sync --agent claude-code --copy -y + ``` + + `npx skills list` shows what is installed first — the walk listed the ten + `objectstack-*` skills — and the removal deletes only the names it is given. + Run before the removal, the sync skips every skill a `skills add` installed, + exits `0`, and leaves the 17.x-era skill text in place: + + ```text + ! Skipped objectstack-ai from @objectstack/skills: installed with `skills add` from objectstack-ai/objectstack + … (one line for each of the ten skills) + — Nothing to sync. + ``` + + After the removal it printed `Synced 10 skills`. In the walk's pnpm project + `npm install --save-dev` exited `1` with + `npm error Cannot read properties of null (reading 'edgesOut')`, so add the + package with the manager that installed the project (`create-objectstack` + uses pnpm when it is available). The command writes the exact version, + `"@objectstack/skills": "18.0.0"`: move it with the other pins at the next + upgrade. *Measured in the walk.* +- **Run `os validate`, then `os build`.** On the walk's app both passed on 18.0.0 + once the declarations read `'^18'`. Step 3 lists what fails a richer app. + *Measured in the walk.* + +#### 2. Upgrade the database + +A database that 17.x (or an `18.0.0-next.N` prerelease) created carries no +organization-ownership marker, and 18.0.0 does not serve it until the ownership +ceremony has certified it. A database 18.0.0 creates from empty writes its own +birth marker and needs none of this. The overview is +[Upgrading to 18.0](/docs/upgrading#upgrading-to-180); the command's reference is +[`os migrate organization-ownership`](/docs/deployment/cli#os-migrate-organization-ownership); +the lines below are the walk's measured output for each step. + +- **Before the first 18.0.0 boot, clear the security-catalog overlays:** + `os migrate security-catalog-overlays` lists the environment's stored + permission-set and position rows whose name a package now holds, which a v18 + cold boot refuses with `422 NAMESPACE_CONFLICT`; the preview exits `1` when it + lists a row, and `--apply` deletes them (`db9cf80`, `e030d43`). On the walk's + 17.7.0-created database it exited `0`: + `✓ No environment-wide permission-set or position row is stored over a package-held name — nothing here refuses the cold boot`. + *Measured in the walk.* +- **Start 18.0.0 against the database once.** It is refused before it listens, + exit `1`, from the `com.objectstack.organization-ownership-boot-guard` plugin + (error code `ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED`): + + ```text + ✗ This database carries no organization-ownership marker: 17.x or an 18.0 pre-release (18.0.0-next.*) created it, and this release serves only a database it created itself or one its ownership ceremony upgraded. sys_audit_log, sys_flow_credential, sys_flow_dispatch, sys_job, sys_job_queue, sys_job_run, sys_metadata, sys_metadata_audit, sys_metadata_commit, sys_metadata_history, sys_migration, sys_migration_journal, sys_secret still carry a physical organization_id column its object no longer declares. The server refuses to start rather than serve it in a shape it was never migrated to. Run 'os migrate organization-ownership --out PLAN' to write and review its plan, take a backup, then run 'os migrate organization-ownership --apply --plan-file PLAN --backup-taken', and start again. + ``` + + The refused boot is not wasted: its schema sync creates the tables 18.0.0 + adds, which the ceremony moves rows into. In the walk, a plan drawn before + that boot covered 77 tables and one drawn after it 81. *Measured in the walk.* +- **Do not reach for `os migrate apply --allow-destructive` first.** Before the + ceremony, the boot log and `os migrate plan` list the `organization_id` + columns as orphaned and name `os migrate apply --allow-destructive` (28 + destructive changes in the walk, with no word of the ceremony). Run on a copy + of the walk's database, that apply exited `1` and dropped nothing: + `✗ Refusing to drop sys_comment.reactions: the emoji reactions stored in it are not yet carried into sys_comment_reaction records …`. + The ceremony comes first. *Measured in the walk, on a copy.* +- **Take a backup.** The platform takes none, and `--apply` asks you to state + that you took one. For SQLite, stop the server and copy the database file + with its `-wal` and `-shm` files, or use `sqlite3 FILE ".backup …"`. + *Measured in the walk (a stopped-server copy).* +- **Write the plan and review it:** + `os migrate organization-ownership --out plan.json`. It reads only; per table + it prints the fate (`attribution`, `column-drop`, `mirror-deletion`, + `report`), the row count, the rows with no organization, and whether the + table will receive `NOT NULL`. The walk's plan: 81 tables, 34 to receive + `NOT NULL`, `unattributableRows: 0`. A row whose owner cannot be derived + blocks certification until you give it an organization or delete it. + *Measured in the walk.* +- **Apply the reviewed plan:** + `os migrate organization-ownership --apply --plan-file plan.json --backup-taken`. + The walk's run attributed 32 rows, rewrote 2 grants to name their permission + set, deleted 49 mirror rows (the retired catalog rows and the seeded email + templates), dropped 14 `organization_id` columns, set `NOT NULL` on 32 tables, + and ended `✓ The ceremony is complete; recorded as adr-0131-organization-ownership.` + The walk ran it with no server running. *Measured in the walk.* +- **Start the server.** In the walk 18.0.0 served the database: the 17.7.0 + admin signed in with the same password and kept `platform_admin`, + `org_owner` and `admin_full_access`; the three notes read back with their + organization, and a fourth was written; an API key minted on 17.7.0 still + answered `200`. *Measured in the walk.* +- **Then clear the drift the ceremony leaves, and only that.** The first served + boot still logged three orphaned columns (`sys_comment.reactions`, + `sys_sso_provider.created_by` / `updated_by`; on the `group` walk's database + also the index `idx_sys_sso_provider_organization_id`). Carry the stored emoji + reactions into `sys_comment_reaction` records first — + `os migrate comment-reactions` to preview, then + `os migrate comment-reactions --apply --yes`, which printed + `✓ Converted 1 reaction(s); … the run is recorded as verified` (`9963322`, + `6673c80`); without `--yes` it applies nothing and asks for it. Then run + `os migrate plan` and read its destructive entries: ⛔ go on only if they are + the ceremony's leftovers listed here. `os migrate apply --allow-destructive` + applies **every** destructive change in the plan, drops and tightenings + alike, so any other entry is your app's own — a column that may hold data — + and is decided separately, not in this step. Then run + `os migrate apply --allow-destructive` in a terminal: it prints the changes + and asks `Apply 4 change(s) to DATABASE? [y/N]` (four on the `group` walk's copy); answer + `y` only when the list is the one you checked (`n` printed + `ℹ Aborted — no changes made.`). In a non-interactive shell it applies + nothing, prints + `⚠ Confirmation required. Re-run with --yes to apply, or use "os migrate plan" to preview.` + and still exits `0`: read the output, not the exit code. The walk itself ran + `os migrate apply --allow-destructive --yes` after that check, which dropped + the three columns; the next boot logged no drift, and the reaction read back + from `sys_comment_reaction`. *Measured in the walk.* + +**A development database (`os dev`).** Measured on a 17.7.0 app whose database +17.7.0's `os dev` created: `os serve --dev` against that database file was +refused with the ceremony named in its `--dev` form +(`Run 'os migrate organization-ownership --dev --out PLAN' …`), and `os dev` +18.0.0 upgraded it in place — `✓ Upgraded the development database to the v18 +organization ownership`, with a copy kept under `.objectstack/data/backups/` — +then served it. ⚠️ **Run `os build` first:** while `dist/objectstack.json` was +still the artifact 17.7.0's `os start` had compiled, the same `os dev` refused +its plan with the protocol-`^17` refusal and changed nothing +(`✗ The upgrade plan was refused, and nothing was changed.`). *Measured in the +walk.* + +#### 3. Edit what 18.0.0 refuses + +Grouped by what you edit. Each line names the change in the 18.0.0 CHANGELOG +(grep its hash) and what 18.0.0 answers when the old shape is still there. Most +are refused at author time, so `os validate` finds them before a deploy: a +schema refusal throws `STACK_SCHEMA_INVALID` (422) from `defineStack`, a +cross-reference one `STACK_CROSS_REFERENCE_INVALID` (422), and an author-time +rule fails `os validate` with exit `1`. The metadata save door (Studio, +`/api/v1/meta`, MCP) refuses the same shapes on a save: in the walk, +`PUT /api/v1/meta/page/NAME` answered `422 INVALID_METADATA` for an undeclared +`page:header` prop and for `kind: 'jsx'`. + +##### Flows: one expression dialect per slot + +```text +✗ flows.0.nodes.1.config.outputVariable: This `get_record` node's config is refused at `outputVariable` by the get_record contract: `$deal` is a `$` name, and the `$` names are reserved for the flow engine's own variables … +• flow 'pp_f1' · node 'u' (update_record) update_record field value at config.fields.owner: A value slot no longer reads the `{…}` template dialect: a string here is the literal text it spells, so a `{…}` token in it is refused rather than stored with its braces. … +• flow 'pp_f5' · node 'u' (notify) notify title at config.title: A flow text slot reads `{{ }}` template holes (ADR-0032 §3), not the single-brace `{…}` dialect … +• flow 'pp_f7' · node 'u' (update_record) update_record field value at config.fields.owner: `user` is not bound in a flow expression, so the run fails with `Unknown variable: user` … +``` + +- **Value slots take a CEL value envelope.** The `fields` of `create_record` / + `update_record`, `assignment` values, each operand of a CRUD node's `filter`, + a `notify` node's `recipients`, `sourceId` and `templateData` values, a + `loop` (with a `body`) or `map` `collection`, a `subflow` / `map` `input`, a + `script` node's `inputs`, a `screen` node's `defaults`, `defaultValue` and + `recordId`, and an `http` node's `body`. `'{record.owner}'` → + `{ dialect: 'cel', source: 'record.owner' }`; `'{$User.Id}'` → + `current_user.id` (guard it with `current_user != null` in a flow that can + run without a user); `'{TODAY()}'` → `isoDate(today())`, `'{TODAY() + 7}'` → + `isoDate(daysFromNow(7))`, `'{NOW()}'` → `isoDatetime(now())`. CEL fails the + run on an absent variable where the template wrote nothing: guard one that may + be absent, `has(vars.x) ? vars.x : null`. A query placeholder in a CRUD filter + (`'{current_user_id}'`, `'{today}'`) stays as it is. The refusal names the CEL + spelling of each token (`0e9e7b7`, `6a06e3b`, `d85615d`, `615c696`, + `111ffff`, `9fdb127`, `f66fdc7`, `12b9daf`, `968acc8`). *Probed on 18.0.0 + (`os validate`) at each of those positions but `sourceId`, `templateData`, + `defaults` and `recordId`.* +- **Text slots take `{{ }}` holes.** A `notify` node's `title`, `message` and + `actionUrl`, a `screen` node's `title` and `description`, a refusing `end` + node's `message`, an `http` node's `url` and `headers` values, and the + translated screen heading `flows.FLOW.screens.NODE.title`: `{x}` → `{{ x }}`. + Compute anything else into a variable first, and a run-user value too — + `{{ $User.Id }}` is refused (`2b61f2d`, `3073b72`, `968acc8`, `d973c34`). + *Probed on 18.0.0 (`os validate`) on `notify` `title`, `message` and + `actionUrl`, `screen` `title` and `http` `url`.* +- **CEL reads only the roots the flow binds.** `user.id`, `ctx.user.id`, + `os.user.id` → `current_user.id`. `record` is bound only on a flow an + entrance hands a record (a record trigger, a `type: 'flow'` action, an inbound + hook, …): elsewhere read the variable by its name or as `vars.NAME`. A + condition is CEL: `'{amount} > 100'` → `'amount > 100'` — the old spelling is + refused as `invalid CEL predicate` (`5fb1746`, `0f77ff5`, `243dd3c`, + `7958bdf`). *Probed on 18.0.0 (`os validate`).* +- **No `$` names of your own.** An `outputVariable`, an `errorVariable` other + than the default `$error`, an `iteratorVariable` / `indexVariable`, an + `idVariable`, a screen field's `name` and a declared variable's `name` drop + the `$`, and every read of them follows (`3e72f93`, `9f5eca5`). *Probed on + 18.0.0 (`defineStack`) for `outputVariable`, `errorVariable` and + `iteratorVariable`.* +- **Node config is held to its executor's contract.** An undeclared key, or a + value of the wrong type, is refused at its location: `fieldValues` → `fields`; + `bulk` / `all` / `multiple` → `multi: true`; a screen field's `visibleIf` → + `visibleWhen`; a loop's `itemVariable` → `iteratorVariable`; delete an `http` + node's `outputVariable`; a `subflow` `config.timeoutMs` moves onto the node; + `limit: '10'` → `limit: 10`; a `{token}` in a literal key (`limit: '{x}'`) → + the literal (`2f70c22`, `78f841b`, `ac9f8bd`, `ca0bdbc`). *Probed on 18.0.0 + (`defineStack`), each.* +- **The retired aliases are refused with their rename.** A CRUD node's + `config.object` → `objectName` and `config.filters` → `filter`; a `notify` + node's `to` / `subject` / `body` / `url` → `recipients` / `title` / `message` + / `actionUrl`, and `source: { object, id }` → `sourceObject` / `sourceId`; + `map` / `subflow` `flow` → `flowName`; `script` `functionName` / `input` → + `function` / `inputs`; node types `http_request`, `http_call` and `webhook` → + `http`; a `wait` node's loose `config` keys → `waitEventConfig` + (`duration` → `timerDuration`, `signal` → `signalName`, `timeoutMs` → + `timerDuration` as a string) and a `connector_action` node's → + `connectorConfig`. `os migrate meta --from 16 --write` writes the ones it can + prove (`5fd2c76`, `f901746`, `d176b0a`). *Probed on 18.0.0 (`defineStack`), + each but the nested `source` pair.* +- **An edge names two nodes of its graph, once.** An edge whose `source` or + `target` is no node, or that repeats an earlier edge, is refused: delete it or + re-point it (`db4c45b`). *Probed on 18.0.0 (`defineStack`).* +- **A retry block states its retries.** A job `retryPolicy` with no + `maxRetries`, or `0`, is refused: write `maxRetries: 3` (what such a block ran + with before 17), or delete the block for a single attempt; + `backoffMultiplier` defaults to `1`, so state `2` to keep the old curve. A + retry policy refuses a key it does not declare (`retry.maxRetry`) (`f606ba2`, + `ac8f2c5`). *Probed on 18.0.0 (`defineStack`).* + +##### Pages, views and actions + +```text +• page "pp_a14" · page:header: `bogusProp` is not a prop `page:header` declares (ComponentPropsMap, @objectstack/spec/ui): Unrecognized key(s) on this `page:header`: `bogusProp`. … +• page "pp_a3" · element:record_picker: dataSource.object: `element:record_picker` reads its records from the node-level `dataSource` binding only, and this node names no object there … +✗ pages.0.kind: Page `kind: 'jsx'` was removed in @objectstack/spec 18 (ADR-0087 D2) … Use `kind: 'html'`. … +✗ Object 'pp_deal' has two default record pages in `pages`: 'pp_r1' (stack.pages[0]) and 'pp_r2' (stack.pages[1]). Set `isDefault: false` on all but one of them. … +``` + +- **A page component's props are errors, not advisories.** A prop its + `ComponentPropsMap` row does not declare, a value the row refuses, a retired + prop, or a missing data binding fails `os validate`, `os build` and `os lint`, + and the metadata save door refuses the page (`4f68ba1`, `32fa194`, + `d0c09e4`). The `component-props-*` advisories you left from 17.7.0 now stop + the build. *Probed on 18.0.0 (`os validate`).* +- **Elements bind data through `dataSource`.** The flat `object` / `filter` / + `sort` / `limit` of `element:record_picker`, `element:number` and + `element:repeater` move to the node-level `dataSource`, a sibling of `type`; + `object-grid` `defaultFilters` → `filter` where `filter` is empty, deleted + where it is not (`317cddd`). *Probed on 18.0.0 (`os validate`).* +- **Renamed and retired page keys.** `kind: 'jsx'` → `'html'` (`c5dd7fc`); a + page component's `visibility` and a form field's `visibleOn` → `visibleWhen` + (`105902f`); `page:header` `description` → `subtitle` (`8693110`); delete + `page:header` `showStar` (`ae0ca66`); delete a `marketplace:installed-list` + component (`14a8bd2`). *Probed on 18.0.0.* +- **Page layout refusals.** A `main` region takes only `width: 'full'` — delete + `width` to keep the page as it rendered (`3a734e0`); a record page that + authors both `slots.details` and `slots.tabs` is refused — move + `record:details` into the first tab's `children` (`e223152`); at most one + `isDefault: true` record page per object (`5f37d36`). *Probed on 18.0.0 + (`defineStack`).* +- **List views.** A `type: 'chart'` view binds `chart: { dataset, values }` + (`20e7d52`); delete `filterableFields` — `userFilters.fields` declares a quick + filter bar (`4bc3bec`); `data: { provider: 'api' }` is retired — bind the view + to its object or static rows, and move an endpoint read onto a page + component's `dataSource` (`1912af4`, `05ab9ef`). *Probed on 18.0.0 + (`defineStack`).* +- **List views are editable in place by default.** A `userActions` block that + leaves `editInline` out now parses to `editInline: true`: write + `userActions: { editInline: false }` on a list that must stay read-only in + place — a log, an audit trail (`f53f14b`). *Probed on 18.0.0 + (`ListViewSchema`).* +- **A url target opens an address or an in-app path.** A `type: 'url'` action's + `target`, a page action row's url target, a dashboard header action's + `actionUrl` and `onSuccess.navigate` take `http://`, `https://`, `mailto:` or + a path starting `/` or `./`; `javascript:`, `data:` and every other scheme are + refused (`9b37edb`, `f931e76`). *Probed on 18.0.0 (`defineStack`).* + +##### Objects, fields, formulas and validation rules + +- **Retired object and index keys.** Delete `externalSharingModel` (`301eec6`). + A declared index's `unique: true` → `unique: 'global'` (the same index, nothing + on disk changes) or `unique: 'organization'` (`ef1fcb2`). *Probed on 18.0.0 + (`defineStack`).* +- **A `controlled_by_parent` object has exactly one required `master_detail`.** + Anything else is refused, as an object or as an `objectExtensions` entry; if + the rows must outlive their parent, give the object its own `sharingModel` + (`76815b9`, `4e0838d`, `ace0a53`). *Probed on 18.0.0 (`defineStack`).* +- **A stage field's options declare their lifecycle.** Every option of the field + an object names as `stageField` carries `kind: 'open' | 'won' | 'lost'` + (`adad73f`). *Probed on 18.0.0 (`defineStack`).* +- **Formulas and validation rules read one hop, and only declared fields.** A + second relationship hop (`record.acct.parent.name`) is refused — store the + value one hop away; an unknown field is refused in every spelling + (`record['typo']`, `record.?typo`), and through a reference a field the + target does not declare (`d537a31`, `94379f4`, `45bec43`, `913fd6d`). *Probed + on 18.0.0 (`os validate`).* +- **A select option's `visibleWhen` reads what the server binds:** `record`, + `previous` and the acting user (`current_user`, and its aliases `user`, `ctx`, + `os`). `parent`, `ctx.locale`, `os.env` and `current_user.email` are + refused, and a gate the server + cannot evaluate refuses the write instead of admitting the value (`bb4f5cc`, + `b1f7a7a`, `46692c1`, `440bed6`). *Probed on 18.0.0 (`os validate`).* +- **A relationship does not target a stored-metadata table** (`sys_metadata` and + its siblings): store the item's type and name instead (`3983965`). *Probed on + 18.0.0 (`os validate`).* +- **One name, one entry.** Two entries with one name in one of 27 stack + collections, and two `functions` with one `name`, are refused, naming both + (`1959aac`, `147a0d1`). `defineSeed()` refuses a record key that is no column + of the object (`9ad3d24`). *Probed on 18.0.0 for `objects`, `functions` and + `defineSeed`.* + +##### Positions, permission sets and sharing rules + +- **A position names its permission sets** in `permissionSets` on its + definition. A boot-time binder that wrote `sys_position_permission_set` rows + has nothing left to write to: the four catalog row objects are retired + (`bfc15d2`, `66949b7`). *Probed on 18.0.0 (`defineStack` accepts + `permissionSets`); see the data line below for the rows.* +- **Permission-set keys.** Delete `managedBy` (`bbdfc05`) and + `adminScope.manageBindings` / `adminScope.authorEnvironmentSets` (`be48c5e`); a + body `packageId` that disagrees with the package shipping the set is refused — + drop it (`eeb95f7`); a field-permission key with no object part (`'.name'`) is + refused — write `'OBJECT.FIELD'` (`30b0fc2`). *Probed on 18.0.0.* +- **One holder per name.** A package that registers a position, permission-set + or capability name another package, the environment catalog or a built-in + holds is refused `422 NAMESPACE_CONFLICT`, naming both (`fe98cc6`, `e030d43`). + *Read off the 18.0.0 tarball (`@objectstack/objectql`).* +- **Sharing rules.** `accessLevel: 'full'` → `'edit'` (`5fd2c76`). A declared + rule (`defineStack({ sharingRules })`) is read from the registry and is + read-only: `sharing_rule` is no longer written through `/api/v1/meta`, and no + stored rule may take a declared name — clone it under a new name to customize + it, and switch the declared one off through the activation ledger (`1e8b628`, + `964a237`, `5e97907`). *`'full'` probed on 18.0.0; the rest read off the + tarball (`@objectstack/plugin-sharing`).* +- **A package manifest's `permissions` is the structured block** + `{ services, hooks, network, fs }`; a flat list of strings is refused + (`fec87e7`). *Probed on 18.0.0 (`defineStack`).* + +##### Datasources, email templates, webhooks and inbound hooks + +- **`driver: 'mongo'` → `'mongodb'`,** and `OS_DATABASE_DRIVER=mongo` no longer + selects a driver: `os serve` exits `1` with + `✗ Unsupported driver "mongo" (OS_DATABASE_DRIVER / --database-driver).` + (`bc0538a`). *Probed on 18.0.0 (`defineStack`, and `os serve` in the walk's + app).* +- **Email templates.** Delete `category` and `isSystem` (`ec7f7f8`). Templates + resolve from the registry: declare them in code — an organization can no + longer create or edit a `sys_email_template` row, and the built-in seed is + gone (`7de0265`, `8c5aa50`). *`category` probed on 18.0.0; the row refusal + (`an organization cannot … an email template row`) read off the 18.0.0 + tarball (`@objectstack/plugin-email`). The ceremony deleted the 24 seeded + template rows in the walk.* +- **Declared webhooks are read from the registry.** No `sys_webhook` row is + written for one, a `sys_webhook` row cannot take a declared name + (`409 RESOURCE_CONFLICT`), and one is switched on and off through the + activation ledger (`191e043`, `fc34d00`). *Read off the 18.0.0 tarball + (`DeclaredWebhookNameConflictError`, `@objectstack/plugin-webhooks`).* +- **An inbound `api` hook verifies only the timestamped signature.** Send + `x-objectstack-signature: t=UNIX_SECONDS,v1=HEX`, the HMAC-SHA256 of + `UNIX_SECONDS.RAW_BODY` under the flow's secret, within 300 seconds of the + server clock; the body-only `sha256=…` form is refused (`5ce6773`). *Read off + the 18.0.0 tarball (`signature-body-only`, `@objectstack/trigger-api`).* + +##### Hooks and code against the published packages + +- **`objectstack build` refuses some hook bodies it used to lower:** one that + reaches `ctx.dispatch.scope` or `ctx.submitted` — do the work at + `ctx.dispatch.index === 0` and read `ctx.previous` — and a parameter list it + cannot rebind to the sandbox's `ctx` (`1eff322`, `6d36017`). *Read off the + 18.0.0 tarball (`@objectstack/cli`, `utils/extract-hook-body.js`).* +- **An in-process engine call carries a context:** the caller's, or + `{ isSystem: true }`. A context with no principal that is not a system context + is refused (`a3bcbcf`). *Read off the 18.0.0 tarball + (`@objectstack/plugin-security`).* +- **Removed exports** — each is a `TS2305` after the upgrade: delete the import. + `normalizeVisibleWhen`, `VISIBILITY_ALIAS_KEYS` and `VISIBILITY_STRICT_OPTIONS` + (read `visibleWhen`; `2c702fd`, `ef1fcb2`); `CONVERSION_CONFLICT_CODE` and the + conversion-conflict channel (`a65a59b`); `checkPageComponentViewData` + (`a35f7a6`); `RunProvenanceContext` (`746637e`); the zero-reader document + schemas (`326a90a`); `ISecurityService.discardPermissionSetOverlay` (`60bcfe7`). + *Probed on 18.0.0 for the first five names (absent from the package).* +- **Renamed registry key.** A custom metadata type registration's + `allowOrgOverride` → `allowEnvironmentOverlay`, also on `GET /meta/types` + (`e23eccb`). *Read off the 18.0.0 tarball (`@objectstack/spec`).* +- **Hand-built kernels.** `I18nServicePlugin` loses `registerRoutes` / + `basePath` — the runtime dispatcher owns `/api/v1/i18n` (`249130f`); + `SecurityPlugin` refuses a kernel that registers no `sys_user` / `sys_member` + (`c6fe02d`). *Read off the 18.0.0 tarball (`@objectstack/service-i18n`, + `@objectstack/plugin-security`).* +- **`os meta resync` is deleted** (`9e9e6f9`): it answers + `Error: command meta:resync not found`, exit `2`. *Measured in the walk.* +- **Tests on `@objectstack/verify`.** `bootStack` composes what + `objectstack serve` composes from the same configuration, and boots the + production `single` shape (`7098aca`, `97610a5`, `34dba5a`); + `runDashboardWindowProofs` takes no options (`06dbc4e`); + `hooks.run(object, 'delete', …)` resolves with the engine's answer + (`a6f3a13`). *Read off the 18.0.0 tarball (`@objectstack/verify`).* + +##### Clients of the REST API, the SDK and API keys + +Every line here was answered by the walk's 18.0.0 server after the ceremony. + +- **API keys expire.** A key minted with no `expires_at` gets 90 days, and the + response echoes the date; one asked for beyond a year is refused; a key unused + for 90 days answers `403 API_KEY_INACTIVE`; a revoked or expired key answers + `403 API_KEY_REVOKED` / `API_KEY_EXPIRED` where it used to answer `401`. A key + that existed with no expiry keeps working for one year from the first 18.0.0 + boot, which stamps that date on it: rotate those keys within the year. The + windows are the `auth` settings `api_key_default_expiry_days`, + `api_key_max_expiry_days` and `api_key_inactivity_days` (`1d79556`). In the + walk the 17.7.0 key still answered `200`, with `expires_at` stamped one year + after the first 18.0.0 boot; a new key got `expires_at` 90 days out; and a + two-year key was refused: + + ```text + 400 API_KEY_EXPIRY_TOO_LONG: expires_at 2028-10-20T00:00:00.000Z is further away than this deployment allows: an API key lives at most 365 days, so the latest expiry a key signed now may have is 2027-10-20T14:27:27.641Z. … + ``` + + *Measured in the walk; the `403` codes read off the 18.0.0 tarball + (`@objectstack/core`).* +- **Name your cross-origin callers.** With no origin list, the CORS answer + admits no other origin: the walk's GET and preflight from + `https://app.example.com` came back with no `access-control-allow-origin`. + Set `OS_CORS_ORIGIN=https://app.example.com` (comma-separated for several), or + the server plugin's `cors.origins`. A list holding `'*'` while credentials are + on fails the boot: name the origins, or set `OS_CORS_CREDENTIALS=false`. A + loopback origin is admitted on a development boot only. Same-origin requests, + the Console included, need nothing (`f7546a4`). *Measured in the walk.* +- **No anonymous reads of the API description or the translations.** + `GET /api/v1/openapi.json` and `GET /api/v1/i18n/locales` answered + `401 UNAUTHENTICATED` with no session (`166a94f`, `b53b949`). *Measured in + the walk.* +- **Request bodies are read as sent.** A JSON body sent as `text/plain` answered + `400 INVALID_REQUEST` (`The request body was sent as text/plain, which this server does not read into a request body.`); + a `null` or non-record body on a `/data` create answered + `400 VALIDATION_FAILED`, as did a `PATCH` whose `expectedVersion` was the + number `3` — send the version you read as a string (`76d9dca`, `75ced06`, + `7c6b7f4`, `cac0be8`, `1f3ac4f`). *Measured in the walk.* +- **The catalog row objects are gone.** `GET /api/v1/data/sys_permission_set`, + `sys_position`, `sys_position_permission_set` and `sys_capability` answered + `404`; read positions and permission sets through + `/api/v1/meta/position` and `/api/v1/meta/permission`. A grant row in + `sys_user_permission_set` names its set in `permission_set` (the set's name), + and carries no `permission_set_id` (`66949b7`, `6f21216`, `0253192`). + *Measured in the walk.* +- **Upload scopes are a closed list:** `user`, `tenant`, `private`, `temp`, + `attachments`. `scope: 'public'` answered `400 INVALID_REQUEST` (`scope 'public' is not accepted: a storage scope never made a file publicly readable.`): + a file is served without sign-in only when its file record carries + `acl: 'public_read'`. The storage settings' `max_upload_mb` is enforced + (`413 PAYLOAD_TOO_LARGE`) (`6a3fe25`, `ee8751d`, `59d993c`). *Scope measured + in the walk; `PAYLOAD_TOO_LARGE` read off the 18.0.0 tarball + (`@objectstack/service-storage`).* +- **Emoji reactions are records.** `sys_comment.reactions` is retired: read and + write `sys_comment_reaction` (`9963322`). *Measured in the walk.* +- **A public form's submit answers `{ id }` only,** not the stored record + (`7806a14`). An import's `runAutomations: false` and + `treatAsHistorical: true` are honoured only for the system principal and + refused `403 PERMISSION_DENIED` to any other caller (`741aa85`). A search over + an object with no searchable field is refused `400 INVALID_FIELD` instead of + answering every row (`ec749fb`). *Read off the 18.0.0 tarball + (`@objectstack/rest`, `@objectstack/objectql`).* + +#### 4. What 18.0.0 does not carry yet, and what it carries unlisted + +The v18 line keeps landing on `main`, and the version commit did not take +everything that landed before it. Read on `main` at `71696121a6`: 26 pending +changesets, ten of them `minor`, so the release that publishes them is +**18.1.0**. They fall in two sets. + +**Shipped in 18.0.0, listed in no 18.0.0 CHANGELOG.** Eight of the 26 belong to +commits that landed before the 18.0.0 version commit (`06cab4849a`), so their +code is in the 18.0.0 packages while their entries will first appear in +18.1.0's CHANGELOG. Two of them reach an upgrade: + +- **A production `os serve` boot with no auth secret is refused** (18.x step 7, + part 7b-3, #24845; it also stops an organization-less row drawing its + autonumber from the `__global__` counter). Measured on 18.0.0 — `os serve` + exited `1` before it listened: + + ```text + ✗ This stack enables auth, but no auth secret is set, so AuthPlugin was not composed and no caller can authenticate — anonymous access to object data is always denied, so the data API would be unusable, and with no organization every row this boot writes would have no owner. + ``` + + Set `OS_AUTH_SECRET` to a long random value, identical across restarts and + nodes (the walk set the older `AUTH_SECRET`, which 18.0.0 still reads); `os dev` + uses a development fallback. `os start` is not refused: with no secret set it + wrote `HOME_DIR/auth-secret` (mode `0600`) on its first run and served + sign-up. *Measured on 18.0.0.* +- **A declared sharing rule served under `single` carries + `deploymentWide: true`** where it carried `organization_id: null`, and the + organization-less readers of sharing rules, approval units and the seed + loader retire (18.x step 7, part 7a-3, #24828). *Read off the 18.0.0 tarball + (`deploymentWide`, `@objectstack/plugin-sharing`).* + +The other six are fixes with no edit for an upgrader, among them +`invite-member`'s refusal of a membership grade outside its vocabulary with +`code: 'ROLE_NOT_FOUND'` +(#24846, in the 18.0.0 `@objectstack/plugin-auth`). + +**Not in 18.0.0.** Eighteen changesets belong to commits after the version +commit, and only 18.1.0 carries them. Six are `minor`: + +- **18.x step 7, part 7a-2** (#24872, merged after the cut): a grant applies only + in its own organization — an organization-less grant row confers nothing, and + the Default Organization's `admin_full_access` grant is the only + platform-admin anchor; +- `os migrate apply` on an empty database writes the organization-ownership + birth marker (#24875); +- `POST /api/v1/packages/:id/adopt-orphans` refuses a read-only package with + `422 WRITABLE_PACKAGE_REQUIRED` (#24861; absent from the 18.0.0 + `@objectstack/runtime`); +- on PostgreSQL, `introspectSchema` reads each table in its own schema + (#24920); `os lint` reports a retired `filterableFields` by name (#24878); + and a team record no longer lists Sessions (#24881). + +The rest are fixes: masked password inputs in the identity action dialogs, an +impersonation session that a target's forced password reset no longer holds, +`server.trustProxy` read for the vendored auth library's client address, and +others. + +**The walled postures on 18.0.0, measured.** `os serve` 18.0.0 boots `group` +and `isolated` on a fresh database (the `18.0.0-next.5` refusal of a walled +posture is gone). Each prerequisite it names is refused at boot, in this order, +until it is met: + +```text +✖ FATAL: tenancy posture 'group' was requested but @objectstack/organizations could not be loaded, so the organization wall is INACTIVE. Refusing to boot … +✗ [auth] tenancy posture 'group' requires OS_PLATFORM_OWNER_EMAIL to be set. … +✗ ✖ FATAL: tenancy posture 'group' is walled, but this deployment never declared a membership policy. … +``` + +With `@objectstack/organizations@18.0.0` declared in the app, +`OS_PLATFORM_OWNER_EMAIL` set, a membership policy declared +(`OS_AUTH_MEMBERSHIP_POLICY=invite-only`) and a stable `OS_SECRET_KEY`, both +postures printed `✓ Server is ready` with `Tenancy: group` / +`Tenancy: isolated`. *Measured on a fresh database.* + +**A walled 17.x database upgrades through the same ceremony.** Measured on a +17.7.0 app booted under `group` with `@objectstack/organizations@17.7.0` (its +owner signed up and wrote two records): after the move to 18.0.0 — the exact +`"@objectstack/organizations": "17.7.0"` pin moved by hand, like every +`@objectstack/*` pin — `os migrate security-catalog-overlays` exited `0`, the +boot was refused as in step 2, the plan read `posture group` (81 tables, +`unattributableRows: 0`), the apply completed, and 18.0.0 served the database +under `Tenancy: group` with both records in their organization. *Measured in +the walk.* An `isolated` database was not walked. + +#### 5. The npm tags + +`latest` is `18.0.0`; `next` still names `18.0.0-next.5`, which is older — see +[Release facts](#release-facts-as-measured). Pin `18.0.0` or `^18.0.0`, and ⛔ +never `@next`. *Measured with `npm view`.* diff --git a/content/docs/releases/v18/index.mdx b/content/docs/releases/v18/index.mdx index d5979172723..661bfe61026 100644 --- a/content/docs/releases/v18/index.mdx +++ b/content/docs/releases/v18/index.mdx @@ -43,12 +43,17 @@ ceremony**, `os migrate organization-ownership` (ADR-0131 D10). (`os dev`, `os serve --dev`), for example a local one with no `OS_AUTH_SECRET`, both ceremony commands take `--dev`, as the refusal `os serve --dev` prints names them. `os dev` runs the ceremony itself for a - local SQLite development database. + local SQLite development database, once `os build` has replaced the artifact + 17.x compiled: with that artifact in `dist/`, its in-place upgrade refused its + plan and changed nothing. - **Walled postures boot.** `OS_TENANCY_POSTURE=group` and `isolated` (and `OS_MULTI_ORG_ENABLED=true`) boot on a fresh install and on a database the ceremony upgraded, under the same prerequisites as 17.x: the enterprise `@objectstack/organizations` runtime and `OS_PLATFORM_OWNER_EMAIL`. The - 18.0 pre-releases refused them; 18.0.0 does not. + 18.0 pre-releases refused them; 18.0.0 does not. 18.0.0 also refuses a walled + boot until a membership policy is declared + (`OS_AUTH_MEMBERSHIP_POLICY=invite-only`, or `auto` chosen knowingly): + `✖ FATAL: tenancy posture 'group' is walled, but this deployment never declared a membership policy.` - **A production `os serve` needs an auth secret.** A production `os serve` whose stack enables auth and which resolves no `OS_AUTH_SECRET` is refused before any plugin starts: with no AuthPlugin, no caller can @@ -58,7 +63,9 @@ ceremony**, `os migrate organization-ownership` (ADR-0131 D10). generates an auth secret on its first run and persists it to `/auth-secret`. -The step-by-step is [Upgrading to 18.0](/docs/upgrading#upgrading-to-180), and +The step-by-step is [Upgrading to 18.0](/docs/upgrading#upgrading-to-180), each +step's measured output is in the walked +[18.0.0 upgrade checklist](/docs/releases/v18/18-0#2-upgrade-the-database), and every flag is in [`os migrate organization-ownership`](/docs/deployment/cli#os-migrate-organization-ownership). @@ -96,13 +103,25 @@ every flag is in npx skills remove objectstack-ai objectstack-api objectstack-automation \ objectstack-data objectstack-formula objectstack-i18n objectstack-platform \ objectstack-pm-dispatch objectstack-query objectstack-ui objectstack-upgrade -y - npm install --save-dev "@objectstack/skills@$(node -p "require('@objectstack/spec/package.json').version")" + pnpm add -D "@objectstack/skills@$(node -p "require('@objectstack/spec/package.json').version")" npx skills@1.7.2 experimental_sync --agent claude-code --copy -y ``` - `npx skills remove --all` replaces the first command when these are the only - skills the project has. + Add the package with the manager that installed the project: in a pnpm + project, `npm install --save-dev` fails (`npm error Cannot read properties of + null (reading 'edgesOut')`, measured on the [18.0.0 upgrade + walk](/docs/releases/v18/18-0#1-move-the-pins)). `npx skills remove --all` + replaces the first command when these are the only skills the project has: + run `npx skills list` first, because `--all` removes every installed skill + without asking. - **18.0.0 ends the prerelease.** It is the GA cut: it leaves pre mode and - publishes the line to `latest`. Curated notes for the line's releases are - compiled at release time and are not written yet; until then, the changesets - are the record. + publishes the line to `latest`. + +## Per-release notes + +Each release below is a self-contained page: what it changed, what breaks, and +its own upgrade checklist. + +- **[18.0.0](/docs/releases/v18/18-0)** — current; the GA cut. Its + [upgrade checklist](/docs/releases/v18/18-0#upgrade-checklist) moves a 17.x + app and its database to 18.0.0, and was walked from 17.7.0. diff --git a/content/docs/releases/v18/meta.json b/content/docs/releases/v18/meta.json index cb104bd0f03..2dd5e729de1 100644 --- a/content/docs/releases/v18/meta.json +++ b/content/docs/releases/v18/meta.json @@ -1,6 +1,7 @@ { "title": "v18", "pages": [ - "index" + "index", + "18-0" ] } diff --git a/content/docs/upgrading.mdx b/content/docs/upgrading.mdx index b6dd9319706..e858d1052df 100644 --- a/content/docs/upgrading.mdx +++ b/content/docs/upgrading.mdx @@ -114,7 +114,9 @@ command for this move; it is four steps you run yourself. The list names every skill the catalog carried during 17.x, and a name that is not installed is skipped; `npx skills remove --all` does the same when - these are the only skills the project has. + these are the only skills the project has. Run `npx skills list` first: + `--all` removes every installed skill without asking, so use it only when + the list shows nothing but these. Then run [the loop](#the-loop) below — `os validate` before `os build` — because moving the pins can move the gate. @@ -350,16 +352,35 @@ command. **The sequence:** -1. Start 18.0 against the database once (`os serve` / `os start`). The boot is +1. Before the first 18.0 boot, run `os migrate security-catalog-overlays`. It + lists the stored permission-set and position rows whose name a package now + holds, which an 18.0 cold boot refuses with `422 NAMESPACE_CONFLICT`; it exits + `1` when it lists one, and `--apply` deletes them. +2. Start 18.0 against the database once (`os serve` / `os start`). The boot is refused, but its schema sync has already created the tables 18.0 adds, which the ceremony moves rows into. -2. Take a backup. The platform takes none. -3. Write the plan: `os migrate organization-ownership --out plan.json`. It reads +3. Take a backup. The platform takes none. +4. Write the plan: `os migrate organization-ownership --out plan.json`. It reads only. -4. Review the plan. -5. Apply exactly that plan: +5. Review the plan. +6. Apply exactly that plan: `os migrate organization-ownership --apply --plan-file plan.json --backup-taken`. -6. Start the server again. +7. Start the server again. +8. Clear the orphaned columns the first served boot still reports, and only + those. Preview the reactions with `os migrate comment-reactions`, then carry + them with `os migrate comment-reactions --apply --yes`. Then run + `os migrate plan` and check that its destructive entries are only the + columns the ceremony left behind (in the walk: `sys_comment.reactions`, + `sys_sso_provider.created_by`, `sys_sso_provider.updated_by`). Any other + destructive entry is your app's own and is decided separately, not in this + step: `os migrate apply --allow-destructive` applies every destructive change + in the plan. Then run `os migrate apply --allow-destructive` in a terminal and + answer its `[y/N]` prompt after reading the changes it lists. In a + non-interactive shell it applies nothing, prints + `⚠ Confirmation required. Re-run with --yes to apply …` and still exits `0`. + +Each step's measured output, from a walk of a 17.7.0 app to 18.0.0, is in +[the 18.0.0 upgrade checklist, step 2](/docs/releases/v18/18-0#2-upgrade-the-database). ```bash os migrate organization-ownership --out plan.json @@ -371,12 +392,16 @@ with `--dev` (`os dev`, `os serve --dev`), for example a local one with no `OS_AUTH_SECRET`, composes as a development boot, and the ceremony has to boot the same way to see the same database. The refusal `os serve --dev` prints names the command with `--dev`. `os dev` runs the whole sequence itself for a local -SQLite development database. +SQLite development database. Run `os build` before it: while +`dist/objectstack.json` was still the artifact 17.x compiled, `os dev`'s +in-place upgrade refused its plan and changed nothing. **Walled postures upgrade too.** `OS_TENANCY_POSTURE=group` and `isolated` boot in 18.0 on a fresh install and on a database the ceremony upgraded. On a walled database, a customized copy of a declared webhook or sharing rule blocks its -step and names the row. +step and names the row. A walled boot also needs a declared membership policy, +`OS_AUTH_MEMBERSHIP_POLICY=invite-only` (or `auto`, chosen knowingly); without +one it is refused: `✖ FATAL: tenancy posture 'group' is walled, but this deployment never declared a membership policy.` **A production `os serve` deployment needs an auth secret.** A production `os serve` whose stack enables auth and which resolves no `OS_AUTH_SECRET` is @@ -404,8 +429,11 @@ have no other channel to reach you than the page below. | Release | Where its upgrade notes live | | :--- | :--- | -| v18.0.0 | [Upgrading to 18.0](#upgrading-to-180) (the ownership ceremony) · [v18](/docs/releases/v18) | -| v17.4.0 | [17.4.0](/docs/releases/v17/17-4) — ⛔ checklist not written; machine-draft notes only | +| v18.0.0 | [Upgrade checklist — 18.0.0](/docs/releases/v18/18-0#upgrade-checklist) · [Upgrading to 18.0](#upgrading-to-180) (the ownership ceremony) · [v18](/docs/releases/v18) | +| v17.7.0 | [Upgrade checklist — 17.7.0](/docs/releases/v17/17-7#upgrade-checklist) | +| v17.6.0 | [Upgrade checklist — 17.6.0](/docs/releases/v17/17-6#upgrade-checklist) | +| v17.5.0 | [Upgrade checklist — 17.5.0](/docs/releases/v17/17-5#upgrade-checklist) | +| v17.4.0 | [Upgrade checklist — 17.4.0](/docs/releases/v17/17-4#upgrade-checklist) | | v17.3.0 | [Upgrade checklist — 17.3.0](/docs/releases/v17/17-3#upgrade-checklist) | | v17.2.0 | [Upgrade checklist — 17.2.0](/docs/releases/v17/17-2#upgrade-checklist) | | v17.1.0 | [Upgrade checklist — 17.1.0](/docs/releases/v17/17-1#upgrade-checklist) | @@ -417,7 +445,7 @@ have no other channel to reach you than the page below. | v12.0.0 | [Upgrade checklist](/docs/releases/v12#upgrade-checklist) | | v9.0.0 | [Upgrade checklist](/docs/releases/v9#upgrade-checklist) | -Only the v17 line is broken out per release. Each of its three minors shipped +The v17 and v18 lines are broken out per release. Each v17 minor shipped breaking changes under the repo's launch-window convention — which ships a breaking change as `minor` rather than `major`, so the version number is not the migration signal and the changelog entry is. Earlier majors' release pages carry diff --git a/scripts/docs-audit/handwritten-docs.json b/scripts/docs-audit/handwritten-docs.json index 8a8be41dd6c..12c30fc41b3 100644 --- a/scripts/docs-audit/handwritten-docs.json +++ b/scripts/docs-audit/handwritten-docs.json @@ -205,6 +205,7 @@ "content/docs/releases/v17/17-6.mdx", "content/docs/releases/v17/17-7.mdx", "content/docs/releases/v17/index.mdx", + "content/docs/releases/v18/18-0.mdx", "content/docs/releases/v18/index.mdx", "content/docs/releases/v9.mdx", "content/docs/ui/actions.mdx",