diff --git a/README.md b/README.md index 9c94308..d2a132f 100644 --- a/README.md +++ b/README.md @@ -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 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, -and `package.json` exposes `fmt` and `fmt-check` as direct prettier calls — it is -the one of them that the check graph could plausibly have reached, and nothing -reaches it: neither `script/check` nor `script/precommit` calls it, so it never -contributes to their verdicts. Its own 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. +and `package.json` exposes `fmt` and `fmt-check` as direct prettier calls — it +is the one of them that the check graph could plausibly have reached, and +nothing reaches it: neither `script/check` nor `script/precommit` calls it, so +it never contributes to their verdicts. Its own 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 in that graph, which constrains the rest of the build. `script/check` calls `script/lint`, so `make check` cannot run inside diff --git a/test/packaging/lint-once.test.ts b/test/packaging/lint-once.test.ts index 7b2c4bf..da99f02 100644 --- a/test/packaging/lint-once.test.ts +++ b/test/packaging/lint-once.test.ts @@ -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 // 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