From 638243726eb99497bf9bd9f9cdffca492abe190b Mon Sep 17 00:00:00 2001 From: ethernet Date: Fri, 19 Jun 2026 11:56:30 -0400 Subject: [PATCH] refactor(ci): faster docker builds via --link and chmod removal --- .github/workflows/docker-publish.yml | 4 +-- Dockerfile | 26 +++++++------- .../test_dockerfile_immutable_install.py | 36 ++++++++----------- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 48b1f94a6f0..108eac7376c 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -120,13 +120,11 @@ jobs: - name: Install Python dependencies (for docker tests) if: github.event_name != 'pull_request' run: | - uv venv .venv --python 3.11 - source .venv/bin/activate # ``dev`` extra pulls in pytest, pytest-asyncio — # everything tests/docker/ needs. We deliberately avoid ``all`` # here because the docker tests only drive the container via # subprocess and don't import hermes_agent's optional deps. - uv pip install -e ".[dev]" + uv sync --locked --python 3.11 --extra dev - name: Run docker integration tests if: github.event_name != 'pull_request' diff --git a/Dockerfile b/Dockerfile index c01de9857bb..cf262eda47a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -189,7 +189,13 @@ RUN cd web && npm run build && \ # ---------- Source code ---------- # .dockerignore excludes node_modules, so the installs above survive. -COPY . . +# --link decouples this layer from parents for cache purposes; --chmod bakes +# the final read-only permissions at copy time so we skip the separate +# `chmod -R` pass that previously walked ~30k files across the venv + +# node_modules + source (21s amd64 / 222s arm64 — #49113). `a+rX,go-w` +# gives the non-root hermes user read + traverse but no write; root retains +# write so the build steps below don't need chmod u+w dances. +COPY --link --chmod=a+rX,go-w . . # ---------- Permissions ---------- # Link hermes-agent itself (editable). Deps are already installed in the @@ -197,19 +203,15 @@ COPY . . # resolution or downloads. RUN uv pip install --no-cache-dir --no-deps -e "." -# Keep /opt/hermes immutable for the runtime hermes user. Hosted/container -# instances must not be able to self-edit the installed source or venv; user -# data, skills, plugins, config, logs, and dashboard uploads live under -# /opt/data instead. Root can still repair the image during build/boot, but -# supervised Hermes processes drop to the non-root hermes user. +# Wire the exec shim and install-method stamp. Files under /opt/hermes are +# already root-owned (COPY, uv sync, npm install all run as root) and +# read-only for the hermes user (go-w from the --chmod above). + USER root RUN mkdir -p /opt/hermes/bin && \ cp /opt/hermes/docker/hermes-exec-shim.sh /opt/hermes/bin/hermes && \ chmod 0755 /opt/hermes/bin/hermes && \ - printf 'docker\n' > /opt/hermes/.install_method && \ - chown -R root:root /opt/hermes && \ - chmod -R a+rX /opt/hermes && \ - chmod -R a-w /opt/hermes + printf 'docker\n' > /opt/hermes/.install_method # The ``.install_method`` stamp is baked next to the running code (the install # tree), NOT into $HERMES_HOME. $HERMES_HOME (/opt/data) is a shared data # volume that is commonly bind-mounted from the host and even shared with a @@ -240,9 +242,7 @@ RUN mkdir -p /opt/hermes/bin && \ # every published image has it. ARG HERMES_GIT_SHA= RUN if [ -n "${HERMES_GIT_SHA}" ]; then \ - chmod u+w /opt/hermes && \ - printf '%s\n' "${HERMES_GIT_SHA}" > /opt/hermes/.hermes_build_sha && \ - chmod a-w /opt/hermes /opt/hermes/.hermes_build_sha; \ + printf '%s\n' "${HERMES_GIT_SHA}" > /opt/hermes/.hermes_build_sha; \ fi # ---------- s6-overlay service wiring ---------- diff --git a/tests/tools/test_dockerfile_immutable_install.py b/tests/tools/test_dockerfile_immutable_install.py index ffa039a854c..c712bb63cea 100644 --- a/tests/tools/test_dockerfile_immutable_install.py +++ b/tests/tools/test_dockerfile_immutable_install.py @@ -12,22 +12,16 @@ def _dockerfile_text() -> str: return DOCKERFILE.read_text() -def test_dockerfile_makes_opt_hermes_root_owned_and_non_writable() -> None: +def test_dockerfile_makes_opt_hermes_readonly_for_hermes_user() -> None: text = _dockerfile_text() - assert "COPY --chown=hermes:hermes . ." not in text - assert "COPY . ." in text - assert "chown -R root:root /opt/hermes" in text - assert "chmod -R a+rX /opt/hermes" in text - assert "chmod -R a-w /opt/hermes" in text - - immutable_block = re.search( - r"RUN mkdir -p /opt/hermes/bin && \\\n" - r"(?:.*\\\n)+?" - r"\s+chmod -R a-w /opt/hermes", - text, - ) - assert immutable_block, "Dockerfile must lock /opt/hermes after installing code/deps" + # --chmod on the source COPY bakes read-only perms at copy time instead + # of a separate chmod -R pass (which walked ~30k files — #49113). + assert "COPY --link --chmod=a+rX,go-w . ." in text + # The old tree-walking passes must not be present. + assert "chown -R root:root /opt/hermes" not in text + assert "chmod -R a+rX /opt/hermes" not in text + assert "chmod -R a-w /opt/hermes" not in text def test_dockerfile_keeps_mutable_state_under_opt_data() -> None: @@ -68,22 +62,20 @@ def test_dockerfile_bakes_code_scoped_install_method_stamp() -> None: (/opt/hermes/.install_method) first; baking it at build time keeps the published image self-identifying as 'docker' WITHOUT writing into the shared $HERMES_HOME data volume (which a host install may also use). - It must live inside the immutable block so the runtime user can't alter it. + The stamp is created by root in the shim-wiring RUN block; the hermes + user can't modify it (go-w from the --chmod on the source COPY). """ text = _dockerfile_text() assert "printf 'docker\\n' > /opt/hermes/.install_method" in text - immutable_block = re.search( + # The stamp must be in the RUN block that wires the exec shim. + shim_block = re.search( r"RUN mkdir -p /opt/hermes/bin && \\\n" r"(?:.*\\\n)+?" - r"\s+chmod -R a-w /opt/hermes", + r"\s+printf 'docker\\n' > /opt/hermes/\.install_method", text, ) - assert immutable_block, "immutable block must exist" - assert ".install_method" in immutable_block.group(0), ( - "the code-scoped install-method stamp must be baked inside the " - "immutable /opt/hermes block" - ) + assert shim_block, "install-method stamp must be in the shim-wiring RUN block" def test_dockerfile_redirects_lazy_installs_to_durable_target() -> None: