All checks were successful
check / check (push) Successful in 1m2s
Linting now happens in one place only: a new root Dockerfile.lint copies the repo into the digest-pinned node image already used by Dockerfile and runs eslint and prettier as build steps, so a successful build is a clean lint. script/lint is reduced to building it, which also works where the docker daemon is remote and bind mounts are impossible. No host lint path survives: the "lint" script is gone from package.json, so there is no second, unpinned way to get a lint verdict. Caching is waived for lint, because a lint build over an unchanged tree returns success in well under a second having linted nothing. LINT_EPOCH is the cache buster and it fails closed exactly as CHECK_EPOCH does: an unset ARG is the empty string, which is a perfectly stable cache key, so the guard rejects it and a bare `docker build -f Dockerfile.lint .` errors out instead of serving a green it did not earn. Both linters sit below the guard, so a fresh epoch forces them to execute while the bootstrap and dependency layers above stay cached. That makes script/lint a docker build, which nothing inside a container may call. script/check calls script/lint, so the Dockerfile image can no longer run make check: the lint stage and its COPY --from=lint ordering hack are deleted, and the remaining stage runs make test and make build under the existing CHECK_EPOCH guard. script/cibuild is now the composite gate and builds the lint image first, so a lint failure is reported before the slower suite runs. The .dockerignore exclusions are unchanged and still apply to the lint build, including the .claude/ exclusion (eslint's flat config does not ignore dot-directories, so a nested worktree in the context would be linted) and the deliberate exception that keeps .gitignore in the context for prettier. A new test asserts no per-Dockerfile ignore file shadows the root one for either image, and test/packaging/lint-docker.test.ts asserts the whole shape: the docker-only lint path, the digest pin, manifests copied before sources, the fail-closed guard with both linters below it, the absence of a lint stage or make check in Dockerfile, and the build order in script/cibuild.
65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
// The Docker build context is load-bearing in two directions, and both
|
|
// failures are silent.
|
|
//
|
|
// Excluding too little: a worktree left under `.claude/` is copied into the
|
|
// image, vitest globs its `test/` tree as well as the real one, and the
|
|
// containerised `make check` runs the whole suite twice over while reporting
|
|
// success. A compiled `bin/quak` is ~100 MB of context nobody needs.
|
|
//
|
|
// Excluding too much: Prettier 3 reads `.gitignore` as a default ignore file,
|
|
// so dropping it from the context silently changes which files
|
|
// `make fmt-check` looks at inside the image compared to the host.
|
|
//
|
|
// Neither shows up as a build failure, so they are asserted here.
|
|
import { describe, expect, it } from "vitest";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { join } from "node:path";
|
|
|
|
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
|
|
const patterns = (name: string): string[] =>
|
|
readFileSync(join(repoRoot, name), "utf-8")
|
|
.split("\n")
|
|
.map((line) => line.trim())
|
|
.filter((line) => line !== "" && !line.startsWith("#"));
|
|
|
|
const dockerignore = patterns(".dockerignore");
|
|
|
|
describe(".dockerignore", () => {
|
|
// Everything here is either generated, enormous, or secret. `.claude/` is
|
|
// the correctness one: see the header comment and issue #25.
|
|
it.each([
|
|
".claude/",
|
|
".quak/",
|
|
"bin/quak",
|
|
"node_modules",
|
|
"coverage",
|
|
"dist",
|
|
".vitest-cache/",
|
|
".nyc_output/",
|
|
"*.tsbuildinfo",
|
|
])("keeps %s out of the build context", (pattern) => {
|
|
expect(dockerignore).toContain(pattern);
|
|
});
|
|
|
|
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,
|
|
);
|
|
},
|
|
);
|
|
});
|