docs(service-booking): add spec, plans, mockup, and clone-verify script
Kickoff brief, design spec, both implementation plans (rates foundation + booking wizard), the UI mockup, and the hands-off Westin clone-verify/deploy script for the Technician Service Booking feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
163
scripts/verify_service_booking.sh
Executable file
163
scripts/verify_service_booking.sh
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# verify_service_booking.sh
|
||||
#
|
||||
# HANDS-OFF clone-verify (and, behind a flag, deploy) for the Technician
|
||||
# Service Booking feature (fusion_tasks + fusion_claims) on the Westin host.
|
||||
#
|
||||
# It automates the documented "Westin Prod — Clone-Verify / Deploy" procedure
|
||||
# (see Odoo-Modules/CLAUDE.md) end-to-end:
|
||||
# 1. refresh the branch checkout on the host
|
||||
# 2. clone the live DB to a throwaway test DB (+ the orphaned-tax-FK cleanup)
|
||||
# 3. stage the branch modules into the _test shadow prefix (prod untouched)
|
||||
# 4. install/upgrade + run the module tests on the clone (PASS/FAIL gate)
|
||||
# 5. (only with --deploy AND green tests) back up, swap, -u prod, restart
|
||||
# 6. always clean up the clone + staging
|
||||
#
|
||||
# Verify-only by default. Deploy is OFF unless you pass --deploy.
|
||||
#
|
||||
# RUN IT ON THE WESTIN HOST:
|
||||
# ssh odoo-westin # (via your usual jump)
|
||||
# # one-time: put the branch on the host, e.g.
|
||||
# # git clone <remote> /opt/odoo/staging/Odoo-Modules (or scp the tree there)
|
||||
# bash verify_service_booking.sh # verify only
|
||||
# DEPLOY=1 bash verify_service_booking.sh --deploy # verify, then deploy on green
|
||||
#
|
||||
# Prereq: the feature code must already be implemented on $BRANCH. This script
|
||||
# does NOT write code — it verifies/deploys what's on the branch.
|
||||
# =============================================================================
|
||||
set -Eeuo pipefail
|
||||
|
||||
# ----------------------------- CONFIG (env-overridable) ----------------------
|
||||
APP="${APP:-odoo-dev-app}" # Odoo app container
|
||||
DBC="${DBC:-odoo-dev-db}" # Postgres container
|
||||
PROD_DB="${PROD_DB:-westin-v19}" # live DB (cloned, never -u'd unless --deploy)
|
||||
CLONE_DB="${CLONE_DB:-westin-v19-svcbook}" # throwaway verify DB
|
||||
PGPW="${PGPW:-DevSecure2025!}"
|
||||
PGUSER="${PGUSER:-odoo}"
|
||||
|
||||
MODULES="${MODULES:-fusion_tasks,fusion_claims}" # comma list for -u
|
||||
TEST_TAGS="${TEST_TAGS:-/fusion_tasks,/fusion_claims}"
|
||||
MOD_DIRS=(fusion_tasks fusion_claims) # dirs to stage/deploy
|
||||
|
||||
BRANCH="${BRANCH:-claude/technician-service-booking}"
|
||||
SRC="${SRC:-/opt/odoo/staging/Odoo-Modules}" # host checkout of the branch
|
||||
STAGE="${STAGE:-/opt/odoo/custom-addons/_test}" # shadow prefix (CLAUDE.md)
|
||||
LIVE_ADDONS="${LIVE_ADDONS:-/opt/odoo/custom-addons}"
|
||||
BACKUPS="${BACKUPS:-/opt/odoo/backups}" # OUTSIDE the addons path
|
||||
CONF="${CONF:-/etc/odoo/odoo.conf}"
|
||||
|
||||
# _test prefix SHADOWS prod (first match wins); deps load from the real path.
|
||||
ADDONS_PATH="/usr/lib/python3/dist-packages/odoo/addons,/usr/lib/python3/dist-packages/addons,${STAGE},/mnt/enterprise-addons,/mnt/extra-addons"
|
||||
LIVE_ADDONS_PATH="/usr/lib/python3/dist-packages/odoo/addons,/usr/lib/python3/dist-packages/addons,/mnt/enterprise-addons,/mnt/extra-addons"
|
||||
|
||||
DEPLOY=0
|
||||
[[ "${1:-}" == "--deploy" || "${DEPLOY:-0}" == "1" ]] && DEPLOY=1
|
||||
STAMP="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo manual)"
|
||||
LOG="/tmp/svcbook_verify_${STAMP}.log"
|
||||
|
||||
c() { printf '\n\033[1;36m== %s ==\033[0m\n' "$*"; } # section
|
||||
ok() { printf '\033[1;32m%s\033[0m\n' "$*"; }
|
||||
err() { printf '\033[1;31m%s\033[0m\n' "$*" >&2; }
|
||||
dexec() { docker exec "$@"; }
|
||||
psql_clone() { dexec -e PGPASSWORD="$PGPW" "$DBC" psql -U "$PGUSER" -d "$CLONE_DB" -v ON_ERROR_STOP=1 "$@"; }
|
||||
|
||||
# ----------------------------- CLEANUP TRAP ----------------------------------
|
||||
cleanup() {
|
||||
c "Cleanup"
|
||||
rm -rf "${STAGE:?}/"* 2>/dev/null || true
|
||||
dexec -e PGPASSWORD="$PGPW" "$DBC" dropdb -U "$PGUSER" --if-exists "$CLONE_DB" 2>/dev/null || true
|
||||
ok "Dropped clone $CLONE_DB, cleared $STAGE"
|
||||
}
|
||||
trap 'err "FAILED (line $LINENO). See $LOG"; cleanup' ERR
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
# ----------------------------- 0. SANITY -------------------------------------
|
||||
c "Pre-flight"
|
||||
docker ps --format '{{.Names}}' | grep -qx "$APP" || { err "container $APP not running"; exit 1; }
|
||||
docker ps --format '{{.Names}}' | grep -qx "$DBC" || { err "container $DBC not running"; exit 1; }
|
||||
if [[ -d "$SRC/.git" ]]; then
|
||||
git -C "$SRC" fetch --quiet origin "$BRANCH" && git -C "$SRC" checkout --quiet "$BRANCH" && git -C "$SRC" pull --quiet --ff-only origin "$BRANCH"
|
||||
ok "Branch $BRANCH @ $(git -C "$SRC" rev-parse --short HEAD)"
|
||||
else
|
||||
err "WARNING: $SRC is not a git checkout — staging whatever is on disk there."
|
||||
fi
|
||||
for m in "${MOD_DIRS[@]}"; do [[ -d "$SRC/$m" ]] || { err "missing module dir: $SRC/$m"; exit 1; }; done
|
||||
|
||||
# ----------------------------- 1. CLONE THE DB -------------------------------
|
||||
c "Clone $PROD_DB -> $CLONE_DB (read-only on prod)"
|
||||
dexec -e PGPASSWORD="$PGPW" "$DBC" sh -c \
|
||||
"dropdb -U $PGUSER --if-exists $CLONE_DB; createdb -U $PGUSER -O $PGUSER $CLONE_DB && pg_dump -U $PGUSER $PROD_DB | psql -U $PGUSER -q -d $CLONE_DB" \
|
||||
>>"$LOG" 2>&1
|
||||
ok "Cloned."
|
||||
|
||||
# ----------------------------- 2. ORPHAN-TAX-FK CLEANUP (clone only) ---------
|
||||
# westin-v19 has ~3300 orphaned tax m2m rows under validated FKs; a plain
|
||||
# pg_dump|psql clone can't rebuild the validating FK over them -> Odoo fails to
|
||||
# load the registry. Safe to delete ON THE CLONE only. (CLAUDE.md gotcha.)
|
||||
c "Orphaned-tax-FK cleanup (clone only)"
|
||||
psql_clone -c "DELETE FROM product_taxes_rel WHERE tax_id NOT IN (SELECT id FROM account_tax);" >>"$LOG" 2>&1 || true
|
||||
psql_clone -c "DELETE FROM product_supplier_taxes_rel WHERE tax_id NOT IN (SELECT id FROM account_tax);" >>"$LOG" 2>&1 || true
|
||||
# sweep any other %_rel table carrying a tax_id column
|
||||
psql_clone -t -A -c "SELECT table_name FROM information_schema.columns WHERE column_name='tax_id' AND table_name LIKE '%\\_rel';" 2>/dev/null \
|
||||
| while read -r t; do [[ -n "$t" ]] && psql_clone -c "DELETE FROM ${t} WHERE tax_id NOT IN (SELECT id FROM account_tax);" >>"$LOG" 2>&1 || true; done
|
||||
ok "Orphan FKs cleared on clone."
|
||||
|
||||
# ----------------------------- 3. STAGE MODULES (shadow) ---------------------
|
||||
c "Stage modules into $STAGE (shadows prod, prod files untouched)"
|
||||
mkdir -p "$STAGE"
|
||||
for m in "${MOD_DIRS[@]}"; do rm -rf "${STAGE:?}/$m"; cp -r "$SRC/$m" "$STAGE/$m"; done
|
||||
ok "Staged: ${MOD_DIRS[*]}"
|
||||
|
||||
# ----------------------------- 4. INSTALL/UPGRADE + TESTS (clone) -----------
|
||||
# Test-runner gotchas on the prod-config container (CLAUDE.md / fusion_repairs):
|
||||
# --test-enable SILENTLY SKIPS without --workers 0; log_level=warn hides test
|
||||
# output -> add --log-level=test. The EXIT CODE is authoritative.
|
||||
run_odoo() { # $1 = extra args
|
||||
dexec "$APP" odoo -d "$CLONE_DB" \
|
||||
--db_host db --db_port 5432 --db_user "$PGUSER" --db_password "$PGPW" \
|
||||
--addons-path="$ADDONS_PATH" --stop-after-init --no-http $1
|
||||
}
|
||||
|
||||
c "Install/upgrade on clone (catches install/render errors)"
|
||||
if run_odoo "-u $MODULES" >>"$LOG" 2>&1; then ok "Upgrade OK"; else err "UPGRADE FAILED — see $LOG"; tail -40 "$LOG"; exit 2; fi
|
||||
|
||||
c "Run module tests on clone"
|
||||
if run_odoo "-u $MODULES --test-enable --test-tags $TEST_TAGS --workers 0 --log-level=test" >>"$LOG" 2>&1; then
|
||||
TESTS_OK=1; ok "TESTS PASSED"
|
||||
else
|
||||
TESTS_OK=0; err "TESTS FAILED (exit $?)"; grep -E 'FAIL|ERROR|Traceback' "$LOG" | tail -40 || true
|
||||
fi
|
||||
|
||||
echo
|
||||
c "VERIFY RESULT"
|
||||
if [[ "${TESTS_OK:-0}" == "1" ]]; then ok "✅ Clone-verify GREEN (full log: $LOG)"; else err "❌ Clone-verify RED (full log: $LOG)"; fi
|
||||
|
||||
# ----------------------------- 5. DEPLOY (gated) -----------------------------
|
||||
if [[ "$DEPLOY" == "1" ]]; then
|
||||
if [[ "${TESTS_OK:-0}" != "1" ]]; then err "Not deploying — tests are red."; exit 3; fi
|
||||
c "DEPLOY to $PROD_DB (tests green)"
|
||||
mkdir -p "$BACKUPS"
|
||||
# DB backup (-Fc) + module dir backups OUTSIDE the addons path
|
||||
dexec -e PGPASSWORD="$PGPW" "$DBC" pg_dump -Fc -U "$PGUSER" "$PROD_DB" > "$BACKUPS/${PROD_DB}_${STAMP}.dump"
|
||||
for m in "${MOD_DIRS[@]}"; do [[ -d "$LIVE_ADDONS/$m" ]] && cp -r "$LIVE_ADDONS/$m" "$BACKUPS/${m}_${STAMP}"; done
|
||||
ok "Backed up DB + module dirs to $BACKUPS"
|
||||
# swap branch modules into the real addons
|
||||
for m in "${MOD_DIRS[@]}"; do rm -rf "${LIVE_ADDONS:?}/$m"; cp -r "$SRC/$m" "$LIVE_ADDONS/$m"; done
|
||||
# -u prod, gated on exit 0
|
||||
if dexec "$APP" odoo -d "$PROD_DB" --db_host db --db_port 5432 --db_user "$PGUSER" --db_password "$PGPW" \
|
||||
--addons-path="$LIVE_ADDONS_PATH" -u "$MODULES" --stop-after-init --no-http >>"$LOG" 2>&1; then
|
||||
dexec -e PGPASSWORD="$PGPW" "$DBC" psql -U "$PGUSER" -d "$PROD_DB" -c \
|
||||
"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';" >>"$LOG" 2>&1 || true
|
||||
docker restart "$APP" >>"$LOG" 2>&1
|
||||
ok "🚀 Deployed + assets cleared + $APP restarted."
|
||||
else
|
||||
err "PROD -u FAILED — restoring module dirs, NOT restarting."
|
||||
for m in "${MOD_DIRS[@]}"; do rm -rf "${LIVE_ADDONS:?}/$m"; [[ -d "$BACKUPS/${m}_${STAMP}" ]] && cp -r "$BACKUPS/${m}_${STAMP}" "$LIVE_ADDONS/$m"; done
|
||||
err "Restore the DB if needed: pg_restore from $BACKUPS/${PROD_DB}_${STAMP}.dump"
|
||||
exit 4
|
||||
fi
|
||||
else
|
||||
echo
|
||||
ok "Verify-only run (no deploy). Re-run with --deploy to ship on green."
|
||||
fi
|
||||
Reference in New Issue
Block a user