From f67a1c4d92b743215c4d94d0924565f5e1bf70d1 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 4 Sep 2026 12:04:24 +0000 Subject: [PATCH] test(lint-once): match RUN case-insensitively, follow buildx builds WIP round 9: items 1-3. Verification matrix still to re-run. --- test/packaging/lint-once.test.ts | 50 ++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/test/packaging/lint-once.test.ts b/test/packaging/lint-once.test.ts index da99f02..e1ee7ad 100644 --- a/test/packaging/lint-once.test.ts +++ b/test/packaging/lint-once.test.ts @@ -317,6 +317,21 @@ const scripts = packageScripts(); // the rest of the file, for the same reason every other dead end here is. const HEREDOC_OPEN = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/; +// Dockerfile instruction keywords are case-insensitive and are separated from +// their arguments by any run of whitespace, so `run yarn run prettier ...` and +// a tab-separated `RUN\tyarn run prettier ...` are both instructions the image +// really executes. A literal `line.startsWith("RUN ")` saw neither, and either +// one appended to the Dockerfile put a second prettier pass into the image +// that `script/cibuild` builds while this file still reported green. The +// separator is consumed by the match rather than by a fixed-width `slice`, so +// the captured command is the same string for a space and for a tab, and the +// heredoc terminator is read off that command exactly as before. +// +// Leading whitespace before the keyword — which a Dockerfile also permits — is +// already gone by this point: `joinContinuations` trims every line, so an +// indented instruction arrives here flush and needs nothing further. +const DOCKER_RUN = /^RUN\s+(.*)$/i; + const dockerRunCommands = (text: string): string[] => { const commands: string[] = []; let terminator: string | null = null; @@ -326,8 +341,9 @@ const dockerRunCommands = (text: string): string[] => { else commands.push(line); continue; } - if (!line.startsWith("RUN ")) continue; - const command = line.slice("RUN ".length); + const run = DOCKER_RUN.exec(line); + if (run === null) continue; + const command = run[1] as string; terminator = HEREDOC_OPEN.exec(command)?.[2] ?? null; commands.push(command); } @@ -346,8 +362,10 @@ const resolve = (node: string): string[] => { } // The `run:` steps of a workflow, in file order. `uses:` steps are actions, // not commands, and have no edges into this repo's graph. A `run: |` block - // would resolve to the bare `|`, which reaches nothing and therefore fails - // the count rather than passing quietly. + // resolves to the bare `|`, which reaches nothing — so the count is exactly + // what such a step does NOT move, and it is the pinned resolved list, not + // the count, that turns it red. See the exact-equality test at the bottom + // of this file. if (node.startsWith("workflow:")) { return executable(read(node.slice("workflow:".length))) .filter((line) => /^-?\s*run:\s*\S/.test(line)) @@ -390,6 +408,28 @@ const commandsOf = (node: string): string[] => { return commands; }; +// Which invocation shapes read as a build, and which do not. +// +// RECOGNISED: `docker build`, `docker buildx build`, and either of those +// reached through global flags between the command and the subcommand — +// `docker --context ci build`, `docker -H tcp://host:2375 build`, +// `docker --debug buildx build`. A global flag's separate argument is stepped +// over (`--context ci`), and an `=`-joined one is a single token +// (`--context=ci`). `docker buildx build` was the concrete miss: `\bdocker\s+ +// build\b` cannot reach across `buildx`, so `docker buildx build -f +// Dockerfile.extra .` in script/cibuild emitted no edge at all — not even the +// default `Dockerfile` one — and a tree running prettier twice reported green. +// +// NOT RECOGNISED, deliberately: `docker compose build`, which builds compose +// services out of a compose file this parser does not read, so resolving it +// against a Dockerfile path would be a wrong answer rather than a missing one; +// `docker builder build`; and any spelling that puts a non-flag word other +// than `buildx` between `docker` and `build`. Only `-`-prefixed tokens (plus +// their arguments) and the single literal `buildx` are stepped over, so +// `docker run -f build` and `docker image build`-style shapes do not match. +const DOCKER_BUILD = + /\bdocker(?:\s+-{1,2}[A-Za-z][\w-]*(?:=\S+)?(?:\s+[^-\s]\S*)?)*(?:\s+buildx)?\s+build\b/g; + const edgesOf = (line: string): string[] => { const edges: string[] = []; @@ -469,7 +509,7 @@ const edgesOf = (line: string): string[] => { // separators and nothing more: a separator inside quotes or a `$(...)` // substitution ends the slice anyway, and a newline-separated command list // is already one line per invocation by the time it gets here. - for (const match of line.matchAll(/\bdocker\s+build\b/g)) { + for (const match of line.matchAll(DOCKER_BUILD)) { const invocation = line .slice(match.index) .split(/&&|\|\||;|\|/)[0] as string;