#!/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 /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