Files
Odoo-Modules/scripts/verify_service_booking.sh
gsinghpal a10b7425f7 fix(scripts): asset-compile gate — odoo shell needs --no-http (port 8069 held by live app)
The compile gate's 'odoo shell' tried to bind 8069 (the running app holds it) and
died with 'Address already in use' before compiling, false-failing the gate. Add
--no-http --http-port=0 --gevent-port=0 (same as the test run) so the shell loads
the registry and force-compiles the bundles without binding a port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:57:38 -04:00

191 lines
11 KiB
Bash
Executable File

#!/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
# Scope to THIS feature's test classes — the broad /fusion_claims tag also runs
# pre-existing dashboard/wizard tests that fail in this prod-config runner
# (CLAUDE.md fusion_repairs note: post_install trips on a pre-existing module),
# which is unrelated to this feature. Override TEST_TAGS to widen if desired.
TEST_TAGS="${TEST_TAGS:-/fusion_tasks:TestTaskTz,/fusion_claims:TestServiceRate,/fusion_claims:TestServiceBooking}"
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. ORPHANED-FK CLEANUP (clone only) -----------
# westin-v19 has orphaned rows under VALIDATED FKs (deleted taxes, companies,
# journals, ...). A plain pg_dump|psql clone cannot rebuild a validating FK over
# orphans, so the clone is MISSING those FKs; Odoo's check_foreign_keys then
# re-adds them and fails (e.g. payslip_tags_table.res_company_id=3,
# account_payment_method_line.journal_id=35). Generate an orphan-delete for EVERY
# single-column FK that exists on PROD (read-only SELECT on prod) and apply it to
# the clone. The clone is a throwaway; prod is never modified.
# (CLAUDE.md orphan-FK gotcha, generalised beyond the tax tables.)
c "Orphaned-FK cleanup (clone only) — general sweep from prod's FK definitions"
FKSQL="/tmp/svcbook_fkclean_${STAMP}.sql"
printf '%s\n' '\set ON_ERROR_STOP off' > "$FKSQL"
dexec -e PGPASSWORD="$PGPW" "$DBC" psql -U "$PGUSER" -d "$PROD_DB" -t -A -c "SELECT format('DELETE FROM %I a WHERE a.%I IS NOT NULL AND NOT EXISTS (SELECT 1 FROM %I b WHERE b.%I = a.%I);', src.relname, srcatt.attname, tgt.relname, tgtatt.attname, srcatt.attname) FROM pg_constraint con JOIN pg_class src ON src.oid=con.conrelid JOIN pg_namespace ns ON ns.oid=src.relnamespace AND ns.nspname='public' JOIN pg_class tgt ON tgt.oid=con.confrelid JOIN pg_attribute srcatt ON srcatt.attrelid=con.conrelid AND srcatt.attnum=con.conkey[1] JOIN pg_attribute tgtatt ON tgtatt.attrelid=con.confrelid AND tgtatt.attnum=con.confkey[1] WHERE con.contype='f' AND array_length(con.conkey,1)=1;" >> "$FKSQL" 2>>"$LOG" || true
dexec -i -e PGPASSWORD="$PGPW" "$DBC" psql -U "$PGUSER" -d "$CLONE_DB" < "$FKSQL" >>"$LOG" 2>&1 || true
ok "Orphan FKs cleared on clone (general sweep, $(grep -c '^DELETE' "$FKSQL" 2>/dev/null || echo 0) FK relations)."
# ----------------------------- 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
# --test-enable forces http_spawn() even with --no-http (Odoo 19), so the test
# run binds 8069 (held by the live app) and dies with "Address already in use".
# --http-port=0 --gevent-port=0 makes it pick ephemeral ports. (CLAUDE.md gotcha.)
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 --http-port=0 --gevent-port=0 $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
# Asset-bundle compile check: a broken SCSS/SASS breaks the ENTIRE
# web.assets_backend bundle (the whole backend UI for every user), and `-u` does
# NOT compile it — Odoo compiles assets lazily at request time. Force-compile
# both bundles here so a stylesheet error fails the gate BEFORE prod, not after.
# (CLAUDE.md asset cache-busting #3.)
if [[ "${TESTS_OK:-0}" == "1" ]]; then
c "Compile asset bundles on clone (catches SCSS errors)"
echo "env['ir.qweb']._get_asset_bundle('web.assets_backend').css(); env['ir.qweb']._get_asset_bundle('web.assets_web_dark').css(); print('ASSETS_COMPILED_OK')" \
| dexec -i "$APP" odoo shell -d "$CLONE_DB" --db_host db --db_port 5432 --db_user "$PGUSER" --db_password "$PGPW" --addons-path="$ADDONS_PATH" --no-http --http-port=0 --gevent-port=0 >>"$LOG" 2>&1 || true
if grep -q ASSETS_COMPILED_OK "$LOG"; then
ok "Asset bundles compiled OK"
else
TESTS_OK=0; err "ASSET COMPILE FAILED — see $LOG"; grep -iE 'error|scss|sass|Traceback' "$LOG" | tail -25 || true
fi
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