test(lint-once): pin the two-build and later-flag shapes
All checks were successful
check / check (push) Successful in 15s

The false green: script/lint chaining a second docker build after the
lint image ran prettier twice and counted once. The misresolution: a
bare docker build followed by cp -f resolved to the cp's file. Pin
both, and each separator that bounds an invocation.
This commit is contained in:
user
2026-09-04 11:51:17 +00:00
parent 88a5fcaa87
commit ffc817522e
2 changed files with 45 additions and 7 deletions

View File

@@ -129,13 +129,14 @@ invocation graph, so a second pass cannot creep back in unnoticed.
`script/fmt-check` remains as a standalone entrypoint for asking the formatting `script/fmt-check` remains as a standalone entrypoint for asking the formatting
question on its own, without docker and without the rest of lint. It is not the question on its own, without docker and without the rest of lint. It is not the
last formatting path on the host — `script/fmt` writes with the host prettier, last formatting path on the host — `script/fmt` writes with the host prettier,
and `package.json` exposes `fmt` and `fmt-check` as direct prettier calls — it is and `package.json` exposes `fmt` and `fmt-check` as direct prettier calls — it
the one of them that the check graph could plausibly have reached, and nothing is the one of them that the check graph could plausibly have reached, and
reaches it: neither `script/check` nor `script/precommit` calls it, so it never nothing reaches it: neither `script/check` nor `script/precommit` calls it, so
contributes to their verdicts. Its own verdict cannot drift from the container's: prettier is pinned it never contributes to their verdicts. Its own verdict cannot drift from the
to an exact version, installed from `yarn.lock` under `--frozen-lockfile` in container's: prettier is pinned to an exact version, installed from `yarn.lock`
both places, and reads `.gitignore` as its default ignore file — which is why under `--frozen-lockfile` in both places, and reads `.gitignore` as its default
`.dockerignore` deliberately keeps `.gitignore` in the build context. ignore file — which is why `.dockerignore` deliberately keeps `.gitignore` in
the build context.
Lint happens in exactly one place in that graph, which constrains the rest of Lint happens in exactly one place in that graph, which constrains the rest of
the build. `script/check` calls `script/lint`, so `make check` cannot run inside the build. `script/check` calls `script/lint`, so `make check` cannot run inside

View File

@@ -1094,6 +1094,43 @@ describe("the resolver reads what the shell would run", () => {
]); ]);
}); });
// The false green this file exists to prevent, in the one shape it still
// had: `script/lint` chaining a second `docker build` after the lint image
// built prettier twice and counted once, because a single `test` and a
// single `exec` over the line produced exactly one edge and resolved it to
// the first file named. That is the same failure as the original duplicate
// pass — two invocations, one verdict — reached through the walker instead
// of through the repo.
it("follows both docker builds when one line chains two", () => {
expect(
edgesOf(
'docker build --build-arg LINT_EPOCH="$(date +%s)" -f Dockerfile.lint . && docker build -f Dockerfile.extra .',
),
).toEqual(["docker:Dockerfile.lint", "docker:Dockerfile.extra"]);
});
// The other half: a flag search over the whole line reads a *later*
// command's `-f` as the build's own. A bare `docker build .` followed by
// `cp -f` resolved to `Dockerfile.lint`, which both counts an invocation
// that never happens and drops the `Dockerfile` edge — the half of the CI
// graph the second entrypoint exists to cover. Each separator that bounds
// an invocation is pinned, since the bounding is a split rather than a
// shell parse and dropping one of them would be silent.
it("does not read a later command's -f as the build's", () => {
expect(
edgesOf("docker build . && cp -f Dockerfile.lint /tmp/x"),
).toEqual(["docker:Dockerfile"]);
expect(
edgesOf("docker build . || cp -f Dockerfile.lint /tmp/x"),
).toEqual(["docker:Dockerfile"]);
expect(
edgesOf("docker build . ; cp -f Dockerfile.lint /tmp/x"),
).toEqual(["docker:Dockerfile"]);
expect(edgesOf("docker build . | tee -f Dockerfile.lint")).toEqual([
"docker:Dockerfile",
]);
});
// This exact-equality assertion is also the block-scalar guard, which is // This exact-equality assertion is also the block-scalar guard, which is
// not obvious from its name: a `run: |` step resolves to the bare `|`, // not obvious from its name: a `run: |` step resolves to the bare `|`,
// which reaches nothing, so the prettier count would stay 1 no matter what // which reaches nothing, so the prettier count would stay 1 no matter what