Skip to content

os migrate apply writes DDL and THEN exits non-zero on an unloadable host config — should a mutating command refuse before touching the database? #13118

Description

@os-litant

Filed by the domain:cli execution PM seat (#6024), session session_01UjujZN219uFzBhSYfMykCd, carrying an axis the #12953 dev (PR #13113) raised and that the 2026-08-29 maintainer ruling did not cover. ⛔ Not graded. needs-user-decision because it is a behaviour change to a mutating command.

What is settled, and what is not

Settled (#12953, maintainer ruling 2026-08-29, verbatim 「同意」, option A): when a host objectstack.config.{ts,js,mjs}exists but cannot be loaded, os migrate plan and os migrate apply exit non-zero. Shipped in PR #13113 and measured in all three directions (present+unloadable ⇒ non-zero; absent ⇒ unchanged; present+loadable ⇒ unchanged).

Not settled: whether apply should also decline to perform the DDL on that path.

Today — and in PR #13113apply still flushes the deferred schema work and applies drift over the reduced object set, then exits non-zero. The ruling fixed the exit status; it said nothing about the mutation.

Why it is worth asking

⭐ The dev put it precisely: "exit non-zero after having written DDL" is an odd contract for a mutating command. The same run is simultaneously saying "this result is UNMEASURED, not in sync" (the new stderr text, quoted from the ruling's own reasoning) and "…and I went ahead and changed your schema on that basis."

⚠️But the obvious fix has a cost the ruling never weighed, and the dev named it: refusing before mutating stops config-broken deployments from getting their platform floor created. That is a new failure mode in bootstrap-shaped situations — a deployment whose config is broken may need the floor in order to become fixable.

⇒ Two real costs on opposite sides. Neither is obviously smaller, which is why this is a decision rather than a follow-up chore.

Options, ⛔ not prejudged

  1. Leave as shipped.apply behaves exactly as before and reports non-zero. Strictly an improvement over today, where the same DDL ran silently with exit 0. Keeps bootstrap working. ⚠️ Leaves the odd contract.
  2. Refuse before mutating.apply exits non-zero without touching the database. Reads as a migration tool declining an unmeasured run. ⚠️ New failure mode for config-broken deployments that need the floor.
  3. Split the difference — refuse by default, allow the mutation behind an explicit flag that names what it is doing (an operator saying "yes, apply over the reduced set"). ⚠️ Adds a published surface; ⛔ and this repo's habit is not to add a flag before the need is measured.

Not established here

  • How often the unloadable-config path is reached with a bootstrap intent. Nobody has counted. If it is never the bootstrap path, option 2's cost is theoretical and it wins easily; if it is a real path, option 1 or 3.
  • Whether a partial apply on the reduced set is recoverable — i.e. does a later successful apply converge, or does the partial run leave state a full run will not correct? ⛔ This seat did not measure it, and it is the sharpest input to the decision: if partial applies converge, option 1 is cheap; if they can strand a schema, option 2 gets much stronger.

Refs

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions