mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-02 19:58:46 +00:00
Docker: one self-contained image with the browsers above the lock copy
Fold the base/app split back into a single Dockerfile. The base tag was never published, so a plain `docker build -t ouroboros-web .` failed on every fresh machine, and a runtime `UV_PROJECT_ENVIRONMENT` let an agent's `uv sync` in a task workspace rewrite the runtime venv. What the split was for stays: the Playwright browsers pinned to the locked version are installed from an ephemeral uvx tool environment into `/ms-playwright` above `COPY pyproject.toml uv.lock` (every release rewrites the lock), and the BuildKit cache mounts keep the uv and apt caches out of the layers (4.82 GB -> 4.12 GB). The tag-only docker lanes stop forcing `PLAYWRIGHT_BROWSERS_PATH=0`, which pointed at an empty package tree and made them download browsers mid-test; the portable path-length test reads the shipped browser path. The Russian Trusted CA files leave the image: the next commit gives deployments an owner-side setting instead. Docs: ARCHITECTURE §8 Docker, README.
This commit is contained in:
parent
e856ec9d15
commit
9637d194c2
12 changed files with 111 additions and 226 deletions
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
|
|
@ -42,7 +42,6 @@ on:
|
|||
- 'build_linux.sh'
|
||||
- 'build_windows.ps1'
|
||||
- 'Dockerfile'
|
||||
- 'docker/**'
|
||||
- 'scripts/**'
|
||||
- 'devtools/**'
|
||||
- 'packaging/**'
|
||||
|
|
@ -533,9 +532,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: Build Docker image
|
||||
id: docker_build
|
||||
run: |
|
||||
docker build -f docker/Dockerfile.base -t ouroboros-base:local .
|
||||
docker build -t ouroboros-web:test .
|
||||
run: docker build -t ouroboros-web:test .
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
id: setup_python
|
||||
- name: Install UI smoke browser binaries
|
||||
|
|
@ -554,7 +551,7 @@ jobs:
|
|||
docker run --rm --entrypoint sh \
|
||||
-e OUROBOROS_EXPECT_BROWSER_ENGINES=chromium,webkit \
|
||||
ouroboros-web:test -c \
|
||||
"PLAYWRIGHT_BROWSERS_PATH=0 python -m pytest tests/test_browser_tools_smoke.py -m browser -q --tb=short"
|
||||
"python -m pytest tests/test_browser_tools_smoke.py -m browser -q --tb=short"
|
||||
|
||||
docker-portable-test:
|
||||
if: |
|
||||
|
|
@ -564,13 +561,11 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
docker build -f docker/Dockerfile.base -t ouroboros-base:local .
|
||||
docker build -t ouroboros-web:test .
|
||||
run: 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 \
|
||||
"PLAYWRIGHT_BROWSERS_PATH=0 python -m playwright install --only-shell chromium && python -m pytest tests/ -m portable_detail -q --tb=short"
|
||||
"python -m pytest tests/ -m portable_detail -q --tb=short"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Scheduled: the KEYLESS system_e2e scenario lane (tests/system_e2e/).
|
||||
|
|
|
|||
47
Dockerfile
47
Dockerfile
|
|
@ -1,23 +1,46 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Ouroboros — application image for web UI runtime
|
||||
# Ouroboros — Docker 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
|
||||
# The RUN --mount caches need BuildKit, Docker's default builder.
|
||||
|
||||
ARG OUROBOROS_BASE_IMAGE=ouroboros-base:local
|
||||
FROM ${OUROBOROS_BASE_IMAGE}
|
||||
FROM ghcr.io/astral-sh/uv:0.12.1 AS uv
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Application environment
|
||||
ENV APP_HOME=/app \
|
||||
COPY --from=uv /uv /uvx /bin/
|
||||
|
||||
# Browsers first, dependencies second, sources last: every release rewrites
|
||||
# pyproject.toml/uv.lock, so anything below the lock copy is rebuilt per
|
||||
# release while the apt packages and the Chromium/WebKit downloads above it
|
||||
# are reused. The Playwright pin must equal the locked version so the
|
||||
# downloaded browser revisions match the venv's driver
|
||||
# (tests/test_build_scripts.py::TestDockerfile). The installer runs from an
|
||||
# ephemeral uvx tool environment, so the image carries one Playwright: the
|
||||
# venv's. Browsers live in a shared path the runtime honors as-is
|
||||
# (ouroboros/tools/browser.py) — not inside the package tree, which the
|
||||
# per-release dependency layer would rebuild.
|
||||
ARG PLAYWRIGHT_VERSION=1.62.0
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_PROJECT_ENVIRONMENT=/opt/venv \
|
||||
PATH="/opt/venv/bin:$PATH"
|
||||
UV_COMPILE_BYTECODE=1
|
||||
|
||||
# System dependencies: git for the agent's own history and updates, plus
|
||||
# every Chromium/WebKit native library from Playwright's authoritative list.
|
||||
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 git \
|
||||
&& uvx --from "playwright==${PLAYWRIGHT_VERSION}" playwright install-deps chromium webkit \
|
||||
&& uvx --from "playwright==${PLAYWRIGHT_VERSION}" playwright install chromium webkit
|
||||
|
||||
# Working directory
|
||||
ENV APP_HOME=/app \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
WORKDIR ${APP_HOME}
|
||||
|
||||
# Install locked project dependencies separately so source edits reuse this layer.
|
||||
# Resolve only from the reviewed lock; the project itself is installed after
|
||||
# the source copy so source edits reuse this layer.
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --no-dev --extra browser --no-install-project
|
||||
|
|
|
|||
|
|
@ -346,7 +346,6 @@ 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' \
|
||||
|
|
@ -355,10 +354,9 @@ docker run --rm -p 8765:8765 \
|
|||
ouroboros-web
|
||||
```
|
||||
|
||||
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.
|
||||
Docker runs the web runtime, not the native desktop shell. The image bundles Chromium and WebKit for the locked
|
||||
Playwright version and needs Docker's default BuildKit builder; use [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) for network
|
||||
and container policy, including how to trust an extra CA (`OUROBOROS_EXTRA_CA_BUNDLE`).
|
||||
|
||||
### Release tag prerequisite
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
**
|
||||
!docker/
|
||||
!docker/certs/
|
||||
!docker/certs/*.crt
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
-----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-----
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
-----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-----
|
||||
|
|
@ -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`, `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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
|
|
|||
|
|
@ -60,4 +60,6 @@ Docker runs the web and server runtime without PyWebView. The image binds `0.0.0
|
|||
|
||||
The root `.dockerignore` filters `COPY . .`: secrets, host virtualenvs, `node_modules`, caches and runtime/review/operator state stay out of image layers; `.git`, `tests/` and sources stay in, because CI runs pytest inside the image.
|
||||
|
||||
The image is one self-contained `Dockerfile` built with Docker's default BuildKit builder: `python:3.10-slim` with the pinned `uv`, then git plus Playwright's native libraries and the Chromium/WebKit binaries for the Playwright version `uv.lock` pins, installed from an ephemeral `uvx` tool environment into `/ms-playwright` (the `PLAYWRIGHT_BROWSERS_PATH` the runtime honors as-is), then `uv sync --locked` into `/app/.venv`, then the sources. Browsers and apt packages sit above the lock copy because every release rewrites `pyproject.toml`/`uv.lock`, and `RUN --mount=type=cache` keeps the uv and apt caches out of the layers. `tests/test_build_scripts.py::TestDockerfile` pins the version match, the order, the shared browser path and the absence of a runtime `UV_PROJECT_ENVIRONMENT` (a baked one would let an agent's `uv sync` in a task workspace rewrite the runtime venv); the tag-only `docker-ui-smoke` and `docker-portable-test` lanes exercise the shipped browsers without a download. Trust anchors are not baked into the image: a deployment that needs an extra CA sets `OUROBOROS_EXTRA_CA_BUNDLE` (§7; `docs/DEPLOYMENT.md`).
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -544,104 +544,60 @@ class TestDockerignore:
|
|||
|
||||
|
||||
class TestDockerfile:
|
||||
"""The Docker base must bundle Playwright Chromium/WebKit for child images."""
|
||||
"""One self-contained Dockerfile ships Chromium/WebKit for the locked Playwright.
|
||||
|
||||
def test_application_image_uses_dependency_base(self):
|
||||
The pins below are the contract the tag-only ``docker-ui-smoke`` and
|
||||
``docker-portable-test`` lanes rely on: browsers land in the shared
|
||||
``/ms-playwright`` the runtime honors as-is, they are downloaded ABOVE the
|
||||
lock copy (every release rewrites ``pyproject.toml``/``uv.lock``), and CI
|
||||
exercises the image's own browsers instead of re-downloading them.
|
||||
"""
|
||||
|
||||
def test_build_is_self_contained(self):
|
||||
"""``docker build -t ouroboros-web .`` must not depend on a locally built base tag."""
|
||||
src = _read("Dockerfile")
|
||||
assert "ARG OUROBOROS_BASE_IMAGE=ouroboros-base:local" in src
|
||||
assert "FROM ${OUROBOROS_BASE_IMAGE}" in src
|
||||
assert "FROM python:3.10-slim" in src
|
||||
assert "FROM ${" not in src, "the image must not start from an unpublished local tag"
|
||||
|
||||
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("docker/Dockerfile.base")
|
||||
assert "playwright install chromium webkit" in src, (
|
||||
"Dockerfile must call 'playwright install chromium webkit' to bundle the browsers"
|
||||
)
|
||||
|
||||
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")
|
||||
def test_playwright_pin_matches_lock(self):
|
||||
src = _read("Dockerfile")
|
||||
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("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 "
|
||||
"dependency resolver"
|
||||
assert match is not None, "uv.lock must lock playwright"
|
||||
assert f"PLAYWRIGHT_VERSION={match.group(1)}" in src, (
|
||||
"Dockerfile must pin the browser installer to the locked Playwright version"
|
||||
)
|
||||
|
||||
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("docker/Dockerfile.base")
|
||||
def test_shared_browser_path_is_the_image_environment(self):
|
||||
src = _read("Dockerfile")
|
||||
assert "PLAYWRIGHT_BROWSERS_PATH=/ms-playwright" in src
|
||||
assert "PLAYWRIGHT_BROWSERS_PATH=0" not in src, (
|
||||
"browsers must not live in the package tree: the dependency layer is rebuilt on every release"
|
||||
)
|
||||
|
||||
def test_browsers_install_before_the_lock_copy(self):
|
||||
src = _read("Dockerfile")
|
||||
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
|
||||
# find the standalone 'playwright install chromium webkit' (not install-deps)
|
||||
import re as _re
|
||||
binary_match = _re.search(r"(?<!install-deps )playwright install chromium webkit", src)
|
||||
assert deps_pos != -1, "playwright install-deps chromium webkit not found in Dockerfile"
|
||||
assert binary_match is not None, "standalone playwright install chromium webkit not found in Dockerfile"
|
||||
assert deps_pos < binary_match.start(), (
|
||||
"playwright install-deps must appear BEFORE playwright install chromium webkit in Dockerfile"
|
||||
install_pos = src.find("playwright install chromium webkit")
|
||||
lock_pos = src.find("COPY pyproject.toml uv.lock")
|
||||
assert deps_pos != -1 and install_pos != -1 and lock_pos != -1
|
||||
assert deps_pos < install_pos < lock_pos, (
|
||||
"install-deps, then the browser download, then the lock copy — otherwise a release "
|
||||
f"bump re-downloads the browsers (deps {deps_pos}, install {install_pos}, lock {lock_pos})"
|
||||
)
|
||||
|
||||
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("docker/Dockerfile.base")
|
||||
install_pos = src.find("uv pip install --system")
|
||||
deps_pos = src.find("playwright install-deps chromium webkit")
|
||||
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 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_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``.
|
||||
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 = [
|
||||
m.start() for m in _re.finditer(r"python3 -m playwright", src)
|
||||
]
|
||||
assert playwright_invocations, "No 'python3 -m playwright' invocations found in Dockerfile"
|
||||
|
||||
earliest_playwright = min(playwright_invocations)
|
||||
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}"
|
||||
def test_no_runtime_uv_project_environment(self):
|
||||
"""A baked ``UV_PROJECT_ENVIRONMENT`` makes an agent's ``uv sync`` in a task
|
||||
workspace rewrite Ouroboros's own venv."""
|
||||
assert "UV_PROJECT_ENVIRONMENT" not in _read("Dockerfile")
|
||||
|
||||
def test_ci_docker_lanes_use_the_shipped_browsers(self):
|
||||
"""The release-gate container commands must not redirect Playwright away from the image."""
|
||||
ci = _read(".github/workflows/ci.yml")
|
||||
assert "docker build -t ouroboros-web:test ." in ci
|
||||
assert "PLAYWRIGHT_BROWSERS_PATH=0" not in ci, (
|
||||
"a PLAYWRIGHT_BROWSERS_PATH=0 prefix hides /ms-playwright and makes the lane download at test time"
|
||||
)
|
||||
assert "playwright install --only-shell" not in ci, "the portable lane measures the shipped image"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
|
|
@ -175,7 +175,10 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
|
|||
# 20560 -> 20800 (PR #1255; measured 20768): the Docker subsection maps the new root
|
||||
# .dockerignore (what it keeps out of image layers and why .git/tests/ must stay in),
|
||||
# a config BIBLE P6 requires on the map.
|
||||
"docs/architecture/08-git-branching-ci-and-build.md": 20800,
|
||||
# 20800 -> 22000 (PR #1300; measured 21921): the Docker subsection maps the single-Dockerfile layout
|
||||
# (browsers above the lock copy, the shared browser path, cache mounts, the CI lanes that exercise
|
||||
# them) and points at the extra-CA setting; the base sat 16 bytes under the previous budget.
|
||||
"docs/architecture/08-git-branching-ci-and-build.md": 22000,
|
||||
# 12405 -> 14400 (issue #1142): the ordinary-close paragraph gains the mechanism the chapter had
|
||||
# no text for — graceful stop signals the server PID only, the server half (stop event at the
|
||||
# signal, bounded uvicorn drain) is self-sufficient against an old group-SIGTERM launcher.
|
||||
|
|
|
|||
|
|
@ -9,17 +9,31 @@ import pytest
|
|||
pytestmark = pytest.mark.portable_detail
|
||||
|
||||
|
||||
def test_bundled_playwright_headless_shell_paths_stay_short():
|
||||
def _bundled_browsers_root() -> pathlib.Path:
|
||||
"""Where the shipped artifact keeps its Playwright browsers.
|
||||
|
||||
Packaged builds install into the Playwright package tree
|
||||
(``PLAYWRIGHT_BROWSERS_PATH=0``); the Docker image hands the runtime a
|
||||
shared directory through the same variable. Either way the test measures
|
||||
the artifact as shipped, never a download it made itself.
|
||||
"""
|
||||
configured = os.environ.get("PLAYWRIGHT_BROWSERS_PATH", "")
|
||||
if configured and configured != "0":
|
||||
return pathlib.Path(configured)
|
||||
playwright = pytest.importorskip("playwright", reason="Playwright is not installed")
|
||||
root = pathlib.Path(playwright.__file__).parent / "driver" / "package" / ".local-browsers"
|
||||
return pathlib.Path(playwright.__file__).parent / "driver" / "package" / ".local-browsers"
|
||||
|
||||
|
||||
def test_bundled_playwright_headless_shell_paths_stay_short():
|
||||
root = _bundled_browsers_root()
|
||||
if not root.is_dir():
|
||||
if os.environ.get("OUROBOROS_EXPECT_HEADLESS_SHELL") == "1":
|
||||
pytest.fail("Expected Playwright local browser bundle in this CI lane")
|
||||
pytest.fail(f"Expected Playwright browser bundle at {root} in this CI lane")
|
||||
pytest.skip("Playwright local browser bundle not present")
|
||||
shells = sorted(root.glob("chromium_headless_shell-*"))
|
||||
if not shells:
|
||||
if os.environ.get("OUROBOROS_EXPECT_HEADLESS_SHELL") == "1":
|
||||
pytest.fail("Expected Playwright headless-shell bundle in this CI lane")
|
||||
pytest.fail(f"Expected Playwright headless-shell bundle under {root} in this CI lane")
|
||||
pytest.skip("Playwright headless-shell bundle not present")
|
||||
too_long = []
|
||||
for shell in shells:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue