optimize dockerfile and add base image and russian trusted certs

This commit is contained in:
Денис Комсиков 2026-09-25 18:18:51 +03:00
parent feea544273
commit 4fabeadfbc
9 changed files with 185 additions and 58 deletions

View file

@ -42,6 +42,7 @@ on:
- 'build_linux.sh'
- 'build_windows.ps1'
- 'Dockerfile'
- 'docker/**'
- 'scripts/**'
- 'devtools/**'
- 'packaging/**'
@ -532,7 +533,9 @@ jobs:
- uses: actions/checkout@v4
- name: Build Docker image
id: docker_build
run: docker build -t ouroboros-web:test .
run: |
docker build -f docker/Dockerfile.base -t ouroboros-base:local .
docker build -t ouroboros-web:test .
- uses: ./.github/actions/setup-python-env
id: setup_python
- name: Install UI smoke browser binaries
@ -561,7 +564,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t ouroboros-web:test .
run: |
docker build -f docker/Dockerfile.base -t ouroboros-base:local .
docker build -t ouroboros-web:test .
- name: Run portable detail tests in Docker
run: |
docker run --rm --entrypoint sh -e OUROBOROS_EXPECT_HEADLESS_SHELL=1 ouroboros-web:test -c \

View file

@ -1,39 +1,31 @@
# Ouroboros — Docker image for web UI runtime
# syntax=docker/dockerfile:1
# Ouroboros — application image for web UI runtime
# Usage:
# docker build -f docker/Dockerfile.base -t ouroboros-base:local .
# docker build -t ouroboros-web .
# docker run --rm -p 8765:8765 ouroboros-web
FROM ghcr.io/astral-sh/uv:0.12.1 AS uv
FROM python:3.10-slim
ARG OUROBOROS_BASE_IMAGE=ouroboros-base:local
FROM ${OUROBOROS_BASE_IMAGE}
COPY --from=uv /uv /uvx /bin/
# System dependencies (git + Playwright/Chromium native libs installed via playwright install-deps)
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*
# Working directory
ENV APP_HOME=/app
# Application environment
ENV APP_HOME=/app \
UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PROJECT_ENVIRONMENT=/opt/venv \
PATH="/opt/venv/bin:$PATH"
WORKDIR ${APP_HOME}
# Resolve only from the reviewed lock. Keeping dependencies in their own layer
# lets source edits reuse the expensive Python package and browser downloads.
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
PATH="/app/.venv/bin:$PATH"
# Install locked project dependencies separately so source edits reuse this layer.
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-dev --extra browser --no-install-project
# Install all Playwright native system dependencies for Chromium/WebKit (authoritative list from Playwright)
RUN python3 -m playwright install-deps chromium webkit
# Install Playwright Chromium/WebKit browser binaries so browser tools work out of the box
RUN PLAYWRIGHT_BROWSERS_PATH=0 python3 -m playwright install chromium webkit
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --extra browser --no-install-project
# Copy application
COPY . .
RUN uv sync --locked --no-dev --extra browser --no-editable
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --extra browser --no-editable
# Default environment
ENV OUROBOROS_SERVER_HOST=0.0.0.0 \

View file

@ -346,6 +346,7 @@ uv export --locked --no-dev --extra browser --no-emit-project --no-hashes --no-a
### Docker
```bash
docker build -f docker/Dockerfile.base -t ouroboros-base:local .
docker build -t ouroboros-web .
docker run --rm -p 8765:8765 \
-e OUROBOROS_NETWORK_PASSWORD='choose-a-password' \
@ -354,7 +355,10 @@ docker run --rm -p 8765:8765 \
ouroboros-web
```
Docker runs the web runtime, not the native desktop shell. It bundles Chromium and WebKit support; use [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) for network and container policy.
The base image contains only runtime prerequisites: Playwright, Chromium/WebKit,
their system libraries, Git, and trusted certificates from `docker/certs`.
The application image owns the project environment and locked dependencies. Docker runs
the web runtime, not the native desktop shell; use [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) for network and container policy.
### Release tag prerequisite

28
docker/Dockerfile.base Normal file
View file

