Skip to content

Commit 40eaae7

Browse files
feat(cloud-connection)!: refuse a managed upgrade that renames or retypes a stored field until it is declared or confirmed (#31243)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiWDNwmTd9esHVSgrxXCmHkf)_ Fixes #30884 Clause-②: yes (narrowing) — a managed re-install that succeeds today is refused `409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` on an undeclared suspected rename or a `needs_confirm` / `destructive` type change, and a declared rename is refused `501 NOT_IMPLEMENTED` until #30885; the widening half is the new ledger code and the `confirmSchemaUpgrade` body key / `--confirm-upgrade` flag. Ruling record 6709061297. Metadata-protocol: none Skills-docs: updated here — content/docs/deployment/cli.mdx (`--confirm-upgrade` paragraph beside `--confirm-access-upgrade`), content/docs/data-modeling/fields.mdx (the `renamedFrom` row: such an upgrade is refused 501 until #30885); `skills/**` teaches neither flag nor `renamedFrom`, so no skill change. > **BREAKING** — accept-set narrowing at `POST /api/v1/marketplace/install-local` for a managed re-install. A re-install whose package upgrade plan lists an undeclared suspected rename, or a type change that is not `safe`, is now refused `409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` until the same call carries the finding set's token; one that declares a field rename (`renamedFrom`) is refused `501 NOT_IMPLEMENTED` until the runtime can carry the column (#30885). > > **Migration.** Declare the rename on the new field (`renamedFrom: ['<old key>']`) and rebuild, or confirm the findings as listed: `os package install <artifact> --confirm-upgrade <findingSet>` (`os deploy --confirm-upgrade <findingSet>` against a self-hosted runtime), or `"confirmSchemaUpgrade": "<findingSet>"` in the install-local body. `--dry-run` previews the plan first. > > **ADR-0087 disposition** (the changeset carries the one marker): not-required (no-migration-prescription) — no schema key is added, removed or re-shaped; the refusal judges two package bodies at the install door and prints its own way through. ## 维护者速读 - 改了什么:受管软件包重装(升级)时,在写入任何东西之前先比较已装版本和新版本。没声明的疑似改名、或会改动已存数据列的类型变化(needs_confirm / destructive),返回 409 `SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`,附每条发现和一个确认令牌;带上令牌(`--confirm-upgrade`)重发即安装,并在安装记录里留下谁在何时确认了哪一组发现。声明了 `renamedFrom` 的改名在搬列能力(#30885)落地前回 501。`safe` 类型变化、删字段、种子变化不拦。 - 为什么改:今天改名会静默变成一个新的空列,旧数据还在旧列里但看不到;改类型也是静默的。这是 Q3 「默认拒绝,需显式映射」 的落实,田騏 已对本 PR 裁定 A。 - 风险与回滚:只影响受管包的重装;首次安装、unmanaged 安装、无发现的升级行为不变。误报(真的删一个、加一个无关字段)代价是一次确认。回滚即还原本 PR;ledger 里新增的码是纯加法。 - 先看:`packages/cloud-connection/src/marketplace-install-local-plugin.ts` 的 `schemaUpgradeVerdict`(哪些发现要确认、令牌怎么算)和 `evaluateSchemaUpgradeGate`(501 / 409 / 放行三条路);`packages/spec/src/api/error-code-ledger.zod.ts` 的新码一行。 - 两分钟验证:`pnpm --filter @objectstack/cloud-connection exec vitest run src/marketplace-install-local-upgrade-plan.test.ts`,预期全部通过(step 2c′ 一组,含孤儿安装、记录损坏或缺失、声明改名 501 等情形)。 - 需要你做的:无,席位落地。 ## What changes PR 2 of 2 on #30884 (PR 1 #30983 landed the comparison, the environment half and `dryRun`; handoff 6688284693). Under the ruling record 6709061297 (the maintainer chose 「A 全部同意(推荐)」; C1–C4 all A): - **The ledger** (`packages/spec/src/api/error-code-ledger.zod.ts`): one code, `SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`, under `@objectstack/cloud-connection` (C1), in that array's alphabetical order. No other `packages/spec/src/**` hunk. - **The verdict** (`schemaUpgradeVerdict`, module-level in `marketplace-install-local-plugin.ts`): reads `comparePackageVersions` alone (C3), each side folded by `resolveStackCollection` as `planUpgrade` folds it. Declared renames are listed on their own; a suspected rename always needs confirming; a type change needs confirming when its category is `needs_confirm` or `destructive`, on a declared rename too (scope note 6685166393). The token is `schema-upgrade:` plus 32 hex of a SHA-256 over the findings' identities, sorted, never their sentences or row counts: the same two versions always give the same token, any change to the set gives another. - **The gate** (`evaluateSchemaUpgradeGate`, step 2c′ after the access gate 2c, C4), managed re-install only (`conflict === 'marketplace'`), before any write: - a declared rename ⇒ `501 NOT_IMPLEMENTED`, naming each `object.old → object.new`, token or not (C2); the findings a token would still owe are listed in `details.findings` beside it; - findings ⇒ `409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` with `details.findingSet`, `details.findings` (each with `kind` and a one-line `describe`), `details.confirmWith`; the message names both ways forward (declare `renamedFrom` and rebuild, or confirm with the token); a token from another set gets the stale-token sentence; - the matching token ⇒ installs, and the ledger entry records `schemaUpgradeConfirmation` (`findingSet`, `confirmedBy`, `confirmedAt`); - no finding ⇒ unchanged, `planUpgrade` never asked. - When the `manifest` service answers `planUpgrade`, each refused finding carries `rows` (the installed column's non-null rows) and its `describe` says so (C3); without it, or when the reading throws, the refusal is the same without counts. - **The CLI**: `os package install --confirm-upgrade TOKEN` sends `confirmSchemaUpgrade`, and the 409 prints every finding, the `renamedFrom` way forward and the exact retry. `os deploy --confirm-upgrade TOKEN` passes it through to the self-hosted install, as `--confirm-access-upgrade` is (see Acceptance notes: a file-surface extension). ## Tests Each under `os-verify-lock.sh` with its `VERDICT command-exit` line: - `@objectstack/cloud-connection` `marketplace-install-local-upgrade-plan.test.ts`: 16/16, ten of them the new `step 2c′` describe — the 409 with findings, token, both ways forward and row counts, nothing written; the token installing and the ledger entry's `schemaUpgradeConfirmation`; another finding set refusing the old token; a `needs_confirm` retype refused; a declared rename refused 501 by name with and without a token, nothing written; a declared rename that also retypes `needs_confirm` still listing the type change; a `safe` retype, a removed field and a seed change installing unrefused; a no-finding re-install unchanged (`planUpgrade` never asked); no `planUpgrade` ⇒ the same refusal without `rows`; a first install never compared. **Round 2 (seat r1 6712105906, Fable r2 6714005316)** adds, across `marketplace-install-local-upgrade-plan` + `-corrupt-ledger` + `-conflict` (37/37): an orphan's retype refused 409 against the registry body, with a clean-orphan 200 control; an orphan row without a body refused 409 `installed_record_missing`, its token installing; a corrupt ledger entry refused 409 `installed_record_unreadable` (file, cause), its token installing; a corrupt entry plus a declared `renamedFrom` refused 501 with and without the record's token, nothing written; a `destructive` text→formula retype refused 409; the 501 tests with real tokens; `true` confirms nothing. `marketplace-install-local-conflict.test.ts`'s orphan upgrade is unchanged (200, assertion untouched). - PR 1's control test now upgrades without a finding (V1 → V2 is a suspected rename, so it is refused by this PR — a stale fixture, re-judged, not weakened). - `@objectstack/cli`: `package-install-dry-run.test.ts` gains the `--confirm-upgrade` describe (the flag reaches the body as `confirmSchemaUpgrade`, absent without it; the 409 prints each finding, the `renamedFrom` way and the exact retry, exit 1); `deploy.test.ts` gains the pass-through (retry line names `os deploy --confirm-upgrade`, the token reaches the second POST only) and the Cloud-target refusal. <details><summary>Suite readings</summary> - cloud-connection `pnpm test` on `39a525c236`: Test Files 73 passed (73), Tests 969 passed (969), VERDICT command-exit 0; `typecheck` exit 0. - cli `vitest run --project unit` on `a274e36e`: Test Files 399 passed (399), Tests 5637 passed (5637), VERDICT command-exit 0; `typecheck` VERDICT command-exit 0. No cli file changed after `a274e36e`. - spec: the only hunk since round 1 is a clause in the ledger comment; spec `pnpm test` (round 1): Test Files 821 passed, Tests 25964 passed, 1 todo, VERDICT command-exit 0. - Merges of `origin/main` at `02481a8990`, `d04e0e2c88` and `39a525c236` (the last brings #31284); no rebase, no force push. </details> <details><summary>Ablations (each via scripts/ablation-replace.mjs, anchor hit once, restored to the HEAD blob with git diff HEAD empty)</summary> The door's tests import the plugin from `src/` (relative import), so no build sits between a mutation and the run. | # | Mutation in `marketplace-install-local-plugin.ts` | Result | |---|---|---| | A1 | suspected renames dropped from the verdict | 4 failed / 12 passed | | A2 | the `needs_confirm`/`destructive` filter removed (a `safe` retype refused) | 1 failed / 15 passed (`a safe type change … install unrefused`) | | A3 | any string token accepted | 1 failed / 15 passed (`a different finding set refuses the old token`) | | A4 | the 501 branch disabled | 2 failed / 14 passed (both declared-rename tests) | | A5 | a type change riding on a declared rename skipped (scope note) | 1 failed / 15 passed (`… still owes the type change its confirmation`) | | A6 | `schemaUpgradeConfirmation` not written to the ledger entry | 1 failed / 15 passed (`the same call with the token installs …`) | | A7 | the record-unavailable branch returns no finding | 2 failed | | A8 | the `declaredRenames.length === 0` guard removed | 2 failed (both declared-rename cases) | | A9 | `destructive` dropped from the confirmed set | 1 failed | | A10 | `manifestId` dropped from the token digest | 2 failed | | A11 | `registeredPackageBody` returns `undefined` | 2 failed (conflict orphan upgrade + orphan compare) | | A12 | `declaredRenamesOfIncoming` replaced by `[]` | 1 failed (corrupt entry + rename 501) | Each restored, `git diff HEAD` empty, suites green again. </details> ## Acceptance notes - **File surface, one extension to ratify:** `packages/cli/src/commands/deploy.ts` (+ `deploy.test.ts`) is outside the claim's list. `os deploy` runs `os package install` in-process with `retryCommand = 'os deploy'`, so without it the new refusal would print `os deploy --confirm-upgrade TOKEN`, a flag `os deploy` would reject: the self-hosted dev loop (the epic's target) would have no way through. The change mirrors `--confirm-access-upgrade` exactly. `local-manifest-source.ts` gains the `schemaUpgradeConfirmation` type on `InstalledEntry` (the ledger entry the claim names). - **No installed body asks, never fails open.** When the gate has no installed body to compare (the ledger file will not parse, the entry carries no body, or an orphan's registry row carries none), it answers `409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` with one finding, `installed_record_unreadable` (file, cause) or `installed_record_missing`; that set's token installs and records `schemaUpgradeConfirmation`. - **An orphaned install** (no ledger file, the registry row found by `findConflict` step 3) is compared against the body the registry serves. For a multi-package artifact this reads one row; the JSDoc states the limit. - **A declared rename is 501 on every path**, including the record-unavailable one, where the renames are read from the incoming version alone (`declaredRenamesOfIncoming`); the record finding is listed beside it, with or without a token. - **Reported, not refused**: removed fields and objects, `safe` type changes and seed changes reach the installer through `dryRun: true` (PR 1); a successful install's answer gains nothing. - **Two refusals in a row are possible** (C4's accepted cost): an upgrade can answer the access 409 first, then the schema 409. - **The dry run carries no schema token**: an installer reads the token off the 409, not off the plan. Adding it to the dry run would be a widening of its own. - **The 501 names the way to install now** (drop `renamedFrom`, confirm the suspected rename) because it exists; it says the values stay in the old column. - **Ledger position**: the code sits in `@objectstack/cloud-connection`'s array in alphabetical order (after `RESEED_SKIPPED`); its comment names `ACCESS_UPGRADE_CONFIRMATION_REQUIRED` as its sibling. - **Out of scope, noted**: cloud's hosted install door (sibling card 01c) reads the same 409. ## Verification All on `39a525c236` (after merging `origin/main` incl. #31284 `59d9815a03`): - Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` → 116 families, each exit captured before any pipe; `--ran`: "116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED". All 116 exit 0. `pnpm lint` exit 0. (`check:slot-lookup` was red on `a274e36e` for an `any`-typed `ctx.getService`; typed in `5c6cd1cfd7`, holds since.) - Full build `pnpm turbo run build --filter=!@objectstack/docs`: exit 0. - **Dogfood** (narrowing arm), full battery under the lock: VERDICT command-exit 0 (27m32s) · Test Files 435 passed, 3 skipped (438) · Tests 3610 passed, 41 skipped (3651) · no failure. - PR check-runs on `39a525c236`: 57 success, 8 skipped, none red. - Dev report: #30884 comment 6714956140. --- _Generated by [Claude Code](https://claude.ai/code/session_017ihL2dvnT51fbSebUyE3zW)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- signed: domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9020358 commit 40eaae7

15 files changed

Lines changed: 1028 additions & 16 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/cloud-connection': patch
4+
'@objectstack/cli': patch
5+
---
6+
7+
feat(cloud-connection)!: a managed upgrade that renames or retypes a stored field is refused until it is declared or confirmed
8+
9+
Clause-②: yes (narrowing) — a managed re-install through install-local that installed before is refused `409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` when its package upgrade plan lists an undeclared suspected rename or a `needs_confirm` / `destructive` type change, and `501 NOT_IMPLEMENTED` when it declares a field rename (`renamedFrom`) this runtime cannot carry yet. The widening half: the error-code ledger registers `SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` under `@objectstack/cloud-connection`, install-local takes `confirmSchemaUpgrade`, and `os package install` and `os deploy` take `--confirm-upgrade`.
10+
Metadata-protocol: none
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no export is removed: the refusal judges two package bodies at the install door, so there is nothing for a migration to rewrite and no tombstone. The way through is a token the refusal itself prints. -->
13+
14+
**BREAKING** accept-set narrowing at the install-local door (`POST /api/v1/marketplace/install-local`, `@objectstack/cloud-connection`) for a managed re-install over an installed package.
15+
16+
Before any write, after the access upgrade check, the door compares the installed version (its ledger entry) with the incoming one through `comparePackageVersions` (`@objectstack/metadata-core`), the comparison the dry run and `os diff` read:
17+
18+
- **A suspected rename** (in one object, a stored field removed while a stored field is added, with no `renamedFrom` linking them) or **a type change** the drift categories call `needs_confirm` (both columns stored) or `destructive` (the new type stores no column) answers `409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`. `error.details.findings` names each with a one-line `describe` (and, when the runtime's `manifest` service answers `planUpgrade`, the rows the installed column holds); `error.details.findingSet` is the token. The same request with `"confirmSchemaUpgrade": "<findingSet>"` installs, as the additive schema sync leaves the fields, and the ledger entry records `schemaUpgradeConfirmation` (the token, who, when). A token from another finding set is refused again with the fresh one. A type change riding on a declared rename is judged on its own.
19+
- **The installed side** is the ledger entry's body; an orphaned install (the registry still serves the package, the ledger holds no record) is compared against the body the registry serves. When there is no installed body to compare — the ledger file will not parse, or neither the ledger entry nor the orphan's registry row carries a body — the same `409` names that as its one finding (`installed_record_unreadable` with the file and the cause, or `installed_record_missing`), and its token admits the re-install, which rewrites the record; a rename the incoming version declares still answers the `501` below.
20+
- **A declared rename** (`renamedFrom` on a field of the new version) answers `501 NOT_IMPLEMENTED`, naming each rename: this runtime cannot yet move a renamed field's values to its new key, and no token answers it.
21+
- A `safe` type change (the installed field had no stored column), a removed field or object and a seed dataset change are refused by nothing; `dryRun: true` reports them.
22+
23+
A first install, an `unmanaged` install and a re-install whose plan lists none of these answer as before.
24+
25+
**Migration.** A package author whose upgrade is refused either declares the rename on the new field — `renamedFrom: ['<old key>']` — and rebuilds, or confirms the findings as listed: `os package install <artifact> --confirm-upgrade <findingSet>` (`os deploy --confirm-upgrade <findingSet>` against a self-hosted runtime), or `"confirmSchemaUpgrade": "<findingSet>"` in the install-local body. A host that calls install-local itself reads `error.details.findingSet` off the 409 and sends it back. Preview the refusal first with `os package install <artifact> --dry-run`.

‎content/docs/data-modeling/fields.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ Database indexes are declared at the object level in the object's `indexes[]` ar
478478
479479
| Property | Type | Default | Description |
480480
| :--- | :--- | :--- | :--- |
481-
| `renamedFrom` | `string[]` | — | Earlier keys of this field (snake_case), e.g. `renamedFrom: ['company']` on a field now keyed `account_name`: on a package upgrade from a version that has the old key and not this one, the platform carries the old column's data to this field instead of adding an empty column. Each entry once; never the field's own key, a key another field of the object still declares, or an old key another field also claims — each refused at parse. Read only at package upgrade, and declared ahead of the upgrade step that carries the data: until that step ships, no upgrade carries a column on this declaration |
481+
| `renamedFrom` | `string[]` | — | Earlier keys of this field (snake_case), e.g. `renamedFrom: ['company']` on a field now keyed `account_name`: on a package upgrade from a version that has the old key and not this one, the platform carries the old column's data to this field instead of adding an empty column. Each entry once; never the field's own key, a key another field of the object still declares, or an old key another field also claims — each refused at parse. Read only at package upgrade, and declared ahead of the upgrade step that carries the data: until that step ships, a managed upgrade that declares a rename is refused `501 NOT_IMPLEMENTED`, naming it, so it never installs as a silent empty column |
482482
483483
### Security
484484

‎content/docs/deployment/cli.mdx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3518,6 +3518,7 @@ write `-u URL` instead.
35183518
| `--password <password>` | `OS_RUNTIME_PASSWORD` | Account password on the target runtime |
35193519
| `--confirm-global-uniques` | — | Affirm this app's installation-wide `unique` constraints are genuinely platform-wide — see below |
35203520
| `--confirm-access-upgrade <token>` | — | Confirm an upgrade's access findings with the finding-set token the runtime's refusal printed — see below |
3521+
| `--confirm-upgrade <token>` | — | Confirm an upgrade's schema findings (a suspected rename, a type change that is not safe) with the finding-set token the runtime's refusal printed — see below |
35213522
| `--timeout <ms>` | `OS_CLOUD_TIMEOUT_MS` | HTTP timeout in milliseconds, default `120000`. `0` disables it |
35223523
35233524
**`--confirm-global-uniques` answers a stop; it does not force one past.**
@@ -3549,6 +3550,25 @@ and the upgrade stops again with the new set; the confirmation is recorded in
35493550
the install's ledger entry. Under `group` and `isolated` only the platform
35503551
operator reaches this check. A fresh install is never checked.
35513552

3553+
**`--confirm-upgrade <token>` confirms exactly the schema findings it names.**
3554+
Upgrading an installed package stops with `SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`
3555+
when the new version would change a stored field in a way the schema sync does
3556+
not carry: a field removed while another is added with no `renamedFrom` linking
3557+
them (if it is a rename, the new field starts empty and the values stay in the
3558+
old column), or a type change that keeps or drops a stored column
3559+
(`needs_confirm` or `destructive`). It also stops when the runtime cannot read
3560+
its record of the installed version, since nothing could be compared. Nothing is
3561+
installed. The command prints each finding and the exact retry, carrying the
3562+
finding set's token. If a field was renamed, declare its old key in the new
3563+
field's `renamedFrom` and rebuild instead; a declared rename is refused with
3564+
`501 NOT_IMPLEMENTED` until the runtime can carry the column's values to the new
3565+
key. As with `--confirm-access-upgrade`, the token stops matching once the
3566+
findings change, and the confirmation is recorded in the install's ledger entry.
3567+
A type change on a field that had no stored column, a removed field and a seed
3568+
data change are never refused; `--dry-run` lists them. `os deploy` passes
3569+
`--confirm-upgrade` through to a self-hosted runtime. A fresh install is never
3570+
checked.
3571+
35523572
A `404` means the target runtime does not mount `MarketplaceInstallLocalPlugin`
35533573
(from `@objectstack/cloud-connection`). The endpoint is opt-in, so a runtime
35543574
composed without it will not accept installs.

‎content/docs/references/api/automation-api.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ const result = AutomationApiErrorCode.parse(data);
417417

418418
| Property | Type | Required | Description |
419419
| :--- | :--- | :--- | :--- |
420-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +361 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
420+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +362 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
421421
| **message** | `string` | ✅ | The inner refusal's own diagnostic text, verbatim, without the node and operation prefixes the run's failure text adds |
422422

423423
### Allowed Values: `FlowInnerRefusal.code`
@@ -724,6 +724,7 @@ const result = AutomationApiErrorCode.parse(data);
724724
* `RULE_NOT_FOUND`
725725
* `RUN_NOT_FOUND`
726726
* `SAML_REGISTER_FAILED`
727+
* `SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`
727728
* `SELF_REGISTRATION_CLOSED`
728729
* `SERVICE_NOT_REGISTERED`
729730
* `SETTINGS_ACTION_FAILED`

‎content/docs/references/api/contract.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const result = ApiErrorSchema.parse(data);
2828

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
31-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +361 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
31+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +362 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3232
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
3333
| **message** | `string` | ✅ | Readable error message |
3434
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -342,6 +342,7 @@ const result = ApiErrorSchema.parse(data);
342342
* `RULE_NOT_FOUND`
343343
* `RUN_NOT_FOUND`
344344
* `SAML_REGISTER_FAILED`
345+
* `SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`
345346
* `SELF_REGISTRATION_CLOSED`
346347
* `SERVICE_NOT_REGISTERED`
347348
* `SETTINGS_ACTION_FAILED`

‎content/docs/references/api/error-code-ledger.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ const result = ErrorCode.parse(data);
509509
* `RULE_NOT_FOUND`
510510
* `RUN_NOT_FOUND`
511511
* `SAML_REGISTER_FAILED`
512+
* `SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`
512513
* `SELF_REGISTRATION_CLOSED`
513514
* `SERVICE_NOT_REGISTERED`
514515
* `SETTINGS_ACTION_FAILED`

‎packages/cli/src/commands/deploy.ts‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@
6161
* version against this build, printed; nothing is installed. A runtime
6262
* that refuses it, or does not serve install-local, stops the deploy here
6363
* with its sentence and exit 1;
64-
* 3. the install, with `--confirm-access-upgrade` passed through; the door's
65-
* refusals (an access upgrade to confirm, …) stop the deploy with exit 1;
64+
* 3. the install, with `--confirm-access-upgrade` and `--confirm-upgrade`
65+
* passed through; the door's refusals (an access upgrade or a schema
66+
* upgrade to confirm, …) stop the deploy with exit 1;
6667
* 4. the answer — install-local registers the package in the live kernel
6768
* before it answers, so the answer IS "live": `hotLoaded` ⇒ 0; a package
6869
* the running kernel could not load ⇒ 1, naming the restart it waits for.
@@ -102,6 +103,7 @@ export default class Deploy extends Command {
102103
'$ os deploy --env env_abc123 # your account reaches several environments',
103104
'$ os deploy --no-wait # return once the install is recorded',
104105
'$ os deploy --confirm-access-upgrade <finding-set> # self-hosted: confirm the findings the runtime printed',
106+
'$ os deploy --confirm-upgrade <finding-set> # self-hosted: confirm the schema findings the runtime printed',
105107
];
106108

107109
static override flags = {
@@ -140,6 +142,16 @@ export default class Deploy extends Command {
140142
'Self-hosted runtime target: confirm the access findings of this upgrade with the finding-set token the '
141143
+ "runtime's refusal printed (as `os package install --confirm-access-upgrade`)",
142144
}),
145+
// [epic cloud#3652 Q3, #30884] The schema findings' confirmation, passed
146+
// through to the install as `--confirm-access-upgrade` is: the embedded
147+
// install's refusal prints `os deploy --confirm-upgrade <token>` as its
148+
// retry, so this command must take it.
149+
'confirm-upgrade': Flags.string({
150+
description:
151+
'Self-hosted runtime target: confirm the schema findings of this upgrade (a suspected rename, a type change '
152+
+ "that is not safe) with the finding-set token the runtime's refusal printed (as `os package install "
153+
+ '--confirm-upgrade`)',
154+
}),
143155
};
144156

