ci(e2e): the runner owns the paths the containers write through

The first real run got through the image build, composer install, stack
boot and browser install, then died twice on the same rule: Docker creates
a missing bind-mount source, and every missing parent of it, as root.

~/.cache/composer is mounted into the composer container. On a cache miss
nothing had created it, so Docker created ~/.cache as root as well — and
`npx playwright install` then failed with EACCES on mkdir of a SIBLING,
~/.cache/ms-playwright. The composer cache itself was fine; the parent was
not.

var/ and public/assets/ are where compose.test.yaml mounts its named
volumes, so Docker creates them in the checkout as mount points, empty and
root-owned. `mkdir -p var/log` in the log-collection step then failed,
which would have cost the artifact on an otherwise green run.

One mkdir before anything containerised runs fixes both. Preferred to
`--user $(id -u)` on the one-off containers: the image's PHP tooling
expects root, and root-owned FILES were never the problem — tar reads
mode-644 files it does not own, so the artifact upload and the cache save
were always safe. Only creating a directory inside a root-owned one
failed, and Docker never creates a directory that already exists.

vendor/ and assets/vendor/ are deliberately left root-owned: no
runner-user step reads or writes either.

Verified locally on an unprivileged user, both directions. Without the
step, `mkdir -p var/log` reproduces CI's exact error; with it, var/,
var/log and public/assets survive a full `compose up` runner-owned, the
log step writes a tar-readable e2e-server.log, and ~/.cache/ms-playwright
can be created after the composer container has run.
This commit is contained in:
Paul Lützner committed 2026-08-10 16:28:48 +02:00
1 parent 56606f2c78
commit 7bc8d5d1da
1 file changed
+34
+34
View File
@@ -55,6 +55,40 @@ jobs:
steps:
- uses: actions/checkout@v4
# Every host path a container is about to write through, created
# HERE so that the runner user owns it.
#
# Docker creates a missing bind-mount source — and any missing
# parent of it — itself, as root. That is a rule with a long reach,
# and both halves of it broke this job on its first real run:
#
# ~/.cache/composer is mounted into the composer container below.
# On a cache miss nothing had created it, so Docker created
# ~/.cache as root too, and `npx playwright install` then died on
# EACCES trying to mkdir a SIBLING of it, ~/.cache/ms-playwright.
# Nothing was wrong with the composer cache; it was the parent.
#
# var/ and public/assets/ are where compose.test.yaml mounts its
# named volumes, so Docker creates them in the checkout as the
# mount points — empty, root-owned. `mkdir -p var/log` in the log
# step then failed, which is how a green suite would still have
# lost its artifact.
#
# Pre-creating is the whole fix, and it is deliberately preferred to
# running those containers with `--user $(id -u)`: the image's PHP
# tooling expects root (COMPOSER_ALLOW_SUPERUSER, the entrypoint's
# setfacl), and root-owned FILES were never the problem — tar reads
# mode-644 files it does not own, so the artifact upload and the
# cache save are both fine. Only creating a directory inside a
# root-owned one ever failed, and a directory that already exists is
# never created by Docker.
#
# vendor/ and assets/vendor/ are left alone on purpose. Containers
# write them as root and no runner-user step reads or writes either,
# so there is nothing to fix and a chown would only be ceremony.
- name: Prepare the paths containers write through
run: mkdir -p ~/.cache/composer var/log public/assets
- name: Set up Buildx
uses: docker/setup-buildx-action@v3