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
43 changes: 25 additions & 18 deletions .dev/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
# Multi-stage Dockerfile for JetThoughts test environment.
#
# Debian (glibc) base, NOT Alpine: this image records the canonical
Expand Down Expand Up @@ -32,8 +33,9 @@ ENV \
BUNDLE_JOBS=4 \
BUNDLE_PATH=/opt/bundle \
BUNDLE_RETRY=3 \
CHROME_BIN=/opt/cft/chrome-linux64/chrome \
CHROMEDRIVER_PATH=/opt/cft/chromedriver-linux64/chromedriver \
BUNDLE_WITHOUT=development:assets \
CHROME_BIN=/opt/cft/chrome/chrome \
CHROMEDRIVER_PATH=/opt/cft/chromedriver/chromedriver \
DEBUG_COLORS=true \
DISPLAY=:99 \
DOCKER=true \
Expand All @@ -55,7 +57,10 @@ ENV \
RUBY_YJIT_ENABLE=1 \
TEST_SERVER_PORT=1314

RUN apt-get update && apt-get install -y --no-install-recommends \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
&& apt-get update && apt-get install -y --no-install-recommends \
bash \
build-essential git pkg-config curl ca-certificates unzip \
libjpeg62-turbo libvips42 \
Expand All @@ -73,17 +78,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxkbcommon0 libxrandr2; do \
apt-cache show "$p" >/dev/null 2>&1 && deps="$deps $p"; \
done \
&& apt-get install -y --no-install-recommends $deps \
&& rm -rf /var/lib/apt/lists/*
&& apt-get install -y --no-install-recommends $deps

# Pinned Chrome for Testing + matching chromedriver (the rendering pin that
# makes linux/ baselines reproducible - version in .dev/cft-version).
COPY .dev/cft-version /tmp/cft-version
RUN CFT=$(tr -d '[:space:]' < /tmp/cft-version) && mkdir -p /opt/cft && cd /opt/cft && \
curl -sSLO "https://storage.googleapis.com/chrome-for-testing-public/${CFT}/linux64/chrome-linux64.zip" && \
curl -sSLO "https://storage.googleapis.com/chrome-for-testing-public/${CFT}/linux64/chromedriver-linux64.zip" && \
unzip -q chrome-linux64.zip && unzip -q chromedriver-linux64.zip && \
rm chrome-linux64.zip chromedriver-linux64.zip && \
RUN CFT=$(tr -d '[:space:]' < /tmp/cft-version) && \
case "$(dpkg --print-architecture)" in \
arm64) CFT_PLAT=linux-arm64 ;; \
*) CFT_PLAT=linux64 ;; \
esac && \
mkdir -p /opt/cft && cd /opt/cft && \
curl -sSLO "https://storage.googleapis.com/chrome-for-testing-public/${CFT}/${CFT_PLAT}/chrome-${CFT_PLAT}.zip" && \
curl -sSLO "https://storage.googleapis.com/chrome-for-testing-public/${CFT}/${CFT_PLAT}/chromedriver-${CFT_PLAT}.zip" && \
unzip -q "chrome-${CFT_PLAT}.zip" && unzip -q "chromedriver-${CFT_PLAT}.zip" && \
rm "chrome-${CFT_PLAT}.zip" "chromedriver-${CFT_PLAT}.zip" && \
ln -sfn "/opt/cft/chrome-${CFT_PLAT}" /opt/cft/chrome && \
ln -sfn "/opt/cft/chromedriver-${CFT_PLAT}" /opt/cft/chromedriver && \
"$CHROME_BIN" --version && "$CHROMEDRIVER_PATH" --version

# Deterministic font rendering (see .dev/fonts.conf header).
Expand All @@ -97,19 +108,15 @@ WORKDIR /app

COPY Gemfile Gemfile.lock ./

# No `bundle config set --local`: that writes /app/.bundle/config, and the
# `..:/app` bind mount replaces /app at runtime, so those settings never reach
# the running container. Every one of them is an ENV above instead, which does
# survive.
RUN --mount=type=cache,target=/usr/local/bundle/cache,sharing=locked \
--mount=type=cache,target=/root/.bundle \
echo "gem: --no-document" > /root/.gemrc && \
bundle config set --local deployment false && \
bundle config set --local path /opt/bundle && \
bundle config set --local jobs 4 && \
bundle config set --local retry 3 && \
bundle config set --local without development:assets && \
bundle install

VOLUME /app/node_modules
VOLUME /opt/bundle

COPY --from=node-deps /temp/dev/node_modules ./node_modules

COPY .dev/docker-entrypoint.sh /docker-entrypoint.sh
Expand Down
2 changes: 1 addition & 1 deletion .dev/cft-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
152.0.7977.54
153.0.8010.5
44 changes: 32 additions & 12 deletions .dev/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,22 @@ services:
# Interactive shell (optimized)
sh:
image: jetthoughts.com-test:1.0.0
# amd64, not the host arch: this image pins Chrome for Testing linux64
# (.dev/cft-version) and records the linux/ screenshot baselines that CI
# (ubuntu-latest = amd64) renders. Building/running arm64 downloads an
# amd64 chrome it can only exec under emulation and drifts pixels from CI.
platform: linux/amd64
# Pinned to arm64 since 2026-08-22, because that is what records the
# committed linux/ baselines: CI runs ubuntu-24.04-arm. Chrome for Testing
# began publishing linux-arm64 at 153 (.dev/cft-version pins 153.0.8010.5,
# the first Beta with it) and the Dockerfile picks the matching build.
#
# arm64 IS THE STACK (Paul, 2026-08-22) - not a preference, not a trial.
# It is reversed only if something is found that genuinely cannot support
# it, and "an x86 machine would be slower" is not that.
#
# So this is a PIN, not "follow the host". Following the host is what an
# earlier draft did, and it silently breaks the visual gate on an x86
# machine: Chrome renders amd64 while every linux/ baseline is an arm64
# recording, so bin/dtest and required CI disagree by construction. On an
# ARM Mac the pin IS the host arch, so it costs nothing here; on x86 it
# emulates, which is the correct trade because the gate is BLOCKING.
platform: linux/arm64
build:
context: ../
dockerfile: ./.dev/Dockerfile
Expand All @@ -39,6 +50,10 @@ services:
tty: true
volumes:
- ..:/app:delegated
# Anonymous mount, not a named volume: it shields the image's
# Linux-built node_modules from the bind mount (the host's are
# macOS binaries) without persisting stale modules between runs.
- /app/node_modules
- history:/usr/local/hist
environment:
HISTFILE: /usr/local/hist/.bash_history
Expand All @@ -48,8 +63,9 @@ services:
# Test runner with optimized dependency management
t:
image: jetthoughts.com-test:1.0.0
# amd64 to match CI's rendering (see `sh` service note above).
platform: linux/amd64
# Pinned to CI's arch, not the host's - this is the service that produces
# screenshot candidates. See the `sh` service note above for why.
platform: linux/arm64
build:
context: ../
dockerfile: ./.dev/Dockerfile
Expand All @@ -58,13 +74,21 @@ services:
command: bin/test
volumes:
- ..:/app:delegated
# Anonymous mount, not a named volume: it shields the image's
# Linux-built node_modules from the bind mount (the host's are
# macOS binaries) without persisting stale modules between runs.
- /app/node_modules
- hugo_cache_dtest:/tmp/hugo_cache_dtest
environment:
HUGO_DEFAULT_PATH: "_dest/public-dtest"
HUGO_CACHEDIR: "/tmp/hugo_cache_dtest"
CAPYBARA_SCREENSHOT_ON_FAILURE: "true"
RUBY_THREAD_VM_STACK_SIZE: 1048576
# Chrome 152 needs >2g under amd64 emulation (OOM "tab crashed" at 2g).
# 4g was originally sized for amd64 EMULATION ("tab crashed" OOM at 2g).
# Native arm64 probably needs less - and 4g STAYS anyway (Paul, 2026-08-22).
# Settled, not pending: do not "optimize" this down. Headroom on a test
# container is free; an OOM-killed Chrome costs a red suite and an hour of
# diagnosis, and it fails as a rendering difference rather than as an OOM.
mem_limit: 4g
cpus: '4.0'
ulimits:
Expand All @@ -73,10 +97,6 @@ services:
hard: -1

volumes:
node_modules:
driver: local
bundle:
driver: local
history:
driver: local
hugo_cache:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ jobs:
screenshots:
name: Screenshot Tests
if: ${{ github.event_name == 'pull_request' || inputs.screenshots || inputs.update-baselines }}
runs-on: ubuntu-latest
# arm64 to match the local container (both run CfT linux-arm64 since
# .dev/cft-version moved to 153). Free for public repos.
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
# BLOCKING since 2026-08-22: the report-only soak (2026-07-31, #413) ended —
# its motivating defects were fixed (#560 tolerance, #566 baselines, #570
Expand Down
1 change: 1 addition & 0 deletions .okf/build/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* [Hugo build pipeline](hugo-build.md) - bin/hugo-build with the 8 course validators; also the PurgeCSS cold-start race and the minified-unquoted-attribute audit-tool trap
* [Test gates](test-gates.md) - the local suites, when each is a commit blocker, the 0.0001 default tolerance, why the SECTION_CONFIGS shield was deleted and what measuring it cost, why a green run never refreshes a baseline, and why below-fold content is invisible at any tolerance, bin/record-baselines for accepting only the baselines you meant to move, and why a deleted source file still serves from every local _dest/ tree, plus the NULL CHANGE - a diff that passes every gate and alters nothing - and what `okf_validate` actually guards (shape, not truth; error-only conformance) with the two-spec trap, and why computed-style assertions beat more screenshots for below-fold coverage and for contrast (a screenshot returns a pixel delta, never a ratio)
* [A screenshot baseline is a recording of a rendering stack](rendering-stack.md) - which environment records `linux/`, why local dtest and CI can diverge, the all-arm64 migration (CfT 153 Beta, `ubuntu-24.04-arm`), and five wrong explanations for one drift - including the withdrawn "Debian vs Ubuntu" answer and why same-failure-on-both is not confirmation
* [CI gates](ci-gates.md) - what GitHub Actions enforces: build, unit, path-scoped link check (visual regression is report-only), and what gates a PR never sees
* [Template PDFs](pdf-templates.md) - regenerating the downloadable course PDFs
* [Classes only on &lt;th&gt; get purged](hugo-stats-th-classes.md) - Hugo 0.165 writeStats records no class attrs on `<th>`, so PurgeCSS deletes those rules from the production bundle while dev builds look fine and the visual gate stays green - unguardable by a screenshot, guardable by a set-diff unit test (`next_purge_guard_test.rb`); also two sibling false-greens on the /next/ rail (site-scripts in a landing baseof, computed `resources.Get` paths defeating the orphan guard)
Loading
Loading