@ -0,0 +1,28 @@
# syntax=docker/dockerfile:1
# Ouroboros runtime-dependency base. Rebuild when this file or docker/certs changes:
# docker build -f docker/Dockerfile.base -t ouroboros-base:local .
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.12.1
ARG PYTHON_IMAGE=python:3.10-slim
FROM ${UV_IMAGE} AS uv
FROM ${PYTHON_IMAGE}
ARG DEBIAN_FRONTEND=noninteractive
ARG PLAYWRIGHT_VERSION=1.62.0
COPY --from=uv /uv /uvx /bin/
COPY docker/certs/ /usr/local/share/ca-certificates/
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
UV_SYSTEM_CERTS=true
RUN --mount=type=cache,target=/root/.cache/uv \
--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 ca-certificates git \
&& update-ca-certificates \
&& uv pip install --system "playwright==${PLAYWRIGHT_VERSION}" \
&& python3 -m playwright install-deps chromium webkit \
&& python3 -m playwright install chromium webkit

View file

@ -0,0 +1,4 @@
**
!docker/
!docker/certs/
!docker/certs/*.crt

View file

@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
-----END CERTIFICATE-----

View file

@ -0,0 +1,41 @@
-----BEGIN CERTIFICATE-----
MIIHQjCCBSqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
Q0EwHhcNMjIwMzAyMTEyNTE5WhcNMjcwMzA2MTEyNTE5WjBvMQswCQYDVQQGEwJS
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9YPqBKOk19NFymrE
wehzrhBEgT2atLezpduB24mQ7CiOa/HVpFCDRZzdxqlh8drku408/tTmWzlNH/br
HuQhZ/miWKOf35lpKzjyBd6TPM23uAfJvEOQ2/dnKGGJbsUo1/udKSvxQwVHpVv3
S80OlluKfhWPDEXQpgyFqIzPoxIQTLZ0deirZwMVHarZ5u8HqHetRuAtmO2ZDGQn
vVOJYAjls+Hiueq7Lj7Oce7CQsTwVZeP+XQx28PAaEZ3y6sQEt6rL06ddpSdoTMp
BnCqTbxW+eWMyjkIn6t9GBtUV45yB1EkHNnj2Ex4GwCiN9T84QQjKSr+8f0psGrZ
vPbCbQAwNFJjisLixnjlGPLKa5vOmNwIh/LAyUW5DjpkCx004LPDuqPpFsKXNKpa
L2Dm6uc0x4Jo5m+gUTVORB6hOSzWnWDj2GWfomLzzyjG81DRGFBpco/O93zecsIN
3SL2Ysjpq1zdoS01CMYxie//9zWvYwzI25/OZigtnpCIrcd2j1Y6dMUFQAzAtHE+
qsXflSL8HIS+IJEFIQobLlYhHkoE3avgNx5jlu+OLYe0dF0Ykx1PGNjbwqvTX37R
Cn32NMjlotW2QcGEZhDKj+3urZizp5xdTPZitA+aEjZM/Ni71VOdiOP0igbw6asZ
2fxdozZ1TnSSYNYvNATwthNmZysCAwEAAaOCAeUwggHhMBIGA1UdEwEB/wQIMAYB
Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTR4XENCy2BTm6KSo9MI7NM
XqtpCzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzCBxwYIKwYBBQUH
AQEEgbowgbcwOwYIKwYBBQUHMAKGL2h0dHA6Ly9yb3N0ZWxlY29tLnJ1L2NkcC9y
b290Y2Ffc3NsX3JzYTIwMjIuY3J0MDsGCCsGAQUFBzAChi9odHRwOi8vY29tcGFu
eS5ydC5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNydDA7BggrBgEFBQcwAoYv
aHR0cDovL3JlZXN0ci1wa2kucnUvY2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQw
gbAGA1UdHwSBqDCBpTA1oDOgMYYvaHR0cDovL3Jvc3RlbGVjb20ucnUvY2RwL3Jv
b3RjYV9zc2xfcnNhMjAyMi5jcmwwNaAzoDGGL2h0dHA6Ly9jb21wYW55LnJ0LnJ1
L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMDWgM6Axhi9odHRwOi8vcmVlc3Ry
LXBraS5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG9w0BAQsF
AAOCAgEARBVzZls79AdiSCpar15dA5Hr/rrT4WbrOfzlpI+xrLeRPrUG6eUWIW4v
Sui1yx3iqGLCjPcKb+HOTwoRMbI6ytP/ndp3TlYua2advYBEhSvjs+4vDZNwXr/D
anbwIWdurZmViQRBDFebpkvnIvru/RpWud/5r624Wp8voZMRtj/cm6aI9LtvBfT9
cfzhOaexI/99c14dyiuk1+6QhdwKaCRTc1mdfNQmnfWNRbfWhWBlK3h4GGE9JK33
Gk8ZS8DMrkdAh0xby4xAQ/mSWAfWrBmfzlOqGyoB1U47WTOeqNbWkkoAP2ys94+s
Jg4NTkiDVtXRF6nr6fYi0bSOvOFg0IQrMXO2Y8gyg9ARdPJwKtvWX8VPADCYMiWH
h4n8bZokIrImVKLDQKHY4jCsND2HHdJfnrdL2YJw1qFskNO4cSNmZydw0Wkgjv9k
F+KxqrDKlB8MZu2Hclph6v/CZ0fQ9YuE8/lsHZ0Qc2HyiSMnvjgK5fDc3TD4fa8F
E8gMNurM+kV8PT8LNIM+4Zs+LKEV8nqRWBaxkIVJGekkVKO8xDBOG/aN62AZKHOe
GcyIdu7yNMMRihGVZCYr8rYiJoKiOzDqOkPkLOPdhtVlgnhowzHDxMHND/E2WA5p
ZHuNM/m0TXt2wTTPL7JH2YC0gPz/BvvSzjksgzU5rLbRyUKQkgU=
-----END CERTIFICATE-----

View file

@ -153,7 +153,7 @@ or Intent/Scope checklists are.
| 6 | New tool added? | `get_tools()` exports it, its schema description says WHEN to choose it (each profile receives its visible schema set every round, so the schema is the SSOT of the per-tool contract; `prompts/SYSTEM.md` is the cross-tool selection policy and mentions a tool only when the change alters that policy, never as a catalog entry; mechanism documentation lives in ARCHITECTURE/DEVELOPMENT), the handler signature matches the declared schema, and (if it mutates repo state) it is routed through the reviewed commit path rather than ad-hoc `run_command`. Also add an explicit entry in `ouroboros/safety.py::TOOL_POLICY` (`POLICY_SKIP` for trusted built-ins, `POLICY_CHECK` for opaque or outward-facing ones) — the `test_tool_policy_covers_all_builtin_tools` invariant will fail otherwise, and without an entry the tool falls through to `DEFAULT_POLICY = check` and pays a light-model LLM call per invocation. |
| 7 | Tests green before first `commit_reviewed`? | Run `pytest -x` on the narrowest relevant target(s) you can name before the first `preflight_review` / `commit_reviewed` attempt. Size gates no longer block locally: they live in the official-CI-only `size_ratchet` pytest lane (manifest exactness plus the pairwise base-vs-tip shrink-only transition), and local surfaces (`check_worktree_readiness`, `codebase_health`) surface the same `validate_size_ratchet` findings as "official CI will enforce" warnings. When a size warning appears — or a new `.py` file lands under `ouroboros/` or `supervisor/` — run `pytest tests/ -m size_ratchet` and `scripts/regenerate_size_ratchet.py` locally to preview and fix what official CI would reject. A red test suite before the first commit attempt has caused repeated $2-5 blocked-review cycles. |
| 8 | Adding a `README.md` version row? | BIBLE.md P9 hard cap: ≤ 2 major, ≤ 5 minor, ≤ 5 patch visible entries. Categories are mutually exclusive: major = `X.0.0` (minor=0, patch=0); minor = `X.Y.0` (patch=0, Y≠0); patch = all other `X.Y.Z` (Z≠0). Count existing rows in the category you are adding to. Easy check: `run_command(["python", "-c", "import sys; from ouroboros.tools.release_sync import check_history_limit; warns=check_history_limit(open('README.md').read()); print(warns or 'OK')"])` — if it prints warnings, trim the oldest row in the over-limit category **in the same edit** before committing. |
| 9 | Changing any of `build.sh`, `build_linux.sh`, `build_windows.ps1`, `Dockerfile`, or `ouroboros/tools/browser.py`? | Cross-surface doc sync is mandatory. Check ALL of: `README.md` Install section (Linux native-lib caveat), `README.md` Build section (per-platform instructions), `docs/ARCHITECTURE.md` browser tools paragraph, WebKit/mobile verification notes, and inline comments in the touched build script. Any one of these being stale has blocked review twice. Verify before staging. |
| 9 | Changing any of `build.sh`, `build_linux.sh`, `build_windows.ps1`, `Dockerfile`, `docker/Dockerfile.base`, or `ouroboros/tools/browser.py`? | Cross-surface doc sync is mandatory. Check ALL of: `README.md` Install section (Linux native-lib caveat), `README.md` Build section (per-platform instructions), `docs/ARCHITECTURE.md` browser tools paragraph, WebKit/mobile verification notes, and inline comments in the touched build script. Any one of these being stale has blocked review twice. Verify before staging. |
| 10 | Changing `ouroboros/tools/commit_gate.py`? | Coupled surfaces that MUST be updated atomically in the same commit: (a) `claude_advisory_review.py::get_tools()` tool description for `preflight_review` and `review_status`; (b) `claude_advisory_review.py::_next_step_guidance()` strings; (c) `docs/DEVELOPMENT.md` Review & Commit Protocol section; (d) the `prompts/SYSTEM.md` Self-Modification section IF the commit-gate rule it states changed. Missing any one has blocked review. |
| 11 | Changing VERSION + pyproject.toml? | Ordering matters: (1) write `VERSION`, `pyproject.toml`, `uv.lock`, `web/package.json` and `web/modules/api_types.js` first; (2) then write the `README.md` badge + changelog row + download links, both install pages' download links, and the `docs/ARCHITECTURE.md` header; (3) then run `pytest`. Never interleave — updating README before VERSION means `test_version_in_readme` will catch a stale badge. |
| 12 | Writing or editing any JS file under `web/modules/`? | New or changed static inline visual properties are blocked: inspect the diff for added/changed `style=""` markup and `.style.<property>` assignments, and use CSS classes/tokens plus `classList`/`hidden` instead. Unchanged legacy hits are debt, not a blocker. A dynamic measured value may update a narrowly named CSS custom property when that is the actual runtime data flow. |

View file

@ -544,28 +544,48 @@ class TestDockerignore:
class TestDockerfile:
"""Dockerfile must install Playwright Chromium/WebKit binaries so browser tools work
out of the box in the container without additional setup."""
"""The Docker base must bundle Playwright Chromium/WebKit for child images."""
def test_application_image_uses_dependency_base(self):
src = _read("Dockerfile")
assert "ARG OUROBOROS_BASE_IMAGE=ouroboros-base:local" in src
assert "FROM ${OUROBOROS_BASE_IMAGE}" in src
def test_application_owns_project_environment(self):
app = _read("Dockerfile")
base = _read("docker/Dockerfile.base")
assert "UV_PROJECT_ENVIRONMENT=/opt/venv" in app
assert 'PATH="/opt/venv/bin:$PATH"' in app
assert "uv sync --locked --no-dev --extra browser --no-install-project" in app
assert "UV_PROJECT_ENVIRONMENT" not in base
assert "uv sync" not in base
def test_base_installs_project_certificates(self):
src = _read("docker/Dockerfile.base")
assert "COPY docker/certs/ /usr/local/share/ca-certificates/" in src
assert "update-ca-certificates" in src
def test_playwright_install_chromium_present(self):
src = _read("Dockerfile")
src = _read("docker/Dockerfile.base")
assert "playwright install chromium webkit" in src, (
"Dockerfile must call 'playwright install chromium webkit' to bundle the browsers"
)
def test_playwright_browsers_path_zero_set(self):
src = _read("Dockerfile")
assert "PLAYWRIGHT_BROWSERS_PATH=0" in src, (
"Dockerfile must set PLAYWRIGHT_BROWSERS_PATH=0 so Chromium installs "
"inside the pip package tree (not into a user cache that won't survive "
"image layer boundaries)"
)
def test_playwright_uses_shared_browser_path(self):
src = _read("docker/Dockerfile.base")
assert "PLAYWRIGHT_BROWSERS_PATH=/ms-playwright" in src
def test_base_playwright_version_matches_lock(self):
src = _read("docker/Dockerfile.base")
match = re.search(r'\[\[package\]\]\nname = "playwright"\nversion = "([^"]+)"', _read("uv.lock"))
assert match is not None
assert f"PLAYWRIGHT_VERSION={match.group(1)}" in src
def test_playwright_install_deps_present(self):
"""Dockerfile must use 'playwright install-deps chromium webkit' (the authoritative
Playwright dependency resolver) rather than a hand-curated apt library list.
This ensures all runtime native libs required by Chromium/WebKit are present."""
src = _read("Dockerfile")
src = _read("docker/Dockerfile.base")
assert "playwright install-deps chromium webkit" in src, (
"Dockerfile must call 'playwright install-deps chromium webkit' to install all "
"native system libraries required by Chromium/WebKit via Playwright's authoritative "
@ -575,7 +595,7 @@ class TestDockerfile:
def test_install_deps_before_install_chromium(self):
"""Native system dependencies must be installed BEFORE the Chromium binary
is downloaded, so the binary can find its runtime libraries on first launch."""
src = _read("Dockerfile")
src = _read("docker/Dockerfile.base")
deps_pos = src.find("playwright install-deps chromium webkit")
src.find("playwright install chromium webkit")
# binary_pos must not match the install-deps line itself
@ -588,26 +608,26 @@ class TestDockerfile:
"playwright install-deps must appear BEFORE playwright install chromium webkit in Dockerfile"
)
def test_uv_sync_before_playwright_install_deps(self):
"""uv sync must appear BEFORE playwright install-deps chromium webkit — the
def test_playwright_package_before_playwright_install_deps(self):
"""uv pip must install Playwright BEFORE playwright install-deps — the
playwright Python package must be importable when install-deps runs."""
src = _read("Dockerfile")
sync_pos = src.find("uv sync")
src = _read("docker/Dockerfile.base")
install_pos = src.find("uv pip install --system")
deps_pos = src.find("playwright install-deps chromium webkit")
assert sync_pos != -1, "uv sync step not found in Dockerfile"
assert install_pos != -1, "Playwright package install not found in Dockerfile"
assert deps_pos != -1, "playwright install-deps chromium webkit not found in Dockerfile"
assert sync_pos < deps_pos, (
"uv sync must appear BEFORE playwright install-deps chromium webkit in Dockerfile "
f"(sync at char {sync_pos}, install-deps at {deps_pos})"
assert install_pos < deps_pos, (
"Playwright package must be installed before playwright install-deps "
f"(install at char {install_pos}, install-deps at {deps_pos})"
)
def test_uv_sync_before_all_playwright_invocations(self):
"""uv sync must appear BEFORE every ``python3 -m playwright ...`` invocation
def test_playwright_package_before_all_playwright_invocations(self):
"""uv pip must install Playwright before every ``python3 -m playwright`` invocation
in the Dockerfile — both ``install-deps`` and ``install chromium webkit``.
If *any* playwright invocation precedes dependency sync, ModuleNotFoundError occurs."""
src = _read("Dockerfile")
sync_pos = src.find("uv sync")
assert sync_pos != -1, "uv sync step not found in Dockerfile"
Otherwise the module invocation raises ModuleNotFoundError."""
src = _read("docker/Dockerfile.base")
install_pos = src.find("uv pip install --system")
assert install_pos != -1, "Playwright package install not found in Dockerfile"
import re as _re
playwright_invocations = [
@ -616,9 +636,9 @@ class TestDockerfile:
assert playwright_invocations, "No 'python3 -m playwright' invocations found in Dockerfile"
earliest_playwright = min(playwright_invocations)
assert sync_pos < earliest_playwright, (
"uv sync must appear BEFORE the earliest 'python3 -m playwright' invocation "
f"in the Dockerfile (sync at char {sync_pos}, earliest playwright at {earliest_playwright}). "
assert install_pos < earliest_playwright, (
"Playwright package install must appear before its first module invocation "
f"(install at char {install_pos}, earliest playwright at {earliest_playwright}). "
f"Found {len(playwright_invocations)} playwright invocation(s) at positions: "
f"{playwright_invocations}"
)