gendesign/tradein-mvp/browser/Dockerfile
bot-backend 25bebadca5
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m9s
CI / openapi-codegen-check (pull_request) Successful in 2m23s
CI / backend-tests (pull_request) Successful in 18m6s
chore(deps): три места, где сборка не воспроизводится — лок-призрак, глоб и неприпинованный сайдкар
Ничего не апгрейдит. Убирает три способа собрать образ не тем, что в репозитории.

1. tradein-mvp/backend/uv.lock — удалён (файл был untracked, в .gitignore
   с рождения). Мера — uv-workspace, сборка берёт КОРНЕВОЙ tradein-mvp/uv.lock
   (`context: ./tradein-mvp`, `COPY pyproject.toml uv.lock ./` + `uv sync
   --frozen`). Лок, созданный `uv lock` из backend/, не читает никто, и он тихо
   разошёлся с рабочим: на 2026-08-29 в нём pillow 12.2.0, starlette 1.0.1,
   python-multipart 0.0.29, pydantic-settings 2.14.1, weasyprint 68.1 — пять
   пакетов с открытыми advisory, которых в реальной сборке Меры нет вообще.
   Именно он подмешал пять фантомных строк в OSV-скан этой серии.
   Строка .gitignore остаётся (второй лок не нужен), но теперь с объяснением
   почему — иначе следующий читатель снимет ignore и закоммитит призрак.

2. backend/Dockerfile — `COPY pyproject.toml uv.lock* ./` + `if [ -f uv.lock ];
   then uv sync --frozen ...; else uv sync ...; fi` → без глоба и без фолбэка.
   Глоб + фолбэк означали: пропал лок — сборка не падает, а молча переключается
   на резолв «свежайшее из диапазонов pyproject». Образ собрался бы с версиями,
   которых никто не видел ни в одном PR. Теперь пропажа лока роняет COPY.

3. tradein-mvp/browser/Dockerfile — `pip install "camoufox[geoip]" aiohttp`
   без единого пина. У сайдкара нет лока вообще, так что любая пересборка (в том
   числе на несвязанном коммите) тянула свежайший camoufox, а с ним другой
   playwright — под который НЕ написан sed-патч coreBundle.js в том же файле.
   Запинено по факту прод-контейнера tradein-browser: camoufox 0.5.5,
   playwright 1.60.0, aiohttp 3.14.3. playwright явно, хотя и транзитивный
   (camoufox 0.5.5 → playwright<1.61): патч завязан на конкретную сборку драйвера.

   Там же переписан комментарий «Апгрейд playwright невозможен — camoufox 0.4.11
   pinned»: неверны обе половины. camoufox не был запинен ни на что, а 0.4.11
   в проде не стоит с неизвестно каких пор — контейнер сейчас несёт 0.5.5 и
   playwright 1.60.0.

Версии сняты с живого прод-контейнера, наличие на PyPI проверено.
2026-08-29 14:01:14 +03:00

