feat(skills,cli): promote-to-prod replay (ADR-0003 capstone) + supplier payment fix
The human-gated path that carries a reviewed sandbox change to prod.
- promote-plan.sh: render a manifest (JSON array of write ops with symbolic @refs
instead of ids — portable sandbox->prod) as a human-readable change-set.
- promote-apply.sh <manifest> --target sandbox|prod: replay it, resolving each
@ref to the id actually created during the run (dependent ops wire up). sandbox
rehearses via dol-write.sh; prod via dol-prod-write.sh.
- dol-prod-write.sh: the ONLY prod-write path. Prod key read from the ENVIRONMENT
only (DOLIBARR_PROD_WRITE_KEY, never a stored .env); every write refused unless
ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD.
- create scripts take a DOL_WRITE override so promote-apply reuses them per target.
- bin/arcodange: `promote {plan|apply}` group + example manifest.
- payment-record.sh: fixed supplier payments (payment_mode_id + closepaidinvoices).
Proven live: plan renders; apply --target sandbox replays a 3-op chain with refs
resolved (@tp1->id, invoice socid=@tp1, payment invoice=@inv1); --target prod
without the confirm flag is REFUSED before sending. Supplier payment now works
end-to-end via the script.
Limitation (documented): manifests reference entities they create (@ref);
pre-existing prod entities need business-key resolution (follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,10 +38,12 @@ human promotes the reviewed change to prod.
|
|||||||
key is read-only and lives in a different skill's `.env`.
|
key is read-only and lives in a different skill's `.env`.
|
||||||
- **Resettable.** Anything written here is wiped by `ops/sandbox/sandbox-lifecycle.sh
|
- **Resettable.** Anything written here is wiped by `ops/sandbox/sandbox-lifecycle.sh
|
||||||
refresh-from-prod`, so mistakes cost a reset, not data.
|
refresh-from-prod`, so mistakes cost a reset, not data.
|
||||||
- **Promotion to prod is NOT in this skill.** Rehearse here → capture a reviewable
|
- **Promotion to prod is gated, not automatic.** Rehearse here → review the
|
||||||
diff with the read-only `dolibarr-data-snapshot` skill (before/after) → a human
|
change-set (`promote-plan.sh`) → replay it on prod (`promote-apply.sh --target
|
||||||
approves → the same operations are replayed against prod under a separate,
|
prod`). The prod write key is supplied via the **environment at apply time**
|
||||||
human-held prod-write credential. Never wire a prod-write key into this skill.
|
(`DOLIBARR_PROD_WRITE_KEY`), never stored in any `.env`, and `dol-prod-write.sh`
|
||||||
|
refuses every prod write unless `ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD`.
|
||||||
|
See "Promote to prod" below.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -115,6 +117,35 @@ A customer invoice of `type=2` referencing `source_invoice` (`fk_facture_source`
|
|||||||
amounts come out negative (a credit). `validate:true` turns the draft into a
|
amounts come out negative (a credit). `validate:true` turns the draft into a
|
||||||
numbered `AVC…` avoir. Emits `{id, ref, total_ht, total_ttc, fk_facture_source, statut}`.
|
numbered `AVC…` avoir. Emits `{id, ref, total_ht, total_ttc, fk_facture_source, statut}`.
|
||||||
|
|
||||||
|
## Promote to prod (rehearse → review → replay)
|
||||||
|
|
||||||
|
The ADR-0003 capstone: take a change rehearsed in the sandbox and apply the **same
|
||||||
|
operations** to prod, with a human in the loop. The unit is a **manifest** — a JSON
|
||||||
|
array of write ops using **symbolic refs** (`@name`) instead of ids, so it is
|
||||||
|
portable from sandbox to prod (an invoice references `@tp1`, the thirdparty created
|
||||||
|
earlier in the run). See `examples/promote-manifest.json`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/promote-plan.sh change.json # 1. human-readable review
|
||||||
|
scripts/promote-apply.sh change.json --target sandbox # 2. rehearse the replay (safe)
|
||||||
|
# 3. promote — writes PROD; the key is env-only and the confirm flag is mandatory:
|
||||||
|
DOLIBARR_PROD_WRITE_KEY="<prod write key>" \
|
||||||
|
ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD \
|
||||||
|
scripts/promote-apply.sh change.json --target prod
|
||||||
|
```
|
||||||
|
|
||||||
|
`promote-apply` resolves each `@ref` to the id actually created during the run, so
|
||||||
|
dependent ops wire up on the target. `--target sandbox` writes via `dol-write.sh`;
|
||||||
|
`--target prod` writes via `dol-prod-write.sh`, which reads `DOLIBARR_PROD_WRITE_KEY`
|
||||||
|
**from the environment only** (never a stored `.env`) and refuses any write unless
|
||||||
|
`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.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- **Validate before paying.** A draft (`statut=0`, ref `PROV…`) cannot be paid.
|
- **Validate before paying.** A draft (`statut=0`, ref `PROV…`) cannot be paid.
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{ "op": "thirdparty", "ref": "tp1",
|
||||||
|
"input": { "name": "ACME Conseil", "role": "client", "tva_intra": "FR..." } },
|
||||||
|
|
||||||
|
{ "op": "invoice", "ref": "inv1",
|
||||||
|
"input": { "socid": "@tp1", "kind": "customer", "validate": true,
|
||||||
|
"lines": [ { "desc": "Prestation conseil", "qty": 2, "price_ht": 500, "tva": 20, "type": "service" },
|
||||||
|
{ "desc": "Licence annuelle", "qty": 1, "price_ht": 100, "tva": 20, "type": "product" } ] } },
|
||||||
|
|
||||||
|
{ "op": "payment",
|
||||||
|
"input": { "invoice_id": "@inv1", "mode": "VIR", "account_id": 1, "comment": "Acompte" } }
|
||||||
|
]
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
# Emits {id, ref, total_ht, total_ttc, fk_facture_source, statut} on stdout.
|
# Emits {id, ref, total_ht, total_ttc, fk_facture_source, statut} on stdout.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
W="${SCRIPT_DIR}/dol-write.sh"
|
W="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}"
|
||||||
|
|
||||||
SRC="${1:-}"
|
SRC="${1:-}"
|
||||||
if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi
|
if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi
|
||||||
|
|||||||
39
.claude/skills/dolibarr-sandbox-write/scripts/dol-prod-write.sh
Executable file
39
.claude/skills/dolibarr-sandbox-write/scripts/dol-prod-write.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GATED prod-write wrapper — the ONLY path in this skill that writes PRODUCTION.
|
||||||
|
#
|
||||||
|
# Used solely by promote-apply.sh --target prod, at promotion time. Two deliberate
|
||||||
|
# frictions make accidental or autonomous prod writes impossible (ADR-0003):
|
||||||
|
# 1. The prod write key is read from the ENVIRONMENT (DOLIBARR_PROD_WRITE_KEY),
|
||||||
|
# never from a stored .env — a human must supply it per invocation.
|
||||||
|
# 2. Any write method (POST/PUT/DELETE/PATCH) is REFUSED unless ARCO_PROMOTE_CONFIRM
|
||||||
|
# equals exactly "I-UNDERSTAND-THIS-WRITES-PROD".
|
||||||
|
# GET is allowed (id read-back) but still needs the key.
|
||||||
|
#
|
||||||
|
# DOLIBARR_PROD_WRITE_KEY=… ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD \
|
||||||
|
# dol-prod-write.sh POST /thirdparties '{...}'
|
||||||
|
set -euo pipefail
|
||||||
|
PROD_URL="${DOLIBARR_PROD_URL:-https://erp.arcodange.lab}"
|
||||||
|
: "${DOLIBARR_PROD_WRITE_KEY:?dol-prod-write.sh: DOLIBARR_PROD_WRITE_KEY must be supplied in the environment (never stored)}"
|
||||||
|
|
||||||
|
if [[ $# -lt 2 ]]; then echo "usage: dol-prod-write.sh <METHOD> <path> [body]" >&2; exit 2; fi
|
||||||
|
METHOD="$1"; API_PATH="$2"; BODY="${3-}"
|
||||||
|
case "${METHOD}" in
|
||||||
|
GET) ;;
|
||||||
|
POST|PUT|DELETE|PATCH)
|
||||||
|
if [[ "${ARCO_PROMOTE_CONFIRM:-}" != "I-UNDERSTAND-THIS-WRITES-PROD" ]]; then
|
||||||
|
echo "dol-prod-write.sh: REFUSING ${METHOD} ${API_PATH} on PRODUCTION (${PROD_URL})." >&2
|
||||||
|
echo " Set ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD to authorize prod writes." >&2
|
||||||
|
exit 3
|
||||||
|
fi ;;
|
||||||
|
*) echo "dol-prod-write.sh: unsupported method '${METHOD}'" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CURL_ARGS=( -sS -X "${METHOD}" -H "DOLAPIKEY: ${DOLIBARR_PROD_WRITE_KEY}" -H "Accept: application/json" --max-time 30 )
|
||||||
|
[[ -n "${BODY}" ]] && CURL_ARGS+=( -H "Content-Type: application/json" --data "${BODY}" )
|
||||||
|
|
||||||
|
BODY_FILE="$(mktemp -t dolprod.XXXXXX)"; trap 'rm -f "${BODY_FILE}"' EXIT
|
||||||
|
HTTP_CODE=$(curl "${CURL_ARGS[@]}" -o "${BODY_FILE}" -w "%{http_code}" "${PROD_URL}/api/index.php${API_PATH}")
|
||||||
|
cat "${BODY_FILE}"
|
||||||
|
if [[ "${HTTP_CODE}" -ge 400 ]]; then
|
||||||
|
echo "" >&2; echo "dol-prod-write.sh: HTTP ${HTTP_CODE} on ${METHOD} ${API_PATH}" >&2; exit 1
|
||||||
|
fi
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
# Emits {id, ref, ref_supplier, total_ht, total_ttc, statut} on stdout.
|
# Emits {id, ref, ref_supplier, total_ht, total_ttc, statut} on stdout.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
W="${SCRIPT_DIR}/dol-write.sh"
|
W="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}"
|
||||||
|
|
||||||
SRC="${1:-}"
|
SRC="${1:-}"
|
||||||
if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi
|
if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# Emits the new payment id on stdout.
|
# Emits the new payment id on stdout.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
W="${SCRIPT_DIR}/dol-write.sh"
|
W="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}"
|
||||||
|
|
||||||
SRC="${1:-}"
|
SRC="${1:-}"
|
||||||
if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi
|
if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi
|
||||||
@@ -41,7 +41,8 @@ if supplier:
|
|||||||
if d.get("amount") is None:
|
if d.get("amount") is None:
|
||||||
sys.exit("payment-record.sh: supplier payments require an 'amount'")
|
sys.exit("payment-record.sh: supplier payments require an 'amount'")
|
||||||
endpoint = "/supplierinvoices/%s/payments" % inv
|
endpoint = "/supplierinvoices/%s/payments" % inv
|
||||||
body = {"datepaye": epoch, "paymentid": mode, "accountid": d["account_id"],
|
body = {"datepaye": epoch, "payment_mode_id": mode, "closepaidinvoices": "yes",
|
||||||
|
"accountid": d["account_id"],
|
||||||
"amount": str(d["amount"]), "num_payment": d.get("num", ""),
|
"amount": str(d["amount"]), "num_payment": d.get("num", ""),
|
||||||
"comment": d.get("comment", "")}
|
"comment": d.get("comment", "")}
|
||||||
else:
|
else:
|
||||||
|
|||||||
68
.claude/skills/dolibarr-sandbox-write/scripts/promote-apply.sh
Executable file
68
.claude/skills/dolibarr-sandbox-write/scripts/promote-apply.sh
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Replay a promote manifest against a target — sandbox (rehearsal) or prod (real).
|
||||||
|
#
|
||||||
|
# Resolves the manifest's symbolic @refs to the ids actually created during this
|
||||||
|
# run, so dependent ops (an invoice -> its just-created thirdparty) wire up on the
|
||||||
|
# target. Sandbox uses dol-write.sh; prod uses the gated dol-prod-write.sh.
|
||||||
|
#
|
||||||
|
# promote-apply.sh <manifest.json> [--target sandbox|prod]
|
||||||
|
#
|
||||||
|
# --target prod requires, in the environment (never stored):
|
||||||
|
# DOLIBARR_PROD_WRITE_KEY=<prod write key>
|
||||||
|
# ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
MANIFEST="${1:?usage: promote-apply.sh <manifest.json> [--target sandbox|prod]}"; shift || true
|
||||||
|
TARGET="sandbox"
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--target) TARGET="${2:?}"; shift 2 ;;
|
||||||
|
*) echo "promote-apply.sh: unknown arg '$1'" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
case "${TARGET}" in
|
||||||
|
sandbox) export DOL_WRITE="${SCRIPT_DIR}/dol-write.sh" ;;
|
||||||
|
prod) export DOL_WRITE="${SCRIPT_DIR}/dol-prod-write.sh" ;;
|
||||||
|
*) echo "promote-apply.sh: --target must be sandbox|prod" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
echo ">>> promote-apply target=${TARGET} (writes via $(basename "${DOL_WRITE}"))" >&2
|
||||||
|
|
||||||
|
python3 - "$MANIFEST" "$SCRIPT_DIR" <<'PY'
|
||||||
|
import json, sys, subprocess, os
|
||||||
|
manifest_path, script_dir = sys.argv[1], sys.argv[2]
|
||||||
|
ops = json.load(open(manifest_path))
|
||||||
|
OP_SCRIPT = {"thirdparty": "thirdparty-create.sh", "invoice": "invoice-create.sh",
|
||||||
|
"creditnote": "creditnote-create.sh", "payment": "payment-record.sh"}
|
||||||
|
refmap = {}
|
||||||
|
def resolve(v):
|
||||||
|
if isinstance(v, str) and v.startswith("@"):
|
||||||
|
k = v[1:]
|
||||||
|
if k not in refmap:
|
||||||
|
sys.exit("promote-apply: unresolved ref @%s (is it created earlier in the manifest?)" % k)
|
||||||
|
return refmap[k]
|
||||||
|
if isinstance(v, dict):
|
||||||
|
return {kk: resolve(vv) for kk, vv in v.items()}
|
||||||
|
if isinstance(v, list):
|
||||||
|
return [resolve(x) for x in v]
|
||||||
|
return v
|
||||||
|
for i, op in enumerate(ops, 1):
|
||||||
|
t = op["op"]; script = OP_SCRIPT.get(t)
|
||||||
|
if not script:
|
||||||
|
sys.exit("promote-apply: unknown op '%s'" % t)
|
||||||
|
inp = resolve(op.get("input", {}))
|
||||||
|
r = subprocess.run([os.path.join(script_dir, script)], input=json.dumps(inp),
|
||||||
|
capture_output=True, text=True, env=os.environ)
|
||||||
|
if r.returncode != 0:
|
||||||
|
sys.stderr.write(r.stdout + r.stderr + "\n")
|
||||||
|
sys.exit("promote-apply: op %d (%s) FAILED" % (i, t))
|
||||||
|
out = r.stdout.strip()
|
||||||
|
try:
|
||||||
|
rid = json.loads(out).get("id")
|
||||||
|
except Exception:
|
||||||
|
rid = out if out.isdigit() else None
|
||||||
|
ref = op.get("ref")
|
||||||
|
if ref and rid is not None:
|
||||||
|
refmap[ref] = int(rid) if str(rid).isdigit() else rid
|
||||||
|
print(" [%d/%d] %-11s %-8s -> id=%s" % (i, len(ops), t, ("@" + ref) if ref else "", rid))
|
||||||
|
print("OK — promote complete. ref -> id: %s" % json.dumps(refmap))
|
||||||
|
PY
|
||||||
40
.claude/skills/dolibarr-sandbox-write/scripts/promote-plan.sh
Executable file
40
.claude/skills/dolibarr-sandbox-write/scripts/promote-plan.sh
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Render a promote manifest as a human-readable change-set for review.
|
||||||
|
#
|
||||||
|
# A manifest is a JSON array of operations using SYMBOLIC refs (@name) instead of
|
||||||
|
# ids, so the SAME manifest replays on sandbox or prod:
|
||||||
|
# [ {"op":"thirdparty","ref":"tp1","input":{"name":"OVH","role":"supplier"}},
|
||||||
|
# {"op":"invoice","ref":"inv1","input":{"socid":"@tp1","kind":"supplier",
|
||||||
|
# "ref_supplier":"X","validate":true,"lines":[{"desc":"Hosting","qty":1,"price_ht":80,"tva":20,"type":"service"}]}},
|
||||||
|
# {"op":"payment","input":{"invoice_id":"@inv1","mode":"VIR","account_id":1}} ]
|
||||||
|
set -euo pipefail
|
||||||
|
MANIFEST="${1:?usage: promote-plan.sh <manifest.json>}"
|
||||||
|
python3 - "$MANIFEST" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
ops = json.load(open(sys.argv[1]))
|
||||||
|
print("Promote plan — %d operation(s) (symbolic refs resolve at apply time):\n" % len(ops))
|
||||||
|
for i, op in enumerate(ops, 1):
|
||||||
|
t = op["op"]; inp = op.get("input", {}); ref = op.get("ref")
|
||||||
|
print(" %d. %s%s" % (i, t, (" => @%s" % ref) if ref else ""))
|
||||||
|
if t == "thirdparty":
|
||||||
|
print(" name=%r role=%s%s" % (inp.get("name"), inp.get("role", "client"),
|
||||||
|
(" tva=%s" % inp["tva_intra"]) if inp.get("tva_intra") else ""))
|
||||||
|
elif t == "invoice":
|
||||||
|
print(" socid=%s kind=%s%s validate=%s" % (inp.get("socid"), inp.get("kind", "customer"),
|
||||||
|
(" ref_supplier=%s" % inp["ref_supplier"]) if inp.get("ref_supplier") else "", bool(inp.get("validate"))))
|
||||||
|
for ln in inp.get("lines", []):
|
||||||
|
print(" - %r qty=%s pu_ht=%s tva=%s%% [%s]" % (ln.get("desc", ""), ln.get("qty", 1),
|
||||||
|
ln.get("price_ht", ln.get("subprice")), ln.get("tva", ln.get("tva_tx", 20)), ln.get("type", "service")))
|
||||||
|
elif t == "creditnote":
|
||||||
|
print(" socid=%s source_invoice=%s validate=%s" % (inp.get("socid"), inp.get("source_invoice"), bool(inp.get("validate"))))
|
||||||
|
for ln in inp.get("lines", []):
|
||||||
|
print(" - %r qty=%s pu_ht=%s tva=%s%%" % (ln.get("desc", ""), ln.get("qty", 1),
|
||||||
|
ln.get("price_ht", ln.get("subprice")), ln.get("tva", ln.get("tva_tx", 20))))
|
||||||
|
elif t == "payment":
|
||||||
|
print(" invoice=%s mode=%s account=%s %s" % (inp.get("invoice_id"), inp.get("mode", "VIR"),
|
||||||
|
inp.get("account_id"), ("amount=%s" % inp["amount"]) if inp.get("amount") else "(full)"))
|
||||||
|
print("\nNext:")
|
||||||
|
print(" promote-apply.sh <manifest> --target sandbox # rehearse the replay (safe)")
|
||||||
|
print(" promote-apply.sh <manifest> --target prod # WRITES PROD — needs DOLIBARR_PROD_WRITE_KEY")
|
||||||
|
print(" # + ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD")
|
||||||
|
PY
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
# thirdparty-create.sh fournisseur.json
|
# thirdparty-create.sh fournisseur.json
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
W="${SCRIPT_DIR}/dol-write.sh"
|
W="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}"
|
||||||
|
|
||||||
SRC="${1:-}"
|
SRC="${1:-}"
|
||||||
if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi
|
if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ COMMANDS
|
|||||||
creditnote Create an avoir (credit note) of a source invoice
|
creditnote Create an avoir (credit note) of a source invoice
|
||||||
write <METHOD> <path> [body] Raw host-guarded write
|
write <METHOD> <path> [body] Raw host-guarded write
|
||||||
|
|
||||||
|
promote Replay a reviewed change-set sandbox -> prod (ADR-0003)
|
||||||
|
plan <manifest.json> Human-readable review of the change-set
|
||||||
|
apply <manifest.json> [--target sandbox|prod] Replay it (prod is key+confirm gated)
|
||||||
|
|
||||||
whoami GET /users/info — confirm auth
|
whoami GET /users/info — confirm auth
|
||||||
ping GET /status — liveness + Dolibarr version
|
ping GET /status — liveness + Dolibarr version
|
||||||
curl <path> Raw read-only curl through dol-curl.sh
|
curl <path> Raw read-only curl through dol-curl.sh
|
||||||
@@ -306,6 +310,31 @@ EOF
|
|||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
promote)
|
||||||
|
sub="${1:-help}"; shift || true
|
||||||
|
case "${sub}" in
|
||||||
|
plan) exec "${SKILLS}/dolibarr-sandbox-write/scripts/promote-plan.sh" "$@" ;;
|
||||||
|
apply) exec "${SKILLS}/dolibarr-sandbox-write/scripts/promote-apply.sh" "$@" ;;
|
||||||
|
help|-h|--help)
|
||||||
|
cat <<'EOF'
|
||||||
|
arcodange promote — replay a reviewed sandbox change-set onto a target.
|
||||||
|
|
||||||
|
A manifest is a JSON array of write ops with symbolic refs (@name) instead of
|
||||||
|
ids, so the same file replays on sandbox or prod (an invoice refs @tp1, its
|
||||||
|
just-created thirdparty). See dolibarr-sandbox-write/examples/promote-manifest.json.
|
||||||
|
|
||||||
|
plan <manifest.json> Human-readable review of the change-set
|
||||||
|
apply <manifest.json> [--target sandbox|prod]
|
||||||
|
Replay it (sandbox = rehearse; prod = real)
|
||||||
|
|
||||||
|
PROD apply is gated: requires DOLIBARR_PROD_WRITE_KEY + ARCO_PROMOTE_CONFIRM=
|
||||||
|
I-UNDERSTAND-THIS-WRITES-PROD in the environment (the prod key is never stored).
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
*) echo "arcodange promote: unknown subcommand '${sub}' (try 'arcodange promote help')" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
|
||||||
whoami)
|
whoami)
|
||||||
exec "${DOLC}" /users/info
|
exec "${DOLC}" /users/info
|
||||||
;;
|
;;
|
||||||
|
|||||||
Reference in New Issue
Block a user