From cecb4075e0606f34481ae28d5942968cb4bab246 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 03:52:27 +0000 Subject: [PATCH] fix(cli): os deploy takes --confirm-global-uniques on a self-hosted runtime The use-runtime path runs os package install with `os deploy` as its retry command, so the installation-wide uniques stop printed `os deploy --confirm-global-uniques`, a flag os deploy did not declare. Declare it and pass it through to the install beside the two upgrade confirmations, through one seam; refuse it on an ObjectOS Cloud target as the other two are refused. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FEkQ5o7rUkfGZf2hPcQqZJ --- .../31338-deploy-confirm-global-uniques.md | 12 +++++ content/docs/deployment/cli.mdx | 10 ++-- packages/cli/src/commands/deploy.ts | 48 +++++++++++++------ packages/cli/test/deploy.test.ts | 31 ++++++++++++ 4 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 .changeset/31338-deploy-confirm-global-uniques.md diff --git a/.changeset/31338-deploy-confirm-global-uniques.md b/.changeset/31338-deploy-confirm-global-uniques.md new file mode 100644 index 00000000000..a00777f7fe3 --- /dev/null +++ b/.changeset/31338-deploy-confirm-global-uniques.md @@ -0,0 +1,12 @@ +--- +'@objectstack/cli': patch +--- + +fix(cli): `os deploy` to a self-hosted runtime takes `--confirm-global-uniques`, the retry its installation-wide uniques stop prints + +Clause-②: yes (widening) — `os deploy` gains an accepted flag, `--confirm-global-uniques`, widening a published CLI command's accept set +Metadata-protocol: none + +- **What was wrong.** With a self-hosted runtime as the deploy target (`os environments use-runtime`), `os deploy` installs through `os package install` run in the same process. When an `isolated`-posture runtime stops the install on the app's installation-wide (`unique: 'global'`) constraints (`UNIQUE_SCOPE_CONFIRMATION_REQUIRED`), the retry it prints is `os deploy --confirm-global-uniques`, but `os deploy` did not declare that flag: following the printed advice was refused as an unknown flag, and the deploy had no way to pass the confirmation. +- **What changes.** `os deploy` declares `--confirm-global-uniques` and passes it through to the install, as it already does `--confirm-access-upgrade` and `--confirm-upgrade`: all three go through one seam, to the install only (the dry run sends none, as before). Named while the target is ObjectOS Cloud, it is refused before anything is built or sent, like the two upgrade confirmations. +- **What does not change.** `os package install --confirm-global-uniques` and the two upgrade confirmations behave as before. diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx index 56ffee7a1f6..1abba2af305 100644 --- a/content/docs/deployment/cli.mdx +++ b/content/docs/deployment/cli.mdx @@ -3241,6 +3241,7 @@ is its production, so a deploy is always a deliberate act. | `--wait-timeout ` | — | How long to wait for the environment to serve the new version (default `600`) | | `-s, --server ` | `OS_CLOUD_URL` | Control-plane URL. Default `https://cloud.objectos.ai`, or the URL recorded by `os cloud login` | | `-t, --token ` | `OS_CLOUD_API_KEY` | Bearer token; `$OS_TOKEN` and `~/.objectstack/cloud.json` are the fallbacks | +| `--confirm-global-uniques` | — | Self-hosted target only: confirm the app's installation-wide `unique` constraints are genuinely platform-wide when the runtime's refusal asks (as [`os package install`](#os-package-install)). Refused when the target is ObjectOS Cloud | | `--confirm-access-upgrade ` | — | Self-hosted target only: confirm the upgrade's access findings with the finding-set token the runtime's refusal printed (as [`os package install`](#os-package-install)). Refused when the target is ObjectOS Cloud | `os package publish` stays the command for a **release**: it publishes the @@ -3262,9 +3263,12 @@ answers are the ones printed. build is printed; nothing is installed. A runtime that refuses it, or does not serve install-local, stops the deploy with exit `1`. A runtime that predates the dry run ignores it and installs: the command says so and exits `1`. -3. **Install** — with `--confirm-access-upgrade` passed through. The endpoint's - refusals (an access upgrade to confirm, a package the running kernel cannot - register, …) stop the deploy with exit `1`. +3. **Install** — with `--confirm-global-uniques`, `--confirm-access-upgrade` and + `--confirm-upgrade` passed through. The endpoint's refusals (installation-wide + uniques to confirm on an `isolated` runtime, an access upgrade to confirm, a + package the running kernel cannot register, …) stop the deploy with exit `1`, + and the retry they print names `os deploy`: rerun it with the flag it prints, + e.g. `os deploy --confirm-global-uniques`. 4. **Live** — install-local registers the package in the running kernel before it answers, so the answer is the verdict: `hotLoaded` prints `Live: … now serves …` and exits `0`. diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index ca84aa46eb7..2b59a9ce9b7 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -61,9 +61,12 @@ * version against this build, printed; nothing is installed. A runtime * that refuses it, or does not serve install-local, stops the deploy here * with its sentence and exit 1; - * 3. the install, with `--confirm-access-upgrade` and `--confirm-upgrade` - * passed through; the door's refusals (an access upgrade or a schema - * upgrade to confirm, …) stop the deploy with exit 1; + * 3. the install, with `--confirm-global-uniques`, `--confirm-access-upgrade` + * and `--confirm-upgrade` passed through; the door's refusals + * (installation-wide uniques, an access upgrade or a schema upgrade to + * confirm, …) stop the deploy with exit 1, and their retry names + * `os deploy`, so every confirmation the install prints is one this + * command declares; * 4. the answer — install-local registers the package in the live kernel * before it answers, so the answer IS "live": `hotLoaded` ⇒ 0; a package * the running kernel could not load ⇒ 1, naming the restart it waits for. @@ -93,6 +96,18 @@ export const DEPLOY_ARTIFACT = 'dist/objectstack.json'; /** How often the wait asks the control plane. */ export const DEPLOY_POLL_INTERVAL_MS = 3_000; +/** The install confirmations this command passes through to a self-hosted runtime's install. */ +const CONFIRMATION_FLAGS = ['confirm-global-uniques', 'confirm-access-upgrade', 'confirm-upgrade'] as const; + +/** The given confirmations as `os package install` argv: a switch as itself, a token after its flag. */ +function confirmationArgv(flags: { [K in (typeof CONFIRMATION_FLAGS)[number]]?: string | boolean }): string[] { + return CONFIRMATION_FLAGS.flatMap((flag) => { + const value = flags[flag]; + if (value === undefined || value === false) return []; + return value === true ? [`--${flag}`] : [`--${flag}`, value]; + }); +} + export default class Deploy extends Command { static override description = 'Deploy this app to your ObjectOS Cloud environment (build it, publish a dev version, install it, and wait until ' @@ -102,6 +117,7 @@ export default class Deploy extends Command { '$ os deploy', '$ os deploy --env env_abc123 # your account reaches several environments', '$ os deploy --no-wait # return once the install is recorded', + '$ os deploy --confirm-global-uniques # self-hosted: confirm installation-wide uniques', '$ os deploy --confirm-access-upgrade # self-hosted: confirm the findings the runtime printed', '$ os deploy --confirm-upgrade # self-hosted: confirm the schema findings the runtime printed', ]; @@ -137,6 +153,15 @@ export default class Deploy extends Command { description: 'API key for ObjectOS Cloud (bearer token; defaults to OS_CLOUD_API_KEY / OS_TOKEN, then `os cloud login`)', env: 'OS_CLOUD_API_KEY', }), + // The installation-wide uniques' confirmation, passed through like the + // two upgrade confirmations below: the embedded install's + // UNIQUE_SCOPE_CONFIRMATION_REQUIRED stop prints + // `os deploy --confirm-global-uniques` as its retry. + 'confirm-global-uniques': Flags.boolean({ + description: + "Self-hosted runtime target: confirm this app's installation-wide (`unique: 'global'`) constraints are " + + "genuinely platform-wide when the runtime's refusal asks (as `os package install --confirm-global-uniques`)", + }), 'confirm-access-upgrade': Flags.string({ description: 'Self-hosted runtime target: confirm the access findings of this upgrade with the finding-set token the ' @@ -163,11 +188,11 @@ export default class Deploy extends Command { // cloud environment was named. const runtime = flags.env ? undefined : await activeRuntimeTarget(); if (runtime) { - await this.deployToRuntime(runtime, flags['confirm-access-upgrade'], flags['confirm-upgrade']); + await this.deployToRuntime(runtime, confirmationArgv(flags)); return; } - for (const flag of ['confirm-access-upgrade', 'confirm-upgrade'] as const) { - if (flags[flag] === undefined) continue; + for (const flag of CONFIRMATION_FLAGS) { + if (flags[flag] === undefined || flags[flag] === false) continue; printError( `\`--${flag}\` confirms a self-hosted runtime's refusal; the deploy target is ObjectOS Cloud. ` + 'Drop the flag, or record the runtime with `os environments use-runtime --token `.', @@ -298,11 +323,7 @@ export default class Deploy extends Command { * The self-hosted half (the header's "A self-hosted runtime target"): * build → dry run (plan printed) → install → the `hotLoaded` answer. */ - private async deployToRuntime( - runtime: RuntimeTarget, - confirmAccessUpgrade: string | undefined, - confirmUpgrade: string | undefined, - ): Promise { + private async deployToRuntime(runtime: RuntimeTarget, confirmations: string[]): Promise { printStep(`Deploying to the self-hosted runtime ${runtime.url} (os environments use-runtime)`); printStep('Building...'); @@ -334,10 +355,7 @@ export default class Deploy extends Command { } printStep(`Installing ${plan.manifestId} ${plan.version} into ${runtime.url}...`); - const outcome = await install([ - ...(confirmAccessUpgrade !== undefined ? ['--confirm-access-upgrade', confirmAccessUpgrade] : []), - ...(confirmUpgrade !== undefined ? ['--confirm-upgrade', confirmUpgrade] : []), - ]); + const outcome = await install(confirmations); if (outcome?.kind !== 'installed') { printError(`${runtime.url} did not answer the install as a managed package install, so this cannot say ${plan.manifestId} is live (see above).`); this.exit(1); diff --git a/packages/cli/test/deploy.test.ts b/packages/cli/test/deploy.test.ts index c8c12035e2d..8cf3c9f8ed6 100644 --- a/packages/cli/test/deploy.test.ts +++ b/packages/cli/test/deploy.test.ts @@ -28,6 +28,7 @@ vi.mock('../src/utils/deploy-build.js', () => ({ compileForDeploy: (p: string) = const { default: Deploy, reportVerdict } = await import('../src/commands/deploy.js'); const { waitUntilServed, interpretServedReport } = await import('../src/utils/served-version.js'); const { devVersion } = await import('../src/utils/dev-version.js'); +const { default: PackageInstall } = await import('../src/commands/package/install.js'); const CLOUD_PLANE = 'http://cloud.test'; const MANAGED_ENV = ['OS_CLOUD_URL', 'OS_CLOUD_API_KEY', 'OS_TOKEN', 'OS_ENVIRONMENT_ID', 'OS_ORG_ID', 'OS_PACKAGE_MANIFEST_ID', 'OS_CLOUD_TIMEOUT_MS'] as const; @@ -592,6 +593,26 @@ describe('os deploy — a self-hosted runtime as the active target (os environme expect(sent[1].body.confirmSchemaUpgrade).toBe('schema-upgrade:abc'); }); + it("the installation-wide uniques stop's retry names os deploy --confirm-global-uniques, and the confirmation passes through", async () => { + const uniquesRefusal = refused(409, 'UNIQUE_SCOPE_CONFIRMATION_REQUIRED', "acme_lead.email is unique: 'global'"); + let sent = stubRuntime({ install: uniquesRefusal }); + expect(await deploy()).toBe(1); + expect(sent).toHaveLength(2); + expect(text()).toContain('os deploy --confirm-global-uniques'); + + vi.unstubAllGlobals(); + sent = stubRuntime({}); + expect(await deploy(['--confirm-global-uniques']), text()).toBeUndefined(); + expect(sent[0].body.confirmGlobalUniques).toBeUndefined(); + expect(sent[1].body.confirmGlobalUniques).toBe(true); + }); + + it('declares every confirmation flag the embedded install prints as its retry', () => { + const confirmations = Object.keys(PackageInstall.flags).filter((name) => name.startsWith('confirm-')); + expect(confirmations).toContain('confirm-global-uniques'); + for (const name of confirmations) expect(Object.keys(Deploy.flags), name).toContain(name); + }); + it('a package the running kernel could not load is not called live: exit 1', async () => { stubRuntime({ install: ok({ ...INSTALLED, hotLoaded: false, hotLoadError: 'boom' }) }); @@ -626,4 +647,14 @@ describe('os deploy — a self-hosted runtime as the active target (os environme expect(sent).toHaveLength(0); expect(compileForDeploy).not.toHaveBeenCalled(); }); + + it('--confirm-global-uniques is refused when the target is ObjectOS Cloud, before anything is sent', async () => { + await writeFile(join(home, '.objectstack', 'cloud.json'), JSON.stringify({ url: CLOUD_PLANE, token: 'cloud_tok', createdAt: 'now' })); + const sent = stubRuntime({}); + + expect(await deploy(['--confirm-global-uniques'])).toBe(1); + expect(sent).toHaveLength(0); + expect(compileForDeploy).not.toHaveBeenCalled(); + expect(text()).toContain('`--confirm-global-uniques` confirms a self-hosted runtime'); + }); });