86 lines
5.4 KiB
Docker
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# tradein-browser — camoufox Playwright WS server (изолированный контейнер Firefox).
# Backend подключается к этому контейнеру через playwright.firefox.connect.
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# Firefox runtime libs + ca-certificates для TLS
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
libgtk-3-0 \
libasound2 \
libdbus-glib-1-2 \
libx11-xcb1 \
libxtst6 \
ca-certificates \
&& useradd --create-home --uid 1000 app
WORKDIR /app
# Пины, а не плавающие версии: единственный лок этого сайдкара — вот эти строки.
# Без них любая пересборка образа (в том числе автоматическая, на не связанном
# коммите) втягивала свежайший camoufox — а вместе с ним другой playwright, под
# который написан sed-патч coreBundle.js ниже. Версии сняты с работающего
# прод-контейнера tradein-browser (2026-08-29): camoufox 0.5.5, playwright
# 1.60.0, aiohttp 3.14.3. playwright запинен явно, хотя и приходит транзитивно
# (camoufox 0.5.5 требует playwright<1.61) — патч завязан на конкретную сборку
# драйвера, диапазон тут не годится.
RUN pip install --no-cache-dir \
"camoufox[geoip]==0.5.5" \
"playwright==1.60.0" \
"aiohttp==3.14.3"
# ── #915/#918: null-guard + ВАЛИДНЫЕ дефолты для pageError.location (playwright 1.60.0) ─
# Firefox/juggler-бэкенд иногда отдаёт uncaught JS-ошибку страницы (реклама /
# cross-origin скрипты / anti-bot challenge на avito) БЕЗ поля location.
# playwright при доставке pageError-события собирает
# { url: pageError.location.url, line: ...lineNumber, column: ...columnNumber }
# (coreBundle.js, 2 места: BrowserContextDispatcher + trace event).
#
# Двухслойный баг (оба рвут protocol-соединение → "Connection closed while reading
# from the driver" / TargetClosedError → server.py 500 + relaunch браузера):
# 1. без guard → `pageError.location.url` бросает
# `TypeError: Cannot read properties of undefined (reading 'url')`;
# 2. ОДНОГО optional-chaining (`?.url`) НЕДОСТАТОЧНО (это была дыра #918):
# url станет undefined, а protocol-валидатор `tString` требует string →
# `ValidationError: location.url: expected string, got undefined`.
# Поэтому даём валидные дефолты: url → '', line/column → 0 — проходят валидатор,
# событие доставляется без краша. Профит: browser возвращает HTML challenge-
# страницы вместо краша → pipeline видит firewall и штатно ротирует IP, а не
# уходит в crash-storm (подтверждено прод-дебагом локального стека 2026-05-31).
#
# Апгрейд playwright не заблокирован, но связан: camoufox тянет playwright сам
# (0.5.5 → playwright<1.61), поэтому поднимать его можно только вместе с
# camoufox и только проверив, что паттерны sed ниже ещё есть в coreBundle.js.
# Сборка падает (exit 1), если паттерн не найден — это и есть сигнал, что bump
# сломал патч.
RUN PW_BUNDLE="$(python -c 'import os,playwright;print(os.path.join(os.path.dirname(playwright.__file__),"driver","package","lib","coreBundle.js"))')" \
&& sed -i -E "s/pageError\.location\.url,/pageError.location?.url ?? '',/g" "$PW_BUNDLE" \
&& sed -i -E 's/pageError\.location\.lineNumber,/pageError.location?.lineNumber ?? 0,/g' "$PW_BUNDLE" \
&& sed -i -E 's/pageError\.location\.columnNumber/pageError.location?.columnNumber ?? 0/g' "$PW_BUNDLE" \
&& N="$(grep -cF "pageError.location?.url ?? ''" "$PW_BUNDLE")" \
&& echo "tradein-browser: patched pageError.location?.url ?? '' sites: $N" \
&& [ "$N" -ge 2 ]
COPY server.py ./server.py
# camoufox fetch качает (а) Firefox-сборку и (б) GeoLite2 mmdb (geoip=True).
# Firefox идёт в $HOME/.cache → ENV HOME=/home/app кладёт его в кэш app-юзера,
# куда рантайм его ищет (фикс #899). НО mmdb пишется в site-packages пакета
# camoufox (root-owned после pip install) — под app это PermissionError на
# GeoLite2-City.mmdb. Поэтому fetch выполняем под ROOT (пишет и mmdb, и —
# благодаря ENV HOME — Firefox в /home/app/.cache), затем chown'им Firefox-кэш
# на app. mmdb остаётся root-owned в site-packages (world-readable → app читает
# на рантайме). Verified реальной сборкой образа 2026-05-31.
ENV HOME=/home/app
RUN python -m camoufox fetch && chown -R app:app /home/app/.cache
USER app
EXPOSE 3000
CMD ["python", "server.py"]