feat(promote): resolve pre-existing entities by business key (#entity:field=value)

Closes the last promote gap: a manifest can now reference records it does NOT
create. A value like "#thirdparty:name=KissMetrics" (or :code=CL0007) is looked
up on the TARGET at apply time and resolved to that target's id — so the same
manifest is portable (sandbox id on --target sandbox, prod id on --target prod).

promote-apply.sh: resolve() gains a "#" branch + a lookup() helper that queries
the target via the GET wrapper with sqlfilters. Supports thirdparty
(name/code/supplier_code) and invoice/supplierinvoice (ref/ref_supplier). A
lookup matching nothing OR more than one record ABORTS the run — it never
guesses, so it cannot write to the wrong entity.

Proven live: "#thirdparty:name=ACME Conseil" resolved to the existing client and
invoiced it; a not-found code and an ambiguous (2-match) name both aborted with
exit 1. Combined with @refs, arbitrary self-contained-or-referential change-sets
now replay cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 23:57:49 +02:00
parent 7949ab34f8
commit 04985fe15c
2 changed files with 57 additions and 4 deletions

View File

@@ -141,10 +141,20 @@ dependent ops wire up on the target. `--target sandbox` writes via `dol-write.sh
`ARCO_PROMOTE_CONFIRM` is set exactly. Pair it with `dolibarr-data-snapshot` (prod
before/after) to confirm only the intended records changed.
Limitation: a manifest references entities it **creates** (via `@ref`). Referencing
a *pre-existing* prod entity by a sandbox id won't match — resolve those by business
key (name/code/ref) first. Self-contained change-sets (new thirdparty + its invoice
+ payment) replay cleanly today.
A manifest value can reference another entity two ways, both resolved against the
**target** so the same file is portable sandbox↔prod:
- **`@ref`** — an entity **created earlier in this manifest** (resolves to the id
just created on the target). For new records.
- **`#entity:field=value`** — a **pre-existing** entity, looked up on the target by
business key, e.g. `#thirdparty:name=KissMetrics` or `#thirdparty:code=CL0007`.
Supports `thirdparty` (name/code/supplier_code) and `invoice`/`supplierinvoice`
(ref/ref_supplier). A lookup matching **nothing or more than one** record **aborts
the run** — it never guesses, so it can't write to the wrong entity.
So a real change like "invoice the existing client KissMetrics and record its
payment" is `{"socid":"#thirdparty:name=KissMetrics", ...}` — it resolves to the
sandbox KissMetrics on `--target sandbox` and the prod one on `--target prod`.
## Gotchas