|
| 1 | +--- |
| 2 | +"@objectstack/service-automation": minor |
| 3 | +"@objectstack/metadata-protocol": minor |
| 4 | +"@objectstack/cli": patch |
| 5 | +--- |
| 6 | + |
| 7 | +feat(automation,migrate): `os migrate meta --stored` now covers flow rows too (#4454) |
| 8 | + |
| 9 | +#4327 gave the stored-metadata conversion chain a finish line for every |
| 10 | +metadata type except `flow` — the one type where the most stored dialect |
| 11 | +actually lives, since the graduated conversions `flow-node-crud-filter-alias`, |
| 12 | +`flow-node-crud-object-alias`, `flow-node-notify-config-aliases` and |
| 13 | +`flow-node-script-config-aliases` are all flow-node entries. Flow-node |
| 14 | +conversions carry ADR-0078's open-namespace conflict guard, which has to consult |
| 15 | +the *live* executor registry to tell a rename from a clobber, and the metadata |
| 16 | +layer has no way to obtain one. Flows were reported `skipped` with that reason. |
| 17 | +They are now converted. |
| 18 | + |
| 19 | +**One canonicalization policy, two shapes.** |
| 20 | +`AutomationEngine.canonicalizeStoredFlow` is the single implementation and |
| 21 | +`registerFlow` calls it, so the load seam and the migration can never disagree |
| 22 | +about what "canonical" means. It returns `parsed` (for execution — the |
| 23 | +`FlowSchema.parse` + #4347 region output, schema defaults materialized) and |
| 24 | +`storable` (for persistence). |
| 25 | + |
| 26 | +**`storable` excludes schema defaults, and that is the load-bearing decision.** |
| 27 | +Measured rather than assumed: driving a pre-17 flow through all three steps |
| 28 | +*removes* nothing — `FlowSchema` is strict since #4001, so an unrecognized key |
| 29 | +throws instead of being silently dropped, which means the |
| 30 | +`graftNormalizedOperators` precedent (it exists because the *view* parse strips |
| 31 | +Studio-only auxiliary keys) does not transfer — and *adds* only defaults: |
| 32 | +`version`, `runAs`, per-edge `type` / `isDefault`. Persisting a default the |
| 33 | +author never wrote would pin every migrated row to today's value while untouched |
| 34 | +rows follow tomorrow's: two populations with different behaviour, which is |
| 35 | +exactly the drift this pass exists to remove. So the write-back is the |
| 36 | +conversion result plus the `{dialect, source}` envelopes the schema derives for |
| 37 | +edge conditions, and nothing else. |
| 38 | + |
| 39 | +One subtlety worth knowing if you extend this: that envelope is a schema |
| 40 | +transform, not a conversion, so it emits **no** notice while still changing the |
| 41 | +body. Reading notices alone — correct for every other metadata type — would call |
| 42 | +such a row canonical and leave it re-deriving on every boot. Both passes are |
| 43 | +copy-on-write, so identity is the exact test for flows. |
| 44 | + |
| 45 | +**New: `AutomationServicePluginOptions.armRuntime`** (default `true`, so every |
| 46 | +server, dev stack and test host is unaffected). Set `false` and the plugin |
| 47 | +brings up the engine and the complete node registry — built-ins plus whatever |
| 48 | +`automation:ready` contributes, because a *partial* registry would make the |
| 49 | +conflict guard read a live custom node type as unowned and rewrite over it — and |
| 50 | +then stops before anything is armed: |
| 51 | + |
| 52 | +| Skipped when `armRuntime: false` | Why it must be | |
| 53 | +|---|---| |
| 54 | +| flow pull + `kernel:ready` / `metadata:reloaded` re-sync | `registerFlow` calls `activateFlowTrigger` — record triggers and scheduled jobs would go live | |
| 55 | +| declarative connector materialization | opens real connections; an MCP provider spawns a child process | |
| 56 | +| suspended-run wait-timer re-arm | would resume someone's paused approval mid-migration | |
| 57 | + |
| 58 | +`os migrate meta --stored` boots the plugin in that mode. A migration process |
| 59 | +must not become a second server. |
| 60 | + |
| 61 | +A refused rename — the guard firing because the old node-type token is a live |
| 62 | +name something else owns in this environment — fails that row loudly, naming the |
| 63 | +token and its owner. Never a silent skip, never a clobber. A flow that cannot |
| 64 | +canonicalize at all (a strict-schema violation, a malformed control-flow region) |
| 65 | +is reported as failed with the parse message rather than persisted as a guess; |
| 66 | +such a row cannot register today either, so the report is telling you about a |
| 67 | +flow that is already broken at runtime. |
0 commit comments