Run all linting in Docker via Dockerfile.lint (closes #30) #31
27
Dockerfile
27
Dockerfile
@@ -1,36 +1,27 @@
|
||||
# Lint stage — fast feedback on formatting and lint issues
|
||||
# node 22.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09
|
||||
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint
|
||||
WORKDIR /app
|
||||
COPY script/ script/
|
||||
COPY package.json yarn.lock ./
|
||||
RUN script/bootstrap
|
||||
COPY . .
|
||||
RUN make fmt-check
|
||||
RUN make lint
|
||||
|
||||
# Check stage — the full suite and the build
|
||||
# Test and build image: the suite, then the compile.
|
||||
#
|
||||
# Linting deliberately does not happen here. `script/lint` is a build of
|
||||
# Dockerfile.lint, and `script/check` calls `script/lint`, so running
|
||||
# `make check` in this image would mean running `docker build` inside a
|
||||
# container. Lint runs exactly once, in Dockerfile.lint; script/cibuild
|
||||
# builds that first and this second.
|
||||
# node 22.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09
|
||||
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS check
|
||||
WORKDIR /app
|
||||
|
||||
# Force BuildKit to run the lint stage before proceeding. Without this the
|
||||
# two stages run in parallel and a lint failure can lose the race.
|
||||
COPY --from=lint /app/yarn.lock /dev/null
|
||||
|
||||
COPY script/ script/
|
||||
COPY package.json yarn.lock ./
|
||||
RUN script/bootstrap
|
||||
COPY . .
|
||||
|
||||
# CHECK_EPOCH is a cache buster: without it Docker serves `make check` from
|
||||
# CHECK_EPOCH is a cache buster: without it Docker serves the test layer from
|
||||
# cache on an unchanged tree, the suite never executes, and the build still
|
||||
# exits 0. The guard makes an absent argument a hard failure — an unset ARG
|
||||
# is the empty string, which is a perfectly stable cache key, so a plain
|
||||
# `docker build .` would otherwise still get the false green. Fail closed.
|
||||
ARG CHECK_EPOCH
|
||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||
RUN make check
|
||||
RUN make test
|
||||
|
||||
ARG CHECK_EPOCH
|
||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||
|
||||
35
Dockerfile.lint
Normal file
35
Dockerfile.lint
Normal file
@@ -0,0 +1,35 @@
|
||||
# Lint image: every lint run happens here, and nowhere else. The repo is
|
||||
# COPYed into a digest-pinned image and the linters run as build steps, so a
|
||||
# successful build IS a clean lint. `script/lint` does nothing but build this
|
||||
# file, which also works where the docker daemon is remote and bind mounts are
|
||||
# impossible. Nothing that runs inside a container may call `script/lint`:
|
||||
# that is why Dockerfile no longer runs `make check`.
|
||||
# node 22.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09
|
||||
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint
|
||||
WORKDIR /app
|
||||
|
||||
# Manifests before sources, so the dependency install layer stays cached
|
||||
# until package.json or yarn.lock changes. script/bootstrap ends in
|
||||
# `yarn install --frozen-lockfile`; the lint steps below are deliberately
|
||||
# not cached.
|
||||
COPY script/ script/
|
||||
COPY package.json yarn.lock ./
|
||||
RUN script/bootstrap
|
||||
|
||||
COPY . .
|
||||
|
||||
# LINT_EPOCH is a cache buster, with the same fail-closed contract as
|
||||
# CHECK_EPOCH in Dockerfile. No lint cache is wanted: on an unchanged tree
|
||||
# Docker serves the linter layers in well under a second, having linted
|
||||
# nothing, and the build still exits 0. The guard makes an absent argument a
|
||||
# hard failure — an unset ARG is the empty string, which is a perfectly
|
||||
# stable cache key, so a plain `docker build -f Dockerfile.lint .` would
|
||||
# otherwise get exactly that false green. Every layer below this one is a
|
||||
# child of the guard, so a fresh epoch forces all of them to execute.
|
||||
ARG LINT_EPOCH
|
||||
RUN [ -n "$LINT_EPOCH" ] || exit 1
|
||||
|
||||
# The linters are invoked directly rather than through `make lint`, because
|
||||
# `make lint` is the build of this file.
|
||||
RUN yarn run eslint .
|
||||
RUN yarn run prettier --check .
|
||||
70
README.md
70
README.md
@@ -86,15 +86,17 @@ alpine. We provide:
|
||||
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
|
||||
- `script/lint` — run eslint and a prettier check, by building
|
||||
`Dockerfile.lint`; requires docker (see Linting below)
|
||||
- `script/fmt` — format all files with prettier (writes)
|
||||
- `script/fmt-check` — check formatting (read-only)
|
||||
- `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own
|
||||
extension)
|
||||
- `script/docker` — build the Docker image, tagged via `script/projectname`
|
||||
- `script/cibuild` — cd to the repo root and run the image build (what CI runs;
|
||||
the build runs `make fmt-check` and `make lint` in a first stage, then
|
||||
`make check` and `make build` in a second)
|
||||
- `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/precommit` — run by the git pre-commit hook (our own extension); runs
|
||||
`script/lint` and `script/fmt-check` but deliberately not the tests, so the
|
||||
TDD red-phase commit can land
|
||||
@@ -103,14 +105,34 @@ alpine. We provide:
|
||||
|
||||
`make hooks` installs the pre-commit hook that runs `script/precommit`.
|
||||
|
||||
Both `script/docker` and `script/cibuild` pass
|
||||
`--build-arg CHECK_EPOCH="$(date +%s)"`. The Dockerfile refuses to build without
|
||||
it. This is deliberate: on an unchanged tree Docker would otherwise serve the
|
||||
`make check` layer from cache, so the suite would never run and the build would
|
||||
still exit 0. A changing epoch invalidates the check and build layers on every
|
||||
invocation while leaving the dependency layers below them cached, and the
|
||||
missing-argument guard means a bare `docker build .` fails loudly instead of
|
||||
quietly reporting a green it did not earn.
|
||||
### Linting
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Rationale
|
||||
|
||||
@@ -145,8 +167,10 @@ 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. The Dockerfile runs `make check` and `make build`, so
|
||||
neither a red branch nor one that does not compile can pass CI.
|
||||
`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.
|
||||
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
|
||||
@@ -162,9 +186,8 @@ All work on quak is test-driven. No exceptions.
|
||||
8. The pre-commit hook installed by `make hooks` runs `script/precommit`, which
|
||||
runs the lint and format checks but not the full `make check`. This is
|
||||
deliberate so the TDD red-phase commit (failing tests, no implementation yet)
|
||||
can land. The full `make check` runs as part of the image build, which is
|
||||
what CI executes via `script/cibuild`, so a red branch still cannot reach
|
||||
`main`.
|
||||
can land. The suite runs as part of the image build, which is what CI
|
||||
executes via `script/cibuild`, so a red branch still cannot reach `main`.
|
||||
|
||||
## Design
|
||||
|
||||
@@ -191,7 +214,8 @@ quak/
|
||||
quak.ts CLI entrypoint (commander.js)
|
||||
test/ unit + integration tests (vitest)
|
||||
Makefile
|
||||
Dockerfile
|
||||
Dockerfile test suite and compile
|
||||
Dockerfile.lint eslint and prettier, as build steps
|
||||
package.json
|
||||
tsconfig.json
|
||||
```
|
||||
@@ -478,9 +502,11 @@ documents:
|
||||
implementation. Tests are the canonical API documentation and must be
|
||||
commented thoroughly. `main` is always green.
|
||||
|
||||
- **Required checks before every commit:** `make lint` (eslint + prettier check)
|
||||
and `make fmt-check` must pass. The pre-commit hook enforces this.
|
||||
`make check` (which also runs tests) must pass before merging to `main`.
|
||||
- **Required checks before every commit:** `make lint` (eslint + prettier check,
|
||||
which builds `Dockerfile.lint` and therefore needs docker) and
|
||||
`make fmt-check` must pass. The pre-commit hook enforces this. `make check`
|
||||
(which also runs tests) must pass before merging to `main`. 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`.
|
||||
|
||||
11
TODO.md
11
TODO.md
@@ -18,6 +18,17 @@ Update the README API reference section to match the current implementation.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-10: Moved all linting into Docker. `script/lint` builds a new root
|
||||
`Dockerfile.lint`, which copies the repo into the digest-pinned node image and
|
||||
runs eslint and prettier as build steps, so a successful build is a clean
|
||||
lint; no host lint path remains and `yarn lint` is gone from `package.json`. A
|
||||
fail-closed `LINT_EPOCH` guard stops Docker serving the linter layers from
|
||||
cache, which is how a lint build returns success in under a second having
|
||||
linted nothing. The lint stage inside `Dockerfile` and its `COPY --from=lint`
|
||||
ordering hack are gone: that image now runs `make test` and `make build` only,
|
||||
because `script/check` calls `script/lint` and running it in a container would
|
||||
mean docker inside docker. `script/cibuild` builds the lint image first, then
|
||||
the test and build image.
|
||||
- 2026-08-09: Made `make docker` green and policy-conformant. Multi-stage
|
||||
Dockerfile: a lint stage runs `make fmt-check` and `make lint`, and the check
|
||||
stage takes a `COPY --from=lint` dependency on it before running `make check`
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"build": "script/build",
|
||||
"quak": "node ./dist/bin/quak.js",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint .",
|
||||
"fmt": "prettier --write .",
|
||||
"fmt-check": "prettier --check ."
|
||||
},
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/sh
|
||||
# script/check: run all checks (test, lint, fmt-check). Our own
|
||||
# extension to scripts-to-rule-them-all. Must not modify any files.
|
||||
#
|
||||
# script/lint builds Dockerfile.lint, so this script requires docker and
|
||||
# must never be run from inside a container: that is why the Dockerfile
|
||||
# image runs script/test and script/build rather than this.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
#!/bin/sh
|
||||
# script/cibuild: run the CI build. The Dockerfile runs script/check and
|
||||
# script/build, and CHECK_EPOCH differs on every invocation, so those two
|
||||
# layers cannot be served from Docker's cache: a green build here means
|
||||
# the checks ran now, not that a previous run was remembered. The layers
|
||||
# below the epoch (bootstrap, yarn install) are unaffected and stay
|
||||
# cached. A build that omits the argument fails by design.
|
||||
# script/cibuild: run the CI build, which is both images in a defined order.
|
||||
#
|
||||
# First script/lint, which builds Dockerfile.lint and is the one and only
|
||||
# place linting happens — it goes first so a lint failure is reported before
|
||||
# the slower suite runs. Then the Dockerfile image, which runs script/test
|
||||
# and script/build. CHECK_EPOCH and LINT_EPOCH differ on every invocation, so
|
||||
# neither the linters nor the suite can be served from Docker's cache: a
|
||||
# green build here means the checks ran now, not that a previous run was
|
||||
# remembered. The layers below the epochs (bootstrap, yarn install) are
|
||||
# unaffected and stay cached. A build that omits the arguments fails by
|
||||
# design.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
"$SCRIPT_DIR/lint"
|
||||
docker build --build-arg CHECK_EPOCH="$(date +%s)" .
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
# Identical in all repos; the tag comes from script/projectname.
|
||||
# CHECK_EPOCH is passed for the same reason script/cibuild passes it: the
|
||||
# Dockerfile refuses to build without it, so that no path to an image can
|
||||
# quietly serve the check and build layers from cache.
|
||||
# quietly serve the test and build layers from cache. This builds the test
|
||||
# and build image only; linting is a separate image, built by script/lint.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
|
||||
17
script/lint
17
script/lint
@@ -1,13 +1,24 @@
|
||||
#!/bin/sh
|
||||
# script/lint: run the linter (eslint plus a prettier check).
|
||||
# script/lint: run the linters. eslint and prettier are never run against
|
||||
# the working tree from here: linting runs via docker only, one way,
|
||||
# everywhere — script/lint builds Dockerfile.lint, which COPYs the repo into
|
||||
# the pinned node image and runs the linters as build steps. That works even
|
||||
# when the docker daemon is remote and bind mounts are impossible.
|
||||
#
|
||||
# LINT_EPOCH is passed on every invocation because no lint cache is wanted:
|
||||
# on an unchanged tree Docker would otherwise serve the linter layers, having
|
||||
# linted nothing, and still exit 0. Dockerfile.lint refuses to build without
|
||||
# the argument, so no path to a lint result can quietly come from cache.
|
||||
#
|
||||
# Nothing that runs inside a container may call this script; see the header
|
||||
# of Dockerfile.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
yarn run eslint .
|
||||
yarn run prettier --check .
|
||||
docker build --build-arg LINT_EPOCH="$(date +%s)" -f Dockerfile.lint .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
#
|
||||
# Runs lint and fmt-check but deliberately NOT the tests, so the TDD
|
||||
# red-phase commit (failing tests, no implementation yet) can land. CI
|
||||
# runs make check via docker build, which catches any branch that
|
||||
# ships red.
|
||||
# runs script/cibuild, which builds both images and so catches any
|
||||
# branch that ships red.
|
||||
#
|
||||
# script/lint is a docker build (Dockerfile.lint); docker is required to
|
||||
# commit, which is the point of linting one way, everywhere.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//
|
||||
// Neither shows up as a build failure, so they are asserted here.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -46,4 +46,19 @@ describe(".dockerignore", () => {
|
||||
it("leaves .gitignore in the build context for prettier", () => {
|
||||
expect(dockerignore).not.toContain(".gitignore");
|
||||
});
|
||||
|
||||
// Both images are built from this same context, and the lint image runs
|
||||
// eslint and prettier across it. BuildKit lets a `<dockerfile>.dockerignore`
|
||||
// shadow the root one for a single build; such a file would silently give
|
||||
// the lint build a different, unreviewed context — and eslint's flat config
|
||||
// does not ignore dot-directories, so a stray `.claude/` worktree would be
|
||||
// linted.
|
||||
it.each(["Dockerfile", "Dockerfile.lint"])(
|
||||
"is not shadowed by a per-Dockerfile ignore file for %s",
|
||||
(name) => {
|
||||
expect(existsSync(join(repoRoot, `${name}.dockerignore`))).toBe(
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
184
test/packaging/lint-docker.test.ts
Normal file
184
test/packaging/lint-docker.test.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
// Linting runs in Docker, one way, everywhere: `script/lint` builds
|
||||
// `Dockerfile.lint`, which COPYs the repo into a digest-pinned image and runs
|
||||
// eslint and prettier as build steps, so a successful build IS a clean lint.
|
||||
//
|
||||
// Three things can quietly undo that, and none of them shows up as a build
|
||||
// failure, which is why they are asserted here:
|
||||
//
|
||||
// 1. Recursion. `script/check` calls `script/lint`, and `script/lint` is now a
|
||||
// `docker build`. Anything that runs `make check` inside a container is
|
||||
// therefore asking for Docker inside Docker, and CI breaks. The image built
|
||||
// from `Dockerfile` runs the suite and the compile only; lint happens once,
|
||||
// in `Dockerfile.lint`.
|
||||
// 2. Cache. A lint build over an unchanged tree returns success in well under a
|
||||
// second having linted nothing. The `LINT_EPOCH` guard is what forces the
|
||||
// linter layers to execute, and it has to fail closed: an unset build
|
||||
// argument is the empty string, which is a perfectly stable cache key, so an
|
||||
// invocation that omits it must be rejected rather than served a cached
|
||||
// green.
|
||||
// 3. A host lint path surviving alongside the container one, which would let a
|
||||
// lint result come from an unpinned local toolchain.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { join } from "node:path";
|
||||
|
||||
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
|
||||
|
||||
const read = (name: string): string =>
|
||||
readFileSync(join(repoRoot, name), "utf-8");
|
||||
|
||||
// The executable lines of a shell script or Dockerfile: comments carry the
|
||||
// reasoning and frequently name the very commands these tests forbid, so they
|
||||
// would otherwise trigger every assertion below.
|
||||
const instructions = (name: string): string[] =>
|
||||
read(name)
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line !== "" && !line.startsWith("#"));
|
||||
|
||||
const lintScript = instructions("script/lint");
|
||||
const dockerfileLint = instructions("Dockerfile.lint");
|
||||
const dockerfile = instructions("Dockerfile");
|
||||
const cibuild = instructions("script/cibuild");
|
||||
|
||||
const has = (lines: string[], pattern: RegExp): boolean =>
|
||||
lines.some((line) => pattern.test(line));
|
||||
|
||||
describe("script/lint", () => {
|
||||
it("lints by building Dockerfile.lint", () => {
|
||||
expect(has(lintScript, /docker build .*-f Dockerfile\.lint/)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// The whole point of the ruling: no invocation of a linter against the
|
||||
// working tree survives, so a lint verdict can only come from the pinned
|
||||
// image.
|
||||
it("runs no linter on the host", () => {
|
||||
expect(has(lintScript, /eslint|prettier/)).toBe(false);
|
||||
});
|
||||
|
||||
// Without a fresh epoch the build is served from cache in under a second,
|
||||
// having linted nothing, and still exits 0.
|
||||
it("passes a fresh LINT_EPOCH on every run", () => {
|
||||
expect(
|
||||
has(lintScript, /--build-arg LINT_EPOCH="\$\(date \+%s\)"/),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dockerfile.lint", () => {
|
||||
// Tag references are server-mutable, so they are remote code execution.
|
||||
it("pins its base image by digest", () => {
|
||||
expect(has(dockerfileLint, /^FROM \S+@sha256:[0-9a-f]{64}/)).toBe(true);
|
||||
});
|
||||
|
||||
it("runs eslint as a build step", () => {
|
||||
expect(has(dockerfileLint, /^RUN .*eslint \./)).toBe(true);
|
||||
});
|
||||
|
||||
it("runs prettier as a build step", () => {
|
||||
expect(has(dockerfileLint, /^RUN .*prettier --check \./)).toBe(true);
|
||||
});
|
||||
|
||||
// An unset ARG is the empty string, and an empty string is a perfectly
|
||||
// stable cache key. Rejecting it is what stops a bare
|
||||
// `docker build -f Dockerfile.lint .` from reporting a green it did not
|
||||
// earn.
|
||||
it("refuses to build without LINT_EPOCH", () => {
|
||||
expect(has(dockerfileLint, /^ARG LINT_EPOCH$/)).toBe(true);
|
||||
expect(
|
||||
has(dockerfileLint, /^RUN \[ -n "\$LINT_EPOCH" \] \|\| exit 1$/),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// The guard only forces execution of the layers below it, so both linters
|
||||
// have to sit after it. Layer order is the mechanism, not a style choice.
|
||||
it("puts both linters below the epoch guard", () => {
|
||||
const guard = dockerfileLint.findIndex((line) =>
|
||||
/^RUN \[ -n "\$LINT_EPOCH" \]/.test(line),
|
||||
);
|
||||
const linters = dockerfileLint
|
||||
.map((line, index) => ({ line, index }))
|
||||
.filter(({ line }) => /^RUN .*(eslint|prettier)/.test(line));
|
||||
|
||||
expect(linters.length).toBeGreaterThan(0);
|
||||
for (const { line, index } of linters) {
|
||||
expect(
|
||||
index,
|
||||
`${line} must run below the LINT_EPOCH guard`,
|
||||
).toBeGreaterThan(guard);
|
||||
}
|
||||
});
|
||||
|
||||
// Dependency installation is the slow layer and has nothing to do with the
|
||||
// sources, so it caches separately: manifests first, sources afterwards.
|
||||
it("copies the manifests before the sources", () => {
|
||||
const manifests = dockerfileLint.findIndex((line) =>
|
||||
/^COPY package\.json yarn\.lock/.test(line),
|
||||
);
|
||||
const sources = dockerfileLint.findIndex((line) =>
|
||||
/^COPY \. \.$/.test(line),
|
||||
);
|
||||
|
||||
expect(manifests).toBeGreaterThanOrEqual(0);
|
||||
expect(sources).toBeGreaterThan(manifests);
|
||||
});
|
||||
|
||||
// script/lint is a docker build; a lint step that shelled out to it would
|
||||
// recurse.
|
||||
it("does not call script/lint or make lint", () => {
|
||||
expect(has(dockerfileLint, /make lint|script\/lint/)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dockerfile", () => {
|
||||
// `make check` runs script/lint, which is a docker build, so an image that
|
||||
// ran it would need a Docker daemon inside the container.
|
||||
it("does not run make check, make lint or script/lint", () => {
|
||||
expect(
|
||||
has(dockerfile, /make check|make lint|script\/(check|lint)/),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// The replaced lint stage took a `COPY --from=lint` dependency to order
|
||||
// itself before the check stage. Dockerfile.lint is that stage now, and
|
||||
// two definitions of how to lint is one too many.
|
||||
it("has no lint stage", () => {
|
||||
expect(has(dockerfile, /AS lint\b|--from=lint\b/)).toBe(false);
|
||||
});
|
||||
|
||||
it("still runs the suite and the build under the epoch guard", () => {
|
||||
expect(has(dockerfile, /^RUN make test$/)).toBe(true);
|
||||
expect(has(dockerfile, /^RUN make build$/)).toBe(true);
|
||||
expect(
|
||||
has(dockerfile, /^RUN \[ -n "\$CHECK_EPOCH" \] \|\| exit 1$/),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("script/cibuild", () => {
|
||||
// CI has to get both verdicts. Lint goes first so the fast failure is
|
||||
// reported before the suite runs.
|
||||
it("builds the lint image before the test and build image", () => {
|
||||
const lint = cibuild.findIndex((line) => /\/lint"/.test(line));
|
||||
const check = cibuild.findIndex((line) =>
|
||||
/docker build .*CHECK_EPOCH/.test(line),
|
||||
);
|
||||
|
||||
expect(lint).toBeGreaterThanOrEqual(0);
|
||||
expect(check).toBeGreaterThan(lint);
|
||||
});
|
||||
});
|
||||
|
||||
describe("package.json", () => {
|
||||
// `yarn lint` was a second, unpinned way to get a lint verdict, from
|
||||
// whatever eslint the working tree happened to have installed.
|
||||
it("exposes no host lint script", () => {
|
||||
const pkg = JSON.parse(read("package.json")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
expect(pkg.scripts.lint).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user