From a6e3e8de1dfbc23141adc43e3cc8edc838420d1a Mon Sep 17 00:00:00 2001 From: David Sherret Date: Mon, 7 Sep 2026 11:04:38 -0400 Subject: [PATCH] feat: list the remaining files when there are more than GitHub's annotation limit --- README.md | 2 +- scripts/annotate.mjs | 41 ++++++++++++++++++++++++++++++++-------- scripts/annotate_test.ts | 32 +++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4ba010f..b478cc3 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ To pass additional arguments to `dprint check`, pass them to the `args` input. ### Annotations -When a file isn't formatted, the action emits an error annotation for it, which GitHub shows on the pull request's changed files and in the check summary. The annotation points at the first change and lists the changed lines of the diff. This requires dprint 0.57 or later and `node` on the path (always the case on GitHub-hosted runners); otherwise the action only outputs the diffs to the log. Note that GitHub shows at most 10 error annotations per step, so the log is the complete list. +When a file isn't formatted, the action emits an error annotation for it, which GitHub shows on the pull request's changed files and in the check summary. The annotation points at the first change and lists the changed lines of the diff. This requires dprint 0.57 or later and `node` on the path (always the case on GitHub-hosted runners); otherwise the action only outputs the diffs to the log. GitHub shows at most 10 error annotations per step, so when more files than that are not formatted, the last annotation lists the remaining files instead. To disable annotations: diff --git a/scripts/annotate.mjs b/scripts/annotate.mjs index 8379f26..dab34c9 100644 --- a/scripts/annotate.mjs +++ b/scripts/annotate.mjs @@ -5,9 +5,11 @@ import fs from "node:fs"; import path from "node:path"; +// github shows at most this many error annotations per step +const MAX_ANNOTATIONS = 10; // the full diff is in the log, so keep the annotation short const MAX_ANNOTATION_DIFF_LINES = 10; -const MAX_ANNOTATION_DIFF_LENGTH = 4000; +const MAX_ANNOTATION_MESSAGE_LENGTH = 4000; const LINE_ENDINGS_MESSAGE = "Text differed by line endings."; const WINDOWS_LINE_ENDINGS_HINT = "Git on Windows runners checks out files with CRLF line endings, so consider only running this action on Linux: https://github.com/dprint/check#windows-line-endings"; @@ -32,7 +34,10 @@ const entries = fs.readFileSync(jsonlPath, "utf8") } }); -for (const entry of entries) { +// github ignores the annotations past its limit, so when there are too many +// files the last annotation lists the ones that wouldn't be shown instead +const annotatedCount = entries.length > MAX_ANNOTATIONS ? MAX_ANNOTATIONS - 1 : entries.length; +entries.forEach((entry, index) => { const relativePath = toRelativePath(entry.file, workspace); const changes = entry.diff == null ? undefined : parseChanges(entry.diff); // a diff that only changes line endings is every line of the file, so @@ -41,7 +46,13 @@ for (const entry of entries) { console.log(`from ${relativePath}:`); console.log(describeDiff(entry.diff, lineEndings)); console.log("--"); - console.log(annotation(relativePath, changes, lineEndings)); + if (index < annotatedCount) { + console.log(annotation(relativePath, changes, lineEndings)); + } +}); +if (annotatedCount < entries.length) { + const remainingPaths = entries.slice(annotatedCount).map((entry) => toRelativePath(entry.file, workspace)); + console.log(remainingFilesAnnotation(remainingPaths)); } if (entries.length > 0) { @@ -118,12 +129,26 @@ function formatChanges(changes) { } lines.push(...changeLines); } - let text = lines.join("\n"); - if (text.length > MAX_ANNOTATION_DIFF_LENGTH) { - truncated = true; - text = text.slice(0, MAX_ANNOTATION_DIFF_LENGTH); + const text = lines.join("\n"); + if (text.length > MAX_ANNOTATION_MESSAGE_LENGTH) { + return truncatedMessage(text.slice(0, MAX_ANNOTATION_MESSAGE_LENGTH), "diff"); } - return truncated ? `${text}\n(truncated, see the log for the full diff)` : text; + return truncated ? truncatedMessage(text, "diff") : text; +} + +/** Builds the `::error` workflow command listing the files GitHub's annotation limit would otherwise hide. */ +function remainingFilesAnnotation(relativePaths) { + const message = `${relativePaths.length} more files are not formatted. Run \`dprint fmt\` to fix.\n\n${ + relativePaths.join("\n") + }`; + const truncated = message.length > MAX_ANNOTATION_MESSAGE_LENGTH + ? truncatedMessage(message.slice(0, MAX_ANNOTATION_MESSAGE_LENGTH), "list") + : message; + return `::error title=dprint::${escapeMessage(truncated)}`; +} + +function truncatedMessage(text, whatWasTruncated) { + return `${text}\n(truncated, see the log for the full ${whatWasTruncated})`; } function formatChange(change) { diff --git a/scripts/annotate_test.ts b/scripts/annotate_test.ts index badb36d..922ebf7 100644 --- a/scripts/annotate_test.ts +++ b/scripts/annotate_test.ts @@ -120,6 +120,38 @@ Deno.test("truncates a long diff since the full diff is in the log", async () => } }); +Deno.test("lists the remaining files in the last annotation when there are more than github shows", async () => { + const workspace = await Deno.makeTempDir(); + const fileEntries = (count: number) => + Array.from({ length: count }, (_, i) => ({ + file: `${workspace}/file${i + 1}.json`, + diff: "--- original\n+++ formatted\n@@ -1 +1 @@\n-{\"a\":1}\n+{ \"a\": 1 }\n", + })); + const countAnnotations = (stdout: string) => stdout.split("\n").filter((line) => line.startsWith("::error ")).length; + try { + const tooMany = await runAnnotate(workspace, fileEntries(12)); + assertEquals(tooMany.stderr, ""); + assertEquals(tooMany.code, 0); + assertEquals(countAnnotations(tooMany.stdout), 10); + assertStringIncludes(tooMany.stdout, "::error file=file9.json,line=1,title=dprint::"); + assertEquals(tooMany.stdout.includes("::error file=file10.json"), false); + assertStringIncludes( + tooMany.stdout, + "::error title=dprint::3 more files are not formatted. Run `dprint fmt` to fix.%0A%0Afile10.json%0Afile11.json%0Afile12.json\n" + + "Found 12 not formatted files. Run dprint fmt to fix.\n", + ); + + // the limit itself still annotates every file + const atLimit = await runAnnotate(workspace, fileEntries(10)); + assertEquals(atLimit.code, 0); + assertEquals(countAnnotations(atLimit.stdout), 10); + assertStringIncludes(atLimit.stdout, "::error file=file10.json,line=1,title=dprint::"); + assertEquals(atLimit.stdout.includes("more files are not formatted"), false); + } finally { + await Deno.remove(workspace, { recursive: true }); + } +}); + Deno.test("makes carriage returns visible and escapes the annotation", async () => { const workspace = await Deno.makeTempDir(); try {