diff --git a/Dockerfile b/Dockerfile index 972b057..de844aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,11 @@ # # The lint and test phases are the gates: `make lint` (script/lint) # builds the lint stage alone and `make test` (script/test) the test -# stage alone, and the builder stage depends on both, so the image -# cannot be built unless they pass. Each covers the frontend as well, -# through a copy from a node stage. Inside them each tool is invoked -# directly, never through make or script/, whose lint and test are -# themselves docker builds. +# stage alone, and the two build stages, frontend-build and builder, +# depend on both, so the image cannot be built unless they pass. Each +# phase covers the frontend as well, through a copy from a node stage. +# Inside them each tool is invoked directly, never through make or +# script/, whose lint and test are themselves docker builds. # Frontend lint stage: eslint with the rules in eslint.config.js. The # lint phase below runs it. @@ -35,30 +35,26 @@ RUN golangci-lint run --config .golangci.yml ./... # phase run it. COPY --from=frontend-lint /app/yarn.lock /dev/null -# Frontend stage: the unit tests in test/unit/, then the production -# build into dist/, which the runtime stage serves. The test phase below -# runs it. The tests print a dot each; if any fails, they run again with -# every test listed, and the step fails even if that run passes. -# NODE_OPTIONS chooses the reporter because yarn adds its arguments -# after the test files, where node would take a reporter option for one -# more file. +# Frontend test stage: the unit tests in test/unit/. The test phase +# below runs it, and the frontend-build stage starts from it. The tests +# print a dot each; if any fails, they run again with every test listed, +# and the step fails even if that run passes. NODE_OPTIONS chooses the +# reporter because yarn adds its arguments after the test files, where +# node would take a reporter option for one more file. # node:22-alpine as of 2026-02-22 -FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS frontend +FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS frontend-test WORKDIR /app COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile -# vite.config.js reads the commit for the page's footer with git. -RUN apk add --no-cache git COPY . . RUN NODE_OPTIONS=--test-reporter=dot timeout 90 yarn --silent run test || \ { echo "--- Rerunning with every test listed for details ---"; \ NODE_OPTIONS=--test-reporter=spec timeout 90 yarn --silent run test; \ exit 1; } -RUN yarn build # Test phase: the backend's tests with the race detector and coverage, -# and the frontend's through the copy from the frontend stage at the -# end. -race needs cgo and so a C compiler, which the Debian Go image +# and the frontend's through the copy from the frontend-test stage at +# the end. -race needs cgo and so a C compiler, which the Debian Go image # ships and the alpine one does not. -timeout 90s is a backstop above # the 60-second cap on the suite. The rerun with -v only shows details: # the step fails however it ends, because the first run already failed. @@ -75,9 +71,21 @@ RUN --mount=type=tmpfs,target=/go/pkg/mod \ go test -timeout 90s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ go test -timeout 90s -race -v ./...; exit 1; } -# Nothing is wanted from the frontend stage; the copy is what makes +# Nothing is wanted from the frontend-test stage; the copy is what makes # this phase run its tests. -COPY --from=frontend /app/yarn.lock /dev/null +COPY --from=frontend-test /app/yarn.lock /dev/null + +# Frontend build stage: the production build into dist/, which the +# runtime stage serves, on the yarn packages and source of the +# frontend-test stage. Nothing is wanted from the two phases; the copies +# are what make BuildKit build them first, so this stage cannot run +# unless lint and test passed. +FROM frontend-test AS frontend-build +COPY --from=lint /src/go.sum /dev/null +COPY --from=test /src/go.sum /dev/null +# vite.config.js reads the commit for the page's footer with git. +RUN apk add --no-cache git +RUN yarn build # Backend build stage. Nothing is wanted from the two phases; the copies # are what make BuildKit build them first, so this stage cannot run @@ -131,7 +139,7 @@ RUN addgroup -g 1000 -S netwatch && \ RUN rm /etc/nginx/conf.d/default.conf COPY configs/nginx.conf /etc/nginx/templates/netwatch.conf.template COPY configs/security-headers.conf /etc/nginx/security-headers.conf -COPY --from=frontend /app/dist /usr/share/nginx/html +COPY --from=frontend-build /app/dist /usr/share/nginx/html COPY --from=builder /src/netwatch-server /usr/local/bin/netwatch-server COPY bin/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/README.md b/README.md index 7956481..c664a53 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,9 @@ either one is broken. We provide: - `script/clean` — remove what `script/build` writes - `script/projectname` — print the project name (used for the Docker image tag) - `script/test` — build the `test` phase of `Dockerfile` without the cache: the - frontend's unit tests and production build in its `frontend` stage, and the - backend's Go tests with the race detector and coverage + frontend's unit tests in its `frontend-test` stage, and the backend's Go tests + with the race detector and coverage. The frontend's production build is left + to the image build, in the `frontend-build` stage - `script/lint` — build the `lint` phase of `Dockerfile` without the cache: eslint in its `frontend-lint` stage, and golangci-lint over the Go packages - `script/fmt` — format all files (writes): prettier over the JavaScript, CSS, @@ -78,8 +79,8 @@ either one is broken. We provide: - `script/frontend-test` — run the unit tests in `test/unit/` on the host with Node's built-in test runner, through the `test` script in `package.json`, and if any fails, run them again listing every test, and fail; then the production - build. Each test run has a 90-second timeout. `make test` runs the same in - Docker + build. Each test run has a 90-second timeout. `make test` runs the same tests + in Docker, without the production build - `script/frontend-fmt` — format the JavaScript, CSS, HTML and Markdown with prettier (writes) - `script/frontend-fmt-check` — check prettier formatting (read-only) diff --git a/TODO.md b/TODO.md index d119197..a12f8da 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,14 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of # Completed Steps +- 2026-10-14: `make test` no longer runs the frontend's production build, which + the tests do not need ([#118](https://git.eeqj.de/sneak/netwatch/issues/118)). + `yarn build` moved out of the stage that runs the frontend's unit tests, now + named `frontend-test`, into a new `frontend-build` stage, which starts from it + and, like the `builder` stage, waits on the lint and test phases, so the image + still cannot be built unless they pass. The runtime stage copies `dist/` from + it, and the `git` that `vite.config.js` reads the commit with is installed + there. No build cache was added - 2026-10-14: opening the debug log no longer raises the measured latencies ([#131](https://git.eeqj.de/sneak/netwatch/issues/131)). With the panel open, each line written drew the panel again with every line before it, up to 1000, @@ -503,3 +511,9 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of - A configurable host list (an environment variable or a config file) - Export of the latency history (CSV or JSON) - A notification when the health status changes to DEGRADED +- `make test` within the 20-second target + ([#118](https://git.eeqj.de/sneak/netwatch/issues/118)): every run compiles + the Go standard library and every dependency with the race detector from + nothing. Whether test builds may keep Go's compiled packages between runs + waits on sneak's ruling in + [prompts#113](https://git.eeqj.de/sneak/prompts/issues/113) diff --git a/script/frontend-test b/script/frontend-test index 5a8c965..390a0a3 100755 --- a/script/frontend-test +++ b/script/frontend-test @@ -2,9 +2,10 @@ # script/frontend-test: run the frontend test suite on the host: the # unit tests in test/unit/, through the test script in package.json, # then the production build, which fails on broken code. make test runs -# the same in the frontend stage of Dockerfile. The tests print a dot -# each; if any fails, they run again with every test listed, and the -# script fails even if that run passes. NODE_OPTIONS chooses the +# the same tests in the frontend-test stage of Dockerfile, and the image +# build runs the production build in its frontend-build stage. The tests +# print a dot each; if any fails, they run again with every test listed, +# and the script fails even if that run passes. NODE_OPTIONS chooses the # reporter because yarn adds its arguments after the test files, where # node would take a reporter option for one more file. set -eu