-- Set gendesign_reader password from env. -- Applied by .forgejo/workflows/deploy-tradein.yml after migrations: -- psql -v pw="$TRADEIN_READER_PASSWORD" -f ops/db-bootstrap/set_gendesign_reader_password.sql -- cwd на деплое = /opt/gendesign/tradein-mvp (путь относительный). -- -- Idempotent: ALTER если роль существует, NOTICE и продолжает если нет -- (migration 101_gendesign_reader_role.sql может ещё не примениться на первом деплое). -- Пароль НИКОГДА не хранится в этом файле или в git — только имя переменной. -- -- Format %L экранирует пароль как SQL string literal — безопасно даже с кавычками. -- -- psql variable substitution (:'pw') НЕ интерполируется внутри dollar-quoted -- блока ($$...$$) — это правило psql, не bug. Поэтому password передаём в DO -- через сессионный GUC (set_config), который psql интерполирует ВНЕ dollar -- quote, и читаем внутри через current_setting(). -- Reference incident: deploy 2026-05-24 (post-merge PR #503) упал на -- "syntax error at or near ':'" — :'pw' дошёл до сервера as literal вместо -- интерполяции при использовании -c вместо stdin + файл. -- -- Источник переменной pw: TRADEIN_READER_PASSWORD из tradein-mvp/.env.runtime. -- Передаётся через: psql -v "pw=${TRADEIN_READER_PASSWORD}" -- -- ⚠️ `set_config(name, value, is_local) -> text` ВОЗВРАЩАЕТ установленное -- значение. Без `\o /dev/null` psql напечатал бы пароль на stdout → leak в -- Forgejo Actions deploy logs (retained, visible всем с repo read access). -- Поэтому оборачиваем оба set_config вызова в `\o /dev/null` / `\o` brackets -- — output mutes только для этих строк, NOTICE-сообщения из DO block -- (idempotency signal) остаются видимыми. -- -- Rollback path: НЕ revert этого файла (вернёт сломанный :'pw' внутри $$). -- Корректный rollback — unset TRADEIN_READER_PASSWORD в -- /opt/gendesign/tradein-mvp/.env.runtime на VPS, deploy-tradein.yml -- тогда пропустит этот шаг полностью. \o /dev/null SELECT set_config('app.reader_pw', :'pw', false); \o DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gendesign_reader') THEN EXECUTE format('ALTER ROLE gendesign_reader WITH PASSWORD %L', current_setting('app.reader_pw')); RAISE NOTICE 'gendesign_reader password set'; ELSE RAISE NOTICE 'gendesign_reader role missing — migration 101_gendesign_reader_role.sql not applied yet'; END IF; END $$; -- Clear GUC after use (defense-in-depth — не оставляем password в session state -- даже на short connection). Same \o trick — set_config return value is empty -- string here, но всё равно лишний row в stdout не нужен. \o /dev/null SELECT set_config('app.reader_pw', '', false); \o