Make lint and test phases of the Dockerfile (closes #96)
check / check (push) Successful in 33s
check / check (push) Successful in 33s
Follows the template: Dockerfile.lint is gone; the Dockerfile has a lint phase (eslint, prettier --check .) and a test phase (vitest, run as the node user so the not-writable-directory tests are not skipped), and its last stage compiles and depends on both. script/lint and script/test build one phase each with --no-cache; script/docker and script/cibuild pass --no-cache, so CHECK_EPOCH and LINT_EPOCH are removed. script/cibuild is the single image build, so CI runs lint and the tests once each. The tests that checked the old layout are deleted, REPO_POLICIES.md is re-copied and the README describes the new layout. Model: opus-5-5
This commit is contained in:
@@ -97,20 +97,18 @@ alpine. We provide:
|
||||
- `script/build` — compile the TypeScript sources into `dist/`, then verify that
|
||||
the entrypoints `package.json` declares (`main`, `types`, `bin`) are among the
|
||||
files the compiler wrote, and make the CLI executable (our own extension)
|
||||
- `script/test` — run the test suite (vitest, hard-capped at 30s where `timeout`
|
||||
is available, verbose rerun on failure)
|
||||
- `script/lint` — run eslint and a prettier check, by building
|
||||
`Dockerfile.lint`; requires docker (see Linting below)
|
||||
- `script/test` — run the test suite, by building the `test` phase of the
|
||||
`Dockerfile` (vitest, 90s timeout, verbose rerun on failure); requires docker
|
||||
- `script/lint` — run eslint and a prettier check, by building the `lint` phase
|
||||
of the `Dockerfile`; requires docker (see Linting and testing below)
|
||||
- `script/fmt` — format all files with prettier (writes)
|
||||
- `script/fmt-check` — check formatting on the host (read-only); standalone, and
|
||||
not called by `script/check` or `script/precommit`, because `script/lint`
|
||||
already checks formatting in the container (see Linting below)
|
||||
already checks formatting in the container
|
||||
- `script/check` — run all checks: `test`, `lint` (our own extension)
|
||||
- `script/docker` — build the test and build image, tagged via
|
||||
`script/projectname`
|
||||
- `script/cibuild` — cd to the repo root and build both images (what CI runs):
|
||||
`script/lint` first, then the `Dockerfile` image, which runs `make test` and
|
||||
`make build`
|
||||
- `script/docker` — build the image, tagged via `script/projectname`
|
||||
- `script/cibuild` — build the image (what CI runs); its last stage depends on
|
||||
the `lint` and `test` phases, so this one build lints, tests and compiles
|
||||
- `script/precommit` — run by the git pre-commit hook (our own extension); runs
|
||||
`script/lint`, which checks both lint and formatting, but deliberately not the
|
||||
tests, so the TDD red-phase commit can land
|
||||
@@ -119,50 +117,32 @@ alpine. We provide:
|
||||
|
||||
`make hooks` installs the pre-commit hook that runs `script/precommit`.
|
||||
|
||||
### Linting
|
||||
### Linting and testing
|
||||
|
||||
Linting runs in a container, one way, everywhere. `script/lint` builds
|
||||
`Dockerfile.lint`, which copies the repo into a digest-pinned node image and
|
||||
runs eslint and prettier as build steps, so a successful build is a clean lint.
|
||||
There is no host lint path: docker is required to lint, and that also works
|
||||
where the docker daemon is remote and bind mounts are impossible.
|
||||
Linting and testing are phases of the `Dockerfile`. The `lint` phase copies the
|
||||
repo into a digest-pinned node image and runs eslint and `prettier --check .`;
|
||||
the `test` phase does the same with the suite. `script/lint` and `script/test`
|
||||
each build one phase with `docker build --no-cache --target <phase>`. There is
|
||||
no host lint or test path: docker is required, and that also works where the
|
||||
docker daemon is remote and bind mounts are impossible.
|
||||
|
||||
The formatting check is part of that, not a step beside it. `script/check` and
|
||||
`script/precommit` therefore call `script/lint` and stop; neither calls
|
||||
`script/fmt-check` as well, which would run prettier a second time over the same
|
||||
tree for the same verdict — and the weaker of the two, since the host's prettier
|
||||
is whatever the working tree has installed. So `make check` and the pre-commit
|
||||
hook both still fail on a badly formatted tree, and prettier runs exactly once
|
||||
in each. `test/packaging/lint-once.test.ts` asserts that count by walking the
|
||||
invocation graph, so a second pass cannot creep back in unnoticed.
|
||||
The last stage of the `Dockerfile` compiles the package, and it copies a file
|
||||
from each phase, so it cannot be built unless lint and the tests pass. That is
|
||||
why `script/cibuild` is a single `docker build`: it runs lint and the tests once
|
||||
each and then compiles.
|
||||
|
||||
Every `docker build` in `script/` passes `--no-cache`. On an unchanged tree
|
||||
Docker would otherwise serve the lint and test steps from cache, nothing would
|
||||
run, and the build would still exit 0.
|
||||
|
||||
The formatting check is part of the `lint` phase, not a step beside it, so
|
||||
`script/check` and `script/precommit` do not call `script/fmt-check` as well;
|
||||
that would run prettier a second time over the same tree for the same verdict.
|
||||
`script/fmt-check` remains as a standalone entrypoint for asking the formatting
|
||||
question on its own, without docker and without the rest of lint. Its verdict
|
||||
cannot drift from the container's: prettier is pinned to an exact version,
|
||||
installed from `yarn.lock` under `--frozen-lockfile` in both places, and reads
|
||||
`.gitignore` as its default ignore file — which is why `.dockerignore`
|
||||
deliberately keeps `.gitignore` in the build context.
|
||||
|
||||
Lint happens in exactly one place, which constrains the rest of the build.
|
||||
`script/check` calls `script/lint`, so `make check` cannot run inside a
|
||||
container without asking for docker inside docker. The image built from
|
||||
`Dockerfile` therefore runs `make test` and `make build` and does not lint;
|
||||
`script/cibuild` builds `Dockerfile.lint` first and that image second, so CI
|
||||
gets both verdicts.
|
||||
|
||||
### Build epochs
|
||||
|
||||
`script/lint` passes `--build-arg LINT_EPOCH="$(date +%s)"`, and `script/docker`
|
||||
and `script/cibuild` pass `--build-arg CHECK_EPOCH="$(date +%s)"`. Both
|
||||
Dockerfiles refuse to build without their argument. This is deliberate: on an
|
||||
unchanged tree Docker would otherwise serve the linter and test layers from
|
||||
cache, so nothing would run and the build would still exit 0 — a lint build over
|
||||
an untouched tree returns success in well under a second, having linted nothing.
|
||||
A changing epoch invalidates every layer below the guard on every invocation
|
||||
while leaving the dependency layers above them cached, and the missing-argument
|
||||
guard means a bare `docker build .` fails loudly instead of quietly reporting a
|
||||
green it did not earn: an unset build argument is the empty string, which is a
|
||||
perfectly stable cache key.
|
||||
question on the host. Its verdict matches the container's: prettier is pinned to
|
||||
an exact version, installed from `yarn.lock` under `--frozen-lockfile` in both
|
||||
places, and reads `.gitignore` as its default ignore file — which is why
|
||||
`.dockerignore` keeps `.gitignore` in the build context.
|
||||
|
||||
## Rationale
|
||||
|
||||
@@ -197,10 +177,9 @@ All work on quak is test-driven. No exceptions.
|
||||
3. Subsequent commits add the implementation and any refactors needed to make
|
||||
the tests pass.
|
||||
4. A feature branch can only be merged into `main` when `make check` is green.
|
||||
`main` is always green. CI runs `script/cibuild`, which lints via
|
||||
`Dockerfile.lint` and then runs `make test` and `make build` in the
|
||||
`Dockerfile` image, so neither a red branch nor one that does not compile can
|
||||
pass CI.
|
||||
`main` is always green. CI runs `script/cibuild`, which builds the
|
||||
`Dockerfile`: its `lint` and `test` phases, then the compile, so neither a
|
||||
red branch nor one that does not compile can pass CI.
|
||||
5. Tests are the canonical API documentation for this library. Every test file
|
||||
is commented thoroughly enough that a reader who has never seen quak can
|
||||
learn how to use it from the tests alone. Comments explain why a behavior
|
||||
@@ -217,7 +196,7 @@ All work on quak is test-driven. No exceptions.
|
||||
runs `script/lint` — eslint and the prettier check, in the container — but
|
||||
not the tests, and so not the full `make check`. This is deliberate so the
|
||||
TDD red-phase commit (failing tests, no implementation yet) can land. The
|
||||
suite runs as part of the image build, which is what CI executes via
|
||||
`test` phase is part of the image build, which is what CI executes via
|
||||
`script/cibuild`, so a red branch still cannot reach `main`.
|
||||
|
||||
## Design
|
||||
@@ -245,8 +224,7 @@ quak/
|
||||
quak.ts CLI entrypoint (commander.js)
|
||||
test/ unit + integration tests (vitest)
|
||||
Makefile
|
||||
Dockerfile test suite and compile
|
||||
Dockerfile.lint eslint and prettier, as build steps
|
||||
Dockerfile lint phase, test phase, compile
|
||||
package.json
|
||||
tsconfig.json
|
||||
```
|
||||
@@ -739,13 +717,13 @@ documents:
|
||||
commented thoroughly. `main` is always green.
|
||||
|
||||
- **Required checks before every commit:** `make lint` must pass — that is
|
||||
eslint plus the prettier check, and it builds `Dockerfile.lint`, so it needs
|
||||
docker. The pre-commit hook enforces exactly that. `make check` (which also
|
||||
runs the tests) must pass before merging to `main`. `make fmt-check` is
|
||||
available for a host-side formatting check on its own, but it is not a
|
||||
separate requirement: `make lint` already covers it, and running both would
|
||||
check formatting twice. Never invoke eslint or prettier directly; linting runs
|
||||
in the container only.
|
||||
eslint plus the prettier check, and it builds the `lint` phase of the
|
||||
`Dockerfile`, so it needs docker. The pre-commit hook enforces exactly that.
|
||||
`make check` (which also runs the tests) must pass before merging to `main`.
|
||||
`make fmt-check` is available for a host-side formatting check on its own, but
|
||||
it is not a separate requirement: `make lint` already covers it, and running
|
||||
both would check formatting twice. Never invoke eslint or prettier directly;
|
||||
linting runs in the container only.
|
||||
|
||||
- **Formatting:** prettier with 4-space indents and `proseWrap: always` for
|
||||
markdown. Use `make fmt` to format. Use `yarn` not `npm`.
|
||||
|
||||
Reference in New Issue
Block a user