Repository navigation
fix(cli)!: os migrate apply exits 1 when the confirmation it needs is not given - #25004
Conversation
…not given A non-interactive run (non-TTY stdin, or --json) without --yes applied nothing and exited 0; every other os migrate subcommand with a --yes gate exits 1 there. Both faces now exit 1; an interactive decline keeps exit 0. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Every migrate subcommand that writes behind a confirmation, discovered from its flags, answers a non-interactive run without it with exit 1 in both faces, names its confirmation flag, and reaches no write seam. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…ith its BREAKING banner Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ae89e67d7e76c7b765f6b1a7d517754d906bbe3f && git checkout ae89e67d7e76c7b765f6b1a7d517754d906bbe3f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b 5a3e45078e45624641d5fded12beac4ff612c5dd && git checkout -B drift-repro 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b && git merge --no-ff 5a3e45078e45624641d5fded12beac4ff612c5dd
node scripts/docs-audit/affected-docs.mjs --json 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b
|
Contract reviewServed-tier: ① Derived judgmentsNet diff against main (merge-base
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT
Accepted deviations: the review's ③.
Out-of-scope dispositions (one line each):
Landing: through the merge queue by this seat, now: every check on this head reads Generated by Claude Code |
Fixes #24949
Clause-②: no (narrowing)
os migrate applynow exits1when the confirmation it needs is not given (stdin is not a terminal, or--json, and no--yes), as every otheros migratesubcommand with a confirmation gate already does. Nothing else about that run changes: it still applies nothing, prints the same warning and, under--json, emits the same document. An operator who answersnat the terminal prompt still gets exit0.Measured before the change (the CLI binary built at the branch base
e4d604efd3)The fixture is a config-only project on a SQLite file (
OS_DATABASE_URL,NODE_ENV=production). It has one object whoselegacy_notefield was removed after a firstos migrate apply --yes, soos migrate planlists one destructive change,os24949_thing.legacy_note [drop_column]. Asafe driftcopy instead declares one new field (anadd_column). Forrecorded-byandresume, a copy was seeded with one sentinel history row and one interrupted journal run, so each run reaches its gate.organization-ownershipwas given a plan file its own plan run wrote. "Written" compares a content hash of the schema plus every row of every table, read on a separate connection before and after each run. It is never a file hash. The terminal column used a real pseudo-terminal that answerednat the[y/N]prompt.--yes/dev/null--jsonnapply --allow-destructiveapply(safe drift)activity-changed-fields --applyaudit-mail-bodies --applyaudit-metadata-bodies --applycomment-parent-pair --applycomment-reactions --applyfiles-to-references --applymeta --stored --applymulti-value-columns --applyrecorded-by --applyresume --run RUNsecurity-catalog-overlays --applysummary-nulls --applyvalue-shapes --applyorganization-ownership --apply --plan-file PLAN(no--backup-taken)--yesfor every row except the last, which names--backup-taken. No run wrote anything without its confirmation.applywas the only member answering 0.summary-nullsandvalue-shapeswere not in the claim's candidate list. They have--yesgates too and were measured as control rows.flags.json || !process.stdin.isTTY, and everyconfirm()returns false on a non-TTY stdin before it builds a prompt. No CI variable is read,applyincluded. The divergence was only the exit.recorded-by --applyandresume --rungate after a plain boot. Re-measured on a copy whose config was one field ahead of the database, the content hash was unchanged: that boot added no column.The change
packages/cli/src/commands/migrate/apply.ts: the text face now ends withthis.exit(1)after its warning. The--jsonface emits its document with exit1(emitJson(…, 1); return;), which is the spellingapply's own three refusals already use, so the one-shot exit at the end ofrun()leaves with it. The warning text and the document are byte-for-byte unchanged.printWarning(…); this.exit(1)), andCliExitCodeinsrc/utils/format.tsis0 | 1. A new helper adopted by the 14 sites would rewrite 13 that are already right. The pin below holds the family to one code instead, and its population is discovered.The pin:
packages/cli/test/migrate-confirmation-exit.pin.test.ts(unit tier)src/commands/migratewhose command class declares ayesorapplyflag. That is 15 members, and the roster must equal them. A future subcommand with a confirmation gate fails the first case by id until its row is added.process.stdin.isTTYset to false by the case itself: exit1, the text face names the member's confirmation flag, and no write seam is reached. For members that gate before the boot, the write seam is the boot. Forapplyit isflushSchemaDdl/applyMigrationEntries, and forrecorded-by/resumethe journal runner.apply --allow-destructive --yesapplies and exits0.applyon a terminal answerednasks the prompt once, writes nothing and exits0.emitJsonandthis.exitrun for real. The commands are imported fromsrc/, so nodist/sits on the resolution path.Evidence:
main.apply.tswas restored from the base into the tree only (tree blob091e6bbequals the base blob). The pin ran 2 failed, 32 passed:apply — text faceandapply — --json face, eachexpected +0 to be 1. The restore wasgit checkout HEAD, and the blob5581f5dequals HEAD,git diff HEADis empty andgit statusis clean.scripts/ablation-replace.mjs. Each mutation was proven on disk (anchor 1 → 0, blob changed) and restored hash-equal to HEAD withgit diff HEADempty:apply's text-facethis.exit(1)→ 1 failed (apply — text face), 33 passed;apply's--jsonexit1→0→ 1 failed (apply — --json face), 33 passed;comment-reactions's text-facethis.exit(1)→ 1 failed (comment-reactions — text face), 33 passed. The per-member rows hold the siblings, not onlyapply.After, at the public door (the CLI built from this branch)
apply --allow-destructive, stdin/dev/null⚠ Confirmation required …lineapply --allow-destructive --jsonmessage: confirmation_required,hint: pass --yesapply(safe drift), stdin/dev/nullapply --allow-destructive, terminal answerednAborted — no changes made.apply --allow-destructive --yes(control)✓ Applied 1 change(s).Local verification (final head
5a3e45078e)@objectstack/cliunit tier (vitest run --project unit): 316 files, 4684 tests passed, atddd7dfdb62. The only later commit is a type cast in the new test file, and that file re-ran green at5a3e45078e(34 passed).apply's gate (plan.reads-env-files.integration.test.ts, which reads the document and not the exit) was run anyway: 6 passed.pnpm --filter @objectstack/cli typecheck: exit 0 (check:test-typecheck: OK). The first run flagged one TS2352 in the new test file, fixed in5a3e45078e.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 68 commands, and its 11 declared WIDE-population families were run too, 79 in all. 77 exited 0 on the first pass.check:dual-build-cjs-loadsandcheck:i18n-coverageexited 3 (PREREQUISITE NOT MET: nodist/for packages this diff does not touch). After a workspace build both exited 0.--ranprinted✓ dispatch-gates --ran: 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED.node scripts/check-changeset-no-major.mjs --base origin/main: exit 0,✓ This diff introduces no major bump.node scripts/check-adr-0087-registration.mjs --base e4d604efd3: exit 0. It lists24949-migrate-apply-confirmation-exit.md [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription).pnpm lint(the whole repository,eslint . --no-inline-config): exit 0.Changeset:
.changeset/24949-migrate-apply-confirmation-exit.md,@objectstack/climinor, with a BREAKING banner, the FROM → TO, what is unchanged, the one-line script fix (pass--yes, or runos migrate planto preview) and its ADR-0087 disposition. Clause-② narrowing means aCONTRACT_REVIEW_TIERreview of record is owed before the queue.Acceptance notes
organization-ownership's terminal decline exits 1; every other member's exits 0. Its prompt asks whether a backup was taken, and anthere is the refusal "--apply needs a backup of this database first". This was measured and left as it is: an interactive decline keeps its code, and that is a hold-off of this card. No carrier.content/docs/upgrading.mdx(the v18.0.0 step 8) andcontent/docs/releases/v18/18-0.mdx(the 18.0.0 checklist, from docs(releases): the 18.0.0 notes and upgrade checklist, walked from 17.7.0; Upgrading's table gains 18.0.0 and 17.5–17.7 #24922: "read the output, not the exit code") say a non-interactiveapplywithout--yesstill exits0. Both describe the measured 18.0.0 walk, which stays true of 18.0.0. The release page is release-owned and is not edited in a code PR. The behaviour change reaches the next release's notes through this changeset. Carrier: the release compilation (maintainer).apply's--jsondocument names the condition inmessage, where its siblings useerror. The document shape is a hold-off of this card, and it is unchanged. Noted only.mainmoved 5 commits past the base while this ran, none of them underpackages/cli. The branch was not merged with it.Generated by Claude Code