145157
async run(): Promise<void> {
@@ -151,12 +163,13 @@ export default class Deploy extends Command {
151163
// cloud environment was named.
152164
const runtime = flags.env ? undefined : await activeRuntimeTarget();
153165
if (runtime) {
154-
await this.deployToRuntime(runtime, flags['confirm-access-upgrade']);
166+
await this.deployToRuntime(runtime, flags['confirm-access-upgrade'], flags['confirm-upgrade']);
155167
return;
156168
}
157-
if (flags['confirm-access-upgrade'] !== undefined) {
169+
for (const flag of ['confirm-access-upgrade', 'confirm-upgrade'] as const) {
170+
if (flags[flag] === undefined) continue;
158171
printError(
159-
'`--confirm-access-upgrade` confirms a self-hosted runtime\'s refusal; the deploy target is ObjectOS Cloud. '
172+
`\`--${flag}\` confirms a self-hosted runtime's refusal; the deploy target is ObjectOS Cloud. `
160173
+ 'Drop the flag, or record the runtime with `os environments use-runtime <url> --token <key>`.',
161174
);
162175
this.exit(1);
@@ -285,7 +298,11 @@ export default class Deploy extends Command {
285298
* The self-hosted half (the header's "A self-hosted runtime target"):
286299
* build → dry run (plan printed) → install → the `hotLoaded` answer.
287300
*/
288-
private async deployToRuntime(runtime: RuntimeTarget, confirmAccessUpgrade: string | undefined): Promise<void> {
301+
private async deployToRuntime(
302+
runtime: RuntimeTarget,
303+
confirmAccessUpgrade: string | undefined,
304+
confirmUpgrade: string | undefined,
305+
): Promise<void> {
289306
printStep(`Deploying to the self-hosted runtime ${runtime.url} (os environments use-runtime)`);
290307

291308
printStep('Building...');
@@ -317,7 +334,10 @@ export default class Deploy extends Command {
317334
}
318335

319336
printStep(`Installing ${plan.manifestId} ${plan.version} into ${runtime.url}...`);
320-
const outcome = await install(confirmAccessUpgrade !== undefined ? ['--confirm-access-upgrade', confirmAccessUpgrade] : []);
337+
const outcome = await install([
338+
...(confirmAccessUpgrade !== undefined ? ['--confirm-access-upgrade', confirmAccessUpgrade] : []),
339+
...(confirmUpgrade !== undefined ? ['--confirm-upgrade', confirmUpgrade] : []),
340+
]);
321341
if (outcome?.kind !== 'installed') {
322342
printError(`${runtime.url} did not answer the install as a managed package install, so this cannot say ${plan.manifestId} is live (see above).`);
323343
this.exit(1);

0 commit comments

Comments
 (0)