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
Ничего не апгрейдит. Убирает три способа собрать образ не тем, что в репозитории. 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 проверено.
86 lines
5.4 KiB
Docker
86 lines
5.4 KiB
Docker
# 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"]
|