Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/actions/setup-dev-image/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ runs:
echo "ACTIONS_RUNTIME_TOKEN=${ACTIONS_RUNTIME_TOKEN}"
} >> "$GITHUB_ENV"

# Run the compose containers as root on CI. The image defaults to a
# non-root user (UID 1000) so local bind-mount writes are host-owned, but
# the runner's checkout is owned by a *different* UID — a non-root
# container can't create files (wasm pkg/, mdbook book/, dist/) inside
# runner-owned tree directories. Ownership is throwaway on the ephemeral
# runner, so root is the simplest correct choice and matches the
# historical (proven-green) CI behaviour. Read by `user:` in
# docker-compose.yml via the AFM_UID/AFM_GID defaults.
- name: Run compose containers as root on CI
shell: bash
run: |
{
echo "AFM_UID=0"
echo "AFM_GID=0"
} >> "$GITHUB_ENV"

# docker/login-action authenticates the runner against GHCR using
# the standard GITHUB_TOKEN. Skipped on fork PRs where the token
# lacks `packages: read` — those runs go through the build
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ jobs:

book:
runs-on: ubuntu-latest
# This job invokes `docker compose run book` directly (not via the
# setup-dev-image action), so set the root override here too — the book
# image defaults to a non-root user that can't write mdbook output into
# the runner-owned checkout. See docker-compose.yml `user:`.
env:
AFM_UID: "0"
AFM_GID: "0"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
Expand Down
39 changes: 38 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,29 @@ ENV CARGO_HOME=/cargo/home \
# on purpose (see docker-compose.yml): nesting them under /workspace
# made the daemon create root-owned ./target / ./.cargo / ./.sccache
# on the host, littering the working tree and breaking host-side cargo.
RUN mkdir -p /cargo/target /cargo/home/registry /cargo/home/git /cargo/sccache
RUN mkdir -p /cargo/target /cargo/home/registry /cargo/home/git /cargo/sccache \
/workspace/playground/node_modules

# Run as a non-root user so files written into the /workspace bind mount
# (generated artefacts, wasm pkg/, mdbook output, node_modules) are owned
# by the host developer, not root. UID/GID default to the conventional
# first-user 1000; override with `--build-arg UID=$(id -u) --build-arg
# GID=$(id -g)` on hosts that differ. Debian bookworm's base leaves 1000
# free, so the create is unconditional (and fails loudly if it ever isn't).
# The cache dirs at /cargo/* and the playground node_modules mountpoint are
# chowned so a *fresh* named volume initialises dev-owned. CI flips the
# runtime UID back to root via `user:` in docker-compose.yml (AFM_UID=0):
# the ephemeral runner's checkout is owned by a different UID and ownership
# is throwaway there, so root sidesteps cross-UID write failures.
ARG UID=1000
ARG GID=1000
RUN groupadd --gid "${GID}" dev \
&& useradd --uid "${UID}" --gid "${GID}" --create-home --shell /bin/bash dev \
&& chown -R "${UID}:${GID}" /cargo /workspace
ENV HOME=/home/dev

WORKDIR /workspace
USER dev

# Default shell friendly for interactive dev sessions
CMD ["bash"]
Expand All @@ -189,6 +209,11 @@ CMD ["bash"]
########################################################################
FROM dev AS fuzz

# The dev stage ends as USER dev; the nightly toolchain + cargo-fuzz/udeps
# installs below write to /usr/local (root-owned), so switch back to root
# for the install layers and drop to dev again at the end.
USER root

# `rustup toolchain install` tries to self-update by looking for the
# rustup binary at $CARGO_HOME/bin/rustup. The inherited
# `CARGO_HOME=/cargo/home` (set in the dev stage for runtime
Expand All @@ -204,6 +229,8 @@ RUN cargo binstall --no-confirm --locked --root /usr/local \
cargo-fuzz \
cargo-udeps

USER dev

########################################################################
# Stage: ci — fuzz superset; the published GHCR image (used by CI matrix
# jobs) carries every tool every recipe might invoke.
Expand All @@ -218,7 +245,17 @@ FROM node-base AS book
COPY --from=cargo-tools /usr/local/bin/mdbook /usr/local/bin/mdbook
COPY --from=cargo-tools /usr/local/bin/mdbook-linkcheck /usr/local/bin/mdbook-linkcheck

# Match the dev stage's non-root user so mdbook output written into the
# /workspace bind mount is host-owned, not root. `book` is FROM node-base
# (not dev), so it creates its own identical `dev` user.
ARG UID=1000
ARG GID=1000
RUN groupadd --gid "${GID}" dev \
&& useradd --uid "${UID}" --gid "${GID}" --create-home --shell /bin/bash dev
ENV HOME=/home/dev

WORKDIR /workspace/crates/afm-book
USER dev
EXPOSE 3000
CMD ["mdbook", "serve", "--hostname", "0.0.0.0", "--port", "3000"]

Expand Down
21 changes: 16 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ x-common-env: &common-env
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C link-arg=-fuse-ld=mold"
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: "clang"
TERM: ${TERM:-xterm-256color}
# /workspace is bind-mounted from the host as a non-root UID, but the
# container runs as root. Git otherwise refuses operations with
# "dubious ownership"; whitelist via the GIT_CONFIG_* triplet rather
# than baking a `git config` invocation into the image (keeps the
# whitelist local to the compose surface and trivially removable).
# /workspace is bind-mounted from the host. Locally the container runs as
# the matching host UID (1000) so git is happy; in CI it runs as root
# (AFM_UID=0) over a checkout owned by the runner's UID, and git then
# refuses operations with "dubious ownership". Whitelist via the
# GIT_CONFIG_* triplet rather than baking a `git config` invocation into
# the image (keeps the whitelist local to the compose surface).
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: "safe.directory"
GIT_CONFIG_VALUE_0: "/workspace"
Expand All @@ -70,6 +71,12 @@ services:
target: dev
image: afm-dev:local
working_dir: /workspace
# Runtime UID/GID. Unset locally → 1000 (the image's baked `dev` user,
# matched to the conventional host UID) so bind-mount writes are
# host-owned. CI sets AFM_UID=0/AFM_GID=0 (setup-dev-image action) → root,
# since the runner's checkout is owned by a different UID and ownership
# is throwaway on the ephemeral runner.
user: "${AFM_UID:-1000}:${AFM_GID:-1000}"
environment: *common-env
volumes:
- .:/workspace:cached
Expand All @@ -91,6 +98,7 @@ services:
target: fuzz
image: afm-fuzz:local
working_dir: /workspace
user: "${AFM_UID:-1000}:${AFM_GID:-1000}" # see `dev` service
environment: *common-env
volumes:
- .:/workspace:cached
Expand All @@ -109,6 +117,7 @@ services:
target: ci
image: afm-ci:local
working_dir: /workspace
user: "${AFM_UID:-1000}:${AFM_GID:-1000}" # see `dev` service
environment:
<<: *common-env
CI: "true"
Expand All @@ -131,6 +140,7 @@ services:
target: book
image: afm-book:local
working_dir: /workspace/crates/afm-book
user: "${AFM_UID:-1000}:${AFM_GID:-1000}" # see `dev` service
volumes:
- .:/workspace:cached
ports:
Expand Down Expand Up @@ -164,6 +174,7 @@ services:
target: dev
image: afm-dev:local
working_dir: /workspace/playground
user: "${AFM_UID:-1000}:${AFM_GID:-1000}" # see `dev` service
environment: *common-env
volumes:
- .:/workspace:cached
Expand Down
Loading