openapi-codegen-check был RED на каждом PR: CI шаг гонял bare `npx prettier`
(резолвится в плавающий latest), а committed api-types.ts был отформатирован
старым prettier (≤3.6.2, union-типы multi-line). 3.9.0 переносит union-wrapping
на single-line → CI-регенерация всегда давала diff → gate падал.
Чиним детерминизмом одной версии на обоих путях:
- frontend/package.json: добавлен prettier@3.9.0 (exact) в devDependencies
+ обновлён package-lock.json.
- CI openapi-codegen-check: bare `npx prettier` → ./node_modules/.bin/prettier
(project-local pinned, не плавает).
- .pre-commit-config.yaml: prettier hook additional_dependencies=["prettier@3.9.0"]
— тот же движок, что в CI (alpha.8-обёртка байт-в-байт == prettier 3.9.0).
- api-types.ts перегенерирован prettier 3.9.0 (union-типы single-line).
Backend OpenAPI-схема не менялась — только формат. CI-регенерация теперь
байт-в-байт совпадает с committed-файлом и с выводом pre-commit hook.
При быстрых мержах dorny/paths-filter без explicit base диффает per-merge-commit
(parent of merge), а не накопленно от последнего деплоя. Итог: backend-мерж #1829
не попадает в diff frontend-мержа #1830 → build-backend skipped, образа нет.
Решение (host-file mechanism):
- deploy-джоба пишет `echo "$GITHUB_SHA" > /opt/gendesign/.tradein-deployed-sha`
в самом конце SSH-скрипта — только после успешного up+миграций.
- changes-джоба читает файл по SSH (те же DEPLOY_* секреты), валидирует SHA как
40-char hex + git merge-base --is-ancestor HEAD, передаёт в `base:` paths-filter.
- Если файл отсутствует / SHA не предок HEAD / SSH недоступен → FAIL-SAFE:
step `set-all` эмитит все outputs=true, paths-filter пропускается,
все образы собираются (better safe than sorry).
Почему host-file, не git-ref: в workflows не используется `gitea.token` /
`GITHUB_TOKEN` с contents:write — добавлять новое разрешение ради ref push
ненужный риск. DEPLOY_* секреты уже есть, SSH уже есть.
Не изменено: concurrency.cancel-in-progress=false, workflow_dispatch форс,
логика build/deploy/migrations.
backend-tests и openapi-codegen-check тратят ~90% времени не на работу
(дамп схемы + pytest), а на пересборку окружения с нуля каждый прогон.
Доминанта — 'uv sync --frozen' тянет весь geo/WeasyPrint-стек заново, т.к.
~/.cache/uv не кэшировался между прогонами (npm уже кэширован setup-node).
Изменения:
- Cache uv (~/.cache/uv) через actions/cache в обоих job, ключ по backend/uv.lock,
restore-keys для частичного хита. continue-on-error → сбой cache-бэкенда
раннера не ломает gate.
- openapi-codegen-check: 'uv sync --frozen --no-dev' — job только импортирует
app.main для дампа OpenAPI, dev-группа (pytest/ruff/coverage) не нужна.
Dockerfile тоже --no-dev, поэтому импорт гарантирован.
Refs #1709
Каждый коммит в ветку с открытым PR триггерил ДВА прогона ci.yml на один
SHA: push-событие (github.ref=refs/heads/<branch>) и pull_request-событие
(github.ref=refs/pull/<N>/merge). Разный github.ref → разные concurrency-
группы → прогоны не отменяли друг друга → 2x нагрузка на и так дефицитные
2 раннера (CI-шторм 2026-06-17, деплой tradein #1699 простоял 17 мин).
В bot-пайплайне каждый коммит идёт через PR, поэтому pull_request гейтит
его полностью; push-прогон был чистым дублем. Оставляю только pull_request:
теперь github.ref стабилен на весь PR и concurrency cancel-in-progress
корректно отменяет superseded-прогоны при новом пуше.
Refs #1709
- Создан tradein-mvp/ops/db-bootstrap/set_gendesign_reader_password.sql:
set_config GUC + format('%L') + \o /dev/null — зеркальный паттерн
ops/db-bootstrap/set_tradein_fdw_password.sql. Пароль не попадает
в stdout/logs (leak-protected).
- deploy-tradein.yml: шаг bootstrap переписан на stdin-redirect
(< ops/db-bootstrap/set_gendesign_reader_password.sql) вместо сломанного
-c "ALTER ROLE ... PASSWORD :'TRADEIN_READER_PASSWORD'", которое давало
syntax error at ':' (psql не интерполирует :'var' внутри -c строки).
- newbuilding_crossload.py ON CONFLICT DO UPDATE: yandex_jk_id и
cian_internal_house_id → COALESCE(EXCLUDED.col, table.col), чтобы
cian-запись не затирала yandex_jk_id NULL'ом и наоборот.
- Тест test_upsert_sql_coalesce_external_ids проверяет наличие COALESCE
в DO UPDATE секции для обоих id.
Причина: docker restart tradein-backend (при каждом API-деплое) убивал бегущие
sweep'ы avito/cian/rosreestr — они могут идти часами, и их прерывание середине
батча создавало zombie-runs и пропуски данных.
Что сделано:
- scheduler_main.py — standalone entrypoint для tradein-scraper; SIGTERM/SIGINT
отменяют scheduler_loop чисто; GlitchTip init без Starlette/FastAPI интеграций.
- docker-compose.prod.yml — новый сервис scraper (тот же image, команда python -m
app.scheduler_main, только tradein-net); SCHEDULER_ENABLE=false в backend.
- deploy-tradein.yml — селективный up -d --no-deps: scraper пересоздаётся только
при изменениях scraper/infra путей (SCRAPER_CHANGED), не при каждом backend-деплое.
- tests/test_scheduler_main.py — импорт, kill-switch _should_run(), чистая отмена _run().
Mirror the LLM_ENABLED wiring for OWN_DEVELOPER_IDS (own-portfolio
cannibalization, #1169): forward vars.OWN_DEVELOPER_IDS into the SSH deploy
(env block + envs list) and write it to backend/.env.runtime ONLY when
non-empty (conditional, like OPENAI_API_KEY/LLM_ENABLED). Non-sensitive
(public companyGroup ids) → actions variable. Unset → cannibalization keeps
the proxy (dormant). No literal id in the workflow (refs only).
Captures in the pipeline the value already set live on prod via .env.runtime
(OWN_DEVELOPER_IDS=6208 = PRINZIP, verified against domrf_kn → 28 projects).
Owner must set Forgejo Variable OWN_DEVELOPER_IDS=6208 for full pipeline
management; meanwhile the .env.runtime line holds (survives git reset).
Refs #1169
Forward OPENAI_API_KEY (Forgejo secret) and LLM_ENABLED (Forgejo actions
variable) into the SSH deploy and write them to backend/.env.runtime ONLY
when non-empty, so the LLM chat (#960/#957) stays dormant
(llm_enabled=False, openai_api_key=None) until BOTH are set in Forgejo.
Mirrors the OBJECTIVE_API_KEY wiring (env block + envs list + idempotent
.env.runtime grep-sed-or-printf), but guarded by [ -n ] so an unset
secret/var writes nothing. No literal key (refs only). Compose header
documents both as optional. .env.runtime is shared by backend+worker+beat.
Refs #960
Run the merged frontend vitest suite on every frontend-touching PR so the
tests don't rot. Mirrors the #1032 backend gate: per-job dorny/paths-filter
(new `frontend` output, `frontend/**`), node:20 + npm cache on the lockfile,
`npm ci --legacy-peer-deps --no-audit --no-fund` (exact frontend/Dockerfile
flags), then `npm run test`. Additive — backend-tests job unchanged; no
top-level paths filter so frontend-only PRs already trigger the workflow.
Making frontend-tests a required check is a follow-up in the Forgejo UI.
Refs #999.
Forgejo runs only .forgejo/workflows/* — the .github pytest gate never
executed here, so backend changes merged + deployed untested (live bug
#994 district 500 shipped uncaught). Add a real gate: ruff check + the full
backend pytest suite (1687 passed) on PRs to main and feature-branch pushes,
scoped to backend/** + data/sql/** via paths-filter. Mock-only lane (no
postgres service): the one real-DB test self-skips via a connectivity probe;
WeasyPrint PDF tests RUN thanks to installed libpango. uv via official
installer (not setup-uv, PEP 668), uv sync --frozen against the committed lock,
TESTING=1 to activate the test-mode RBAC bypass.
NOTE: making this a HARD required check + having the auto-merge bot consult
check status needs Forgejo branch-protection config (human action) — until
then the gate is visible but advisory.
Refs #944.
The new browser service references ghcr.io/lekss361/gendesign-tradein-browser;
without a build job the deploy's compose-up would fail pulling a missing image.
Adds build-browser job (mirrors build-backend, context ./tradein-mvp/browser),
a browser path-filter (tradein-mvp/browser/**), IMAGE_BROWSER env, and wires
build-browser into deploy needs + the no-failure gate.
Refs #905
🔴 Critical (3):
1. cleanup-stale-claims.sh: export CUTOFF + STALE_HOURS
Без `export` Python heredoc child process получал "0" вместо реального
cutoff → cron всегда видел все issues как fresh → ничего не освобождал.
Plus added abort при CUTOFF=0 в Python heredoc для defense-in-depth.
2. cleanup-stale-claims.sh: datetime.UTC → datetime.timezone.utc
datetime.UTC требует Python 3.11+. Forgejo runner ubuntu-latest имеет 3.10.
datetime.timezone.utc works в 3.8+.
3. cleanup-stale-claims.sh: invert label order — DELETE wip FIRST, потом
POST ready. Если step 2 (add ready) fails — issue в neutral state без
status/*, менее опасно чем оба status/ready+status/wip одновременно.
🟠 + 🟡 + 🟢 (5):
4. Added pagination loop (defensive cap 10 pages × 50 = 500 issues), но
реализована через temp file + Python parsing (не bash JSON concat).
5. Removed dead code: iso_to_epoch() shell helper не использовался.
6. _autonomous_pickup.md: fix broken `Out-Null | if (-not $?)` pattern.
GetEnvironmentVariable НЕ throws при missing — возвращает $null. $? у
Get* всегда $true. Заменено на прямую проверку результата.
7. stale-claims.yml: добавлен concurrency group чтобы overlapping runs
не stomp'ились (хотя 5-min timeout делает overlap unlikely, defensive).
8. Documented updated_at != heartbeat assumption в header script'а.
Не сделано (low/optional):
- Setup script tokens в memory (UX trade-off — не critical)
- Token suffix в console output (минор info leak в shell history)
Refs: PR #610 review by review-bot (sha=297eb29, verdict=changes)
#11: slash-commands и _autonomous_pickup.md теперь читают persistent User-scope
env vars напрямую (FORGEJO_TOKEN_<ROLE>) вместо file-based из ~/.claude/secrets/.
Один раз: scripts/setup-bot-env.ps1 (запускается user'ом локально, кладёт
токены из vault в Windows User registry). После этого окна grab credentials
через [System.Environment]::GetEnvironmentVariable() — никаких файлов.
Все 5 work-as-*.md обновлены (analyst/backend/frontend/reviewer/qa).
#12: stale-claim cleanup — cron каждые 30 минут.
- scripts/cleanup-stale-claims.sh — bash, parses Forgejo API + Python для JSON
- .forgejo/workflows/stale-claims.yml — cron schedule `*/30 * * * *`
Освобождает issues застрявшие в status/wip >4h:
- assignees=[]
- +status/ready -status/wip
- comment "stale claim released, worker likely crashed"
Использует secret FORGEJO_BOT_QA_TOKEN (минимум прав = write:issue).
Setup: secret нужно завести в Forgejo Actions UI после merge —
git.gendsgn.ru/lekss361/gendesign/-/settings/actions/secrets.
Refs: PRs #608, #609. Closes TODO с PR #608 (stale-claim cron).
Replaces tradein.cad_buildings snapshot with live postgres_fdw foreign table reading gendesign.v_tradein_cad_buildings. Fixes /trade-in/api/v1/geocode/reverse 500 (Nominatim ban) and address_not_geocoded for cadastre addresses (e.g. Хохрякова 81).
Security (deep-review fixes):
- 100_tradein_fdw_role.sql: passwordless CREATE ROLE; password set by deploy.yml ALTER ROLE bootstrap reading GENDESIGN_FDW_PASSWORD from backend/.env.runtime (via psql :'pw' var → format %L — injection-safe).
- core/fdw.py: regex whitelist [A-Za-z0-9_-]{32,256} on password, ValueError without echoing value, try/rollback on commit.
- 060_postgres_fdw_extension.sql: connect_timeout='3' on FOREIGN SERVER + ALTER ADD/SET fallback.
- geocoder.py: _cadastral_forward_sync / _cadastral_reverse_sync wrapped in asyncio.to_thread.
- 100_*.sql: REVOKE ALL ON ALL TABLES/SEQUENCES/FUNCTIONS IN SCHEMA public; only GRANT SELECT on v_tradein_cad_buildings.
- pg_user_mappings query handles PUBLIC mapping (usename IS NULL).
Tests: 3 SQL-injection guards on ensure_fdw_user_mapping + rewritten cadastral suite.
502 fix: Next.js standalone server.js биндился на HOSTNAME=<container-id>
вместо 0.0.0.0 → Caddy не мог достучаться до tradein-frontend:3000.
Теперь HOSTNAME=0.0.0.0 и в Dockerfile (runner ENV), и в compose.
API routing fix: фронт собирался без NEXT_PUBLIC_API_BASE_URL, fetch шёл
на same-origin /api/* (главный gendesign-backend) вместо /trade-in/api/*.
Добавлен build-arg в Dockerfile + workflow.
Раньше deploy.yml делал `docker compose exec caddy caddy reload` — это
только reload Caddyfile, но НЕ пересоздаёт container. Когда в
docker-compose.prod.yml добавлялся новый volume mount (например
./preview:/srv/preview:ro в PR #268), он не появлялся в running container
до manual `docker compose up -d --force-recreate caddy` через SSH.
Теперь deploy всегда вызывает force-recreate caddy после image pull —
идемпотентно, +~5 сек к deploy time, гарантирует что новые mounts +
Caddyfile блоки активны.
Problem
- GlitchTip Issues = 0 за всё время, хотя backend + frontend SDK интегрированы (PR #207, #208).
- Старый Sentry.io продолжает получать события — user видит уведомления оттуда.
Root cause
- frontend/Dockerfile не имеет ARG NEXT_PUBLIC_GLITCHTIP_DSN → `npm run build` бьёт
с пустым env var → Next.js инлайнит undefined → SDK init guard `if (dsn)` skips.
Chrome-devtools check на prod bundle подтвердил: ни в одном из 9 chunks DSN-строка
не запечена; `i.env.NEXT_PUBLIC_GLITCHTIP_DSN` evaluates to undefined.
- .forgejo/workflows/deploy.yml build-frontend не передавал build-args.
- На VPS backend/.env.runtime содержит legacy SENTRY_DSN=...@sentry.io/...
config.py:_promote_legacy_sentry_dsn слепо промоутит его в glitchtip_dsn → SDK
шлёт в чужой Sentry. GLITCHTIP_DSN там не задан.
- deploy.yml SSH-скрипт никогда не редактировал SENTRY_DSN/GLITCHTIP_DSN в .env.runtime.
Solution
1. frontend/Dockerfile: ARG NEXT_PUBLIC_GLITCHTIP_DSN + NEXT_PUBLIC_ENVIRONMENT
с пустыми defaults, ENV-mirror перед `npm run build`. Локальный build без
build-args работает по-прежнему (no-op DSN).
2. .forgejo/workflows/deploy.yml:
- build-frontend: `build-args` передаёт `secrets.GLITCHTIP_FRONTEND_DSN`
+ NEXT_PUBLIC_ENVIRONMENT=production. Инвалидирует cache → frontend
image пересоберётся (expected).
- deploy step: GLITCHTIP_BACKEND_DSN через secret, в SSH-скрипте:
a) `sed -i '/^SENTRY_DSN=/d' backend/.env.runtime` — снести legacy
b) upsert GLITCHTIP_DSN (sed/printf) тем же паттерном что SENTRY_RELEASE
c) `compose up -d --force-recreate --no-deps backend worker beat` —
обычный `up -d` не перечитывает env_file без image change.
3. backend/app/core/config.py: _promote_legacy_sentry_dsn ужесточён —
принимает SENTRY_DSN только если host == errors.gendsgn.ru. Для других
URLs (sentry.io) выдаёт UserWarning и НЕ промоутит. Anti-regression на
случай если SENTRY_DSN снова окажется в .env.runtime после ручного
вмешательства.
Required Forgejo secrets (Settings → Actions → Secrets)
- GLITCHTIP_BACKEND_DSN = https://3d6e291003e142458957490c83559867@errors.gendsgn.ru/1
- GLITCHTIP_FRONTEND_DSN = https://5d7bc85e300c4e80a8554ccc818ff56d@errors.gendsgn.ru/2
DSNs публичны (видны в browser bundle) — secrets ради build-time injection,
не для конфиденциальности. Если secrets не заданы → deploy succeeds, SDK
no-op, без регрессии.
Test plan
- Verify Forgejo deploy.yml зелёный после merge
- chrome-devtools: открыть gendsgn.ru → search bundle на DSN string → должна
быть запечена строка errors.gendsgn.ru/2
- Trigger frontend error → POST к errors.gendsgn.ru/api/2/envelope/
- Backend: curl на endpoint вызывающий 500 → событие в GlitchTip backend project
- GlitchTip dashboard https://errors.gendsgn.ru/gendesign/issues — Issues > 0
References
- vault: meta/00_credentials.md (DSNs + incident notes 2026-05-16)
- vault: decisions/Dec_GlitchTip_Frontend_Sentry_SDK.md (env contract)
- vault: fixes/fixes-MOC.md (#204 backend SDK init)