fix(deploy): stop docker compose exec -T from draining the heredoc

Symptom observed twice (migrations 012 and 013): deploy-erosi.sh ran the
migration loop's first iteration (the `CREATE TABLE IF NOT EXISTS
_applied_migrations` step) then silently skipped every file in
supabase/migrations/*.sql without printing a single skip/apply line. Had
to `psql -f <new-migration>` by hand on ambrosio after every deploy.

Root cause: the deploy uses `ssh host bash -s << 'REMOTE' ... REMOTE` so
the outer bash on the remote reads its own script from stdin. Any
`docker compose exec -T db psql ...` inside that script — particularly
inside a command substitution like `already=$(docker compose exec -T ...)`
— inherits that stdin and consumes it, eating the rest of the heredoc.

Fix: pass `</dev/null` (or the migration file for the actual `apply`
step) to every `docker compose exec -T` call so docker gets an empty /
file-scoped stdin instead of the parent's heredoc.

Verified on ambrosio: deploy now prints all 13 `skip ... (applied)` lines
as expected, and will apply new migrations going forward without manual
intervention.

- CLAUDE.md: gotcha #20 documenting the pattern; applies to any
  heredoc-delivered remote script using `docker compose exec -T` or
  `kubectl exec`.
This commit is contained in:
2026-04-15 00:31:00 +02:00
parent 2294a31942
commit 3d85d0a50e
2 changed files with 14 additions and 3 deletions

View File

@@ -110,6 +110,8 @@ Both are flagged in `plan/fase-7-collective-flow-tests.md` as future UI follow-u
19. **GoTrue v2.158.1 inserts OIDC-provisioned users with empty-string `role` and `aud`.** For users created via the external provider path (Keycloak → GoTrue), `auth.users.role` and `auth.users.aud` are left as `''` instead of the expected `'authenticated'`. The JWT minted from that row carries the empty role, and PostgREST's per-request `SET LOCAL role = $role_claim` then fails with `role "" does not exist` — every REST call errors out before policies or RPCs even run. Dev never surfaced this because `seed.sql` hardcodes both columns. Migration `013_auth_users_role_default.sql` backfills existing rows and installs a BEFORE INSERT trigger on `auth.users` that fills empty/NULL role + aud with `'authenticated'`. Users in existing browser sessions must log out + back in once for a fresh JWT after the migration is applied. 19. **GoTrue v2.158.1 inserts OIDC-provisioned users with empty-string `role` and `aud`.** For users created via the external provider path (Keycloak → GoTrue), `auth.users.role` and `auth.users.aud` are left as `''` instead of the expected `'authenticated'`. The JWT minted from that row carries the empty role, and PostgREST's per-request `SET LOCAL role = $role_claim` then fails with `role "" does not exist` — every REST call errors out before policies or RPCs even run. Dev never surfaced this because `seed.sql` hardcodes both columns. Migration `013_auth_users_role_default.sql` backfills existing rows and installs a BEFORE INSERT trigger on `auth.users` that fills empty/NULL role + aud with `'authenticated'`. Users in existing browser sessions must log out + back in once for a fresh JWT after the migration is applied.
20. **`docker compose exec -T` without explicit stdin drains the surrounding heredoc.** When a script is delivered over SSH as `ssh host bash -s << 'REMOTE' ... REMOTE`, the outer bash reads its script from stdin. Any `docker compose exec -T <service> <cmd>` inside that script — especially inside a command substitution like `x=$(docker compose exec -T db psql -c '...')` — inherits and consumes that same stdin, swallowing the rest of the heredoc. Symptom: the first iteration of a loop runs, every following iteration is silently skipped. Caused `infra/scripts/deploy-erosi.sh` to silently skip migrations 012 and 013 on prod until fixed (had to apply manually after every deploy). **Fix:** redirect stdin per call with `</dev/null` (or the file being piped in). Applies to any heredoc-delivered remote script using `docker compose exec -T` or `kubectl exec`.
### Fase 6+ — production deploy to ambrosio (2026-04-14) ### Fase 6+ — production deploy to ambrosio (2026-04-14)
Live URLs: **https://erosi.oier.ovh** (app + Supabase API, single-domain) + **https://auth.oier.ovh** (Keycloak). OVH VPS, Ubuntu 24, Docker 29. Live URLs: **https://erosi.oier.ovh** (app + Supabase API, single-domain) + **https://auth.oier.ovh** (Keycloak). OVH VPS, Ubuntu 24, Docker 29.

View File

@@ -116,13 +116,21 @@ for i in $(seq 1 60); do
done done
echo "--- Applying migrations" echo "--- Applying migrations"
# Every `docker compose exec -T` call below pipes in `</dev/null` (or the
# migration file for the apply step). This matters because the outer bash
# is reading its own script from stdin (`ssh ... bash -s << REMOTE`); a
# `docker compose exec -T` without its own stdin source drains the heredoc,
# silently eating the remainder of the script — observed as "first iteration
# runs, every subsequent migration vanishes from the log".
docker compose --env-file .env -f infra/docker-compose.erosi.yml exec -T db \ docker compose --env-file .env -f infra/docker-compose.erosi.yml exec -T db \
psql -U postgres -d postgres -c "CREATE TABLE IF NOT EXISTS public._applied_migrations (filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ DEFAULT now());" psql -U postgres -d postgres </dev/null \
-c "CREATE TABLE IF NOT EXISTS public._applied_migrations (filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ DEFAULT now());"
for f in supabase/migrations/*.sql; do for f in supabase/migrations/*.sql; do
fn=$(basename "$f") fn=$(basename "$f")
already=$(docker compose --env-file .env -f infra/docker-compose.erosi.yml exec -T db \ already=$(docker compose --env-file .env -f infra/docker-compose.erosi.yml exec -T db \
psql -U postgres -d postgres -tAq -c "SELECT count(*) FROM public._applied_migrations WHERE filename='$fn'") psql -U postgres -d postgres -tAq </dev/null \
-c "SELECT count(*) FROM public._applied_migrations WHERE filename='$fn'")
if [ "${already:-0}" -gt 0 ]; then if [ "${already:-0}" -gt 0 ]; then
echo " skip $fn (applied)" echo " skip $fn (applied)"
continue continue
@@ -131,7 +139,8 @@ for f in supabase/migrations/*.sql; do
if docker compose --env-file .env -f infra/docker-compose.erosi.yml exec -T db \ if docker compose --env-file .env -f infra/docker-compose.erosi.yml exec -T db \
psql -U postgres -d postgres -v ON_ERROR_STOP=1 -q < "$f"; then psql -U postgres -d postgres -v ON_ERROR_STOP=1 -q < "$f"; then
docker compose --env-file .env -f infra/docker-compose.erosi.yml exec -T db \ docker compose --env-file .env -f infra/docker-compose.erosi.yml exec -T db \
psql -U postgres -d postgres -c "INSERT INTO public._applied_migrations (filename) VALUES ('$fn') ON CONFLICT DO NOTHING;" psql -U postgres -d postgres </dev/null \
-c "INSERT INTO public._applied_migrations (filename) VALUES ('$fn') ON CONFLICT DO NOTHING;"
echo "ok" echo "ok"
else else
echo "FAIL"; exit 1 echo "FAIL"; exit 1