Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
41 changes: 33 additions & 8 deletions scripts/annotate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions scripts/annotate_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading