From 148de83fe375d5557367de57e916a6d06df5a0f4 Mon Sep 17 00:00:00 2001 From: Vitalii Perehonchuk Date: Thu, 23 Nov 2023 11:43:33 +0200 Subject: [PATCH 1/3] feat: contextual comments (#2530) * feat: contextual comments * chore: textual change for testing * fix: remove redundant step * fix: remove column & make the command single line * fix: reorder GH params * fix: variable command params * fix: better comment view * chore: cleanup * chore: remove test textual changes --- .github/workflows/spellcheck.yml | 20 +++---- .../{create-message.js => send-comments.js} | 54 +++++++++++-------- 2 files changed, 39 insertions(+), 35 deletions(-) rename scripts/{create-message.js => send-comments.js} (57%) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 49fe00062f..dbeb16c5e1 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -136,19 +136,11 @@ jobs: path: . - name: Create mapping run: node scripts/create-file-mapping.js ./index.txt ${{ needs.prepare-translation.outputs.translation }} > ./mapping.json && cat ./mapping.json - - id: create-message - name: Create message - run: node scripts/create-message.js ${{ needs.prepare-translation.outputs.translation }} > message.txt && cat ./message.txt - # - uses: reviewdog/action-setup@v1 - # - name: Send results - # run: | - # export REVIEWDOG_GITHUB_API_TOKEN=${{ secrets.GITHUB_TOKEN }} - # cat ./message.txt | reviewdog -efm="%A%f:%l:%c:%e:%k: %m%Z" -fail-on-error -reporter=github-pr-review -filter-mode=nofilter -name="LanguageTool" -level=info - - name: Send results - uses: mshick/add-pr-comment@v2 - with: - message-id: ${{ needs.prepare-translation.outputs.translation }} - message-path: ./message.txt - refresh-message-position: true + - env: + COMMIT_ID: ${{ github.event.pull_request.head.sha }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + name: Send results + run: node scripts/send-comments.js ${{ needs.prepare-translation.outputs.translation }} - name: Exit with error run: echo "Spelling errors found" && cat result.json && exit 1 diff --git a/scripts/create-message.js b/scripts/send-comments.js similarity index 57% rename from scripts/create-message.js rename to scripts/send-comments.js index 1532393854..9c3b194396 100644 --- a/scripts/create-message.js +++ b/scripts/send-comments.js @@ -1,8 +1,4 @@ -// Read LanguageTool output from ./results.json -// and txt-to-markdown mapping from ./mapping.json -// and print an errorformat message for each error -// to stdout. - +import { execSync } from "child_process"; import { readFileSync } from "fs"; const results = JSON.parse(readFileSync("./result.json")); @@ -57,24 +53,40 @@ for (const match of results.matches) { throw new Error(`Column not found in source file: ${sentence}`); } // const errorformatLine = `${markdownFile}:${startLine}:${startColumn}:${endLine}:${endColumn}: ${message}`; - // console.log(errorformatLine); - console.log(`## ${message}\n`); - console.log(`\`${markdownFile}:${startLine}:${startColumn}\n\``); - console.log(`${rule.description}:\n`); - console.log( - `> ${context.text.slice(0, context.offset)}**${context.text.slice( - context.offset, - context.offset + context.length, - )}**${context.text.slice(context.offset + context.length)}_`, - ); - console.log("Варіанти заміни:"); - if (replacements.length > 0) { + let comment = `### ${message}\n${rule.description}\n${rule.category.id}/${rule.id}: ${rule.description}\n`; + // console.log(`\`${markdownFile}:${startLine}:${startColumn}\n\``); + comment += `> ${context.text.slice(0, context.offset)}**${context.text.slice( + context.offset, + context.offset + context.length, + )}**${context.text.slice(context.offset + context.length)}`; + if (replacements?.length) { + comment += "\n\n#### Варіанти заміни\n"; // eslint-disable-next-line no-restricted-syntax for (const replacement of replacements) { - console.log(`- ${replacement.value}`); + // console.log(`- ${replacement.value}`); + comment += `- ${replacement.value}\n`; } - } else { - console.log("Немає"); } - console.log("\n"); + const parameters = { + body: comment, + commit_id: process.env.COMMIT_ID, + line: endLine, + path: markdownFile, + side: "RIGHT", + }; + if (startLine !== endLine) { + parameters.start_line = startLine; + parameters.start_side = "RIGHT"; + } + let command = `gh api repos/${process.env.GITHUB_REPOSITORY}/pulls/${process.env.PR_NUMBER}/comments`; + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(parameters)) { + command += + typeof value === "number" + ? ` -F ${key}=${value}` + : ` -f ${key}="${value}"`; + } + console.log(command); + console.log(`GH_TOKEN=${process.env.GH_TOKEN} ${command}`); + execSync(command, { stdio: "inherit" }); } From 4e3c9546915d10b847ce132372257f7ded9d6398 Mon Sep 17 00:00:00 2001 From: Vitalii Perehonchuk Date: Thu, 23 Nov 2023 13:55:17 +0200 Subject: [PATCH 2/3] fix: better comment template --- scripts/send-comments.js | 43 +++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/scripts/send-comments.js b/scripts/send-comments.js index 9c3b194396..1f4a3e0973 100644 --- a/scripts/send-comments.js +++ b/scripts/send-comments.js @@ -12,6 +12,12 @@ const markdown = readFileSync(markdownFile, "utf8"); const markdownRunes = Array.from(markdown); +const MARKDOWN_ESCAPE_REGEX = /([!"#'()*+.[\\\]_`{}-])/g; + +function escapeTextForMarkdown(text) { + return text.replaceAll(MARKDOWN_ESCAPE_REGEX, "\\$1"); +} + function convertOffsetToLineAndColumn(offset) { let line = 1; let column = 1; @@ -29,10 +35,17 @@ function convertOffsetToLineAndColumn(offset) { // eslint-disable-next-line no-restricted-syntax for (const match of results.matches) { - const { context, message, offset, replacements, rule, sentence } = match; - let { length } = match; + const { context, message, replacements, rule, sentence, shortMessage } = + match; + let { length, offset } = match; + let start; + length += 1; + while (!start) { + offset += 1; + length -= 1; + start = Number.parseInt(mapping[offset], 10); + } let endOffset = offset + length; - const start = Number.parseInt(mapping[offset], 10); let end; endOffset -= 1; length -= 1; @@ -53,18 +66,29 @@ for (const match of results.matches) { throw new Error(`Column not found in source file: ${sentence}`); } // const errorformatLine = `${markdownFile}:${startLine}:${startColumn}:${endLine}:${endColumn}: ${message}`; - let comment = `### ${message}\n${rule.description}\n${rule.category.id}/${rule.id}: ${rule.description}\n`; + let comment = ""; + if (shortMessage || rule.description) { + comment += `### ${escapeTextForMarkdown( + shortMessage || rule.description, + )}\n\n`; + } + comment += `${escapeTextForMarkdown(message)}\n${rule.category.id}/${ + rule.id + }: ${escapeTextForMarkdown(rule.description)}\n`; // console.log(`\`${markdownFile}:${startLine}:${startColumn}\n\``); - comment += `> ${context.text.slice(0, context.offset)}**${context.text.slice( - context.offset, - context.offset + context.length, - )}**${context.text.slice(context.offset + context.length)}`; + comment += `> ${escapeTextForMarkdown( + context.text.slice(0, context.offset), + )}**${escapeTextForMarkdown( + context.text.slice(context.offset, context.offset + context.length), + )}**${escapeTextForMarkdown( + context.text.slice(context.offset + context.length), + )}`; if (replacements?.length) { comment += "\n\n#### Варіанти заміни\n"; // eslint-disable-next-line no-restricted-syntax for (const replacement of replacements) { // console.log(`- ${replacement.value}`); - comment += `- ${replacement.value}\n`; + comment += `- ${escapeTextForMarkdown(replacement.value)}\n`; } } const parameters = { @@ -87,6 +111,7 @@ for (const match of results.matches) { : ` -f ${key}="${value}"`; } console.log(command); + // command = command.replaceAll("\n", "\\\n"); console.log(`GH_TOKEN=${process.env.GH_TOKEN} ${command}`); execSync(command, { stdio: "inherit" }); } From 9c8a25b61350011f69ef58fe4cc34e93841a427d Mon Sep 17 00:00:00 2001 From: Vitalii Perehonchuk Date: Thu, 23 Nov 2023 17:40:21 +0200 Subject: [PATCH 3/3] fix: loop exit cond --- .github/workflows/spellcheck.yml | 9 +- scripts/send-comments.js | 200 +++++++++++++++++-------------- 2 files changed, 116 insertions(+), 93 deletions(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index dbeb16c5e1..96de2c7105 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -70,15 +70,16 @@ jobs: name: Convert markdown to plain text run: | file=${{ needs.prepare-translation.outputs.translation }} + tmpHtmlFile=$(echo $file | sed 's/\.md/\.html/') + pandoc -f markdown -t html -o $tmpHtmlFile $file newFileName=$(echo $file | sed 's/\.md/\.txt/') - pandoc -f markdown -t plain -o $newFileName $file + pandoc -f html -t plain -o $newFileName $tmpHtmlFile echo "translation=$newFileName" >> $GITHUB_OUTPUT echo $newFileName - # Error if translation file is not found - if: steps.md2txt.outputs.translation == '' name: Check translation is found run: echo "No translation file found" && exit 1 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: "temurin" java-version: "8" @@ -98,6 +99,7 @@ jobs: run: | cd LanguageTool-6.3 java -jar languagetool-commandline.jar -d ${{steps.disabled-rules.outputs.disabled_rules}} -l uk --json ../${{ steps.md2txt.outputs.translation }} > ../result.json + cat ../result.json matches=$(cat ../result.json | jq '.matches') # Check if matches equal [] echo "has_matches=$(if [ "$matches" == "[]" ]; then echo "false"; else echo "true"; fi)" >> $GITHUB_OUTPUT @@ -142,5 +144,6 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} name: Send results run: node scripts/send-comments.js ${{ needs.prepare-translation.outputs.translation }} + timeout-minutes: 5 - name: Exit with error run: echo "Spelling errors found" && cat result.json && exit 1 diff --git a/scripts/send-comments.js b/scripts/send-comments.js index 1f4a3e0973..9404d39b76 100644 --- a/scripts/send-comments.js +++ b/scripts/send-comments.js @@ -1,24 +1,12 @@ import { execSync } from "child_process"; import { readFileSync } from "fs"; -const results = JSON.parse(readFileSync("./result.json")); -const MARKDOWN_FILE_ARG_INDEX = 2; -/** - * @type {Record} - */ -const mapping = JSON.parse(readFileSync("./mapping.json")); -const markdownFile = process.argv[MARKDOWN_FILE_ARG_INDEX]; -const markdown = readFileSync(markdownFile, "utf8"); - -const markdownRunes = Array.from(markdown); - const MARKDOWN_ESCAPE_REGEX = /([!"#'()*+.[\\\]_`{}-])/g; - function escapeTextForMarkdown(text) { return text.replaceAll(MARKDOWN_ESCAPE_REGEX, "\\$1"); } -function convertOffsetToLineAndColumn(offset) { +function convertOffsetToLineAndColumn(markdownRunes, offset) { let line = 1; let column = 1; for (let index = 0; index < offset; index += 1) { @@ -32,86 +20,118 @@ function convertOffsetToLineAndColumn(offset) { } return [line, column]; } +try { + const results = JSON.parse(readFileSync("./result.json")); + const MARKDOWN_FILE_ARG_INDEX = 2; + /** + * @type {Record} + */ + const mapping = JSON.parse(readFileSync("./mapping.json")); + const markdownFile = process.argv[MARKDOWN_FILE_ARG_INDEX]; + const markdown = readFileSync(markdownFile, "utf8"); -// eslint-disable-next-line no-restricted-syntax -for (const match of results.matches) { - const { context, message, replacements, rule, sentence, shortMessage } = - match; - let { length, offset } = match; - let start; - length += 1; - while (!start) { - offset += 1; - length -= 1; - start = Number.parseInt(mapping[offset], 10); - } - let endOffset = offset + length; - let end; - endOffset -= 1; - length -= 1; - while (!end) { - endOffset += 1; + const markdownRunes = Array.from(markdown); + + const TOTAL_TIMEOUT = 180_000; + const timeout = setTimeout(() => { + throw new Error("Timeout"); + }, TOTAL_TIMEOUT); + const mappingMaxOffset = Math.max(...Object.keys(mapping)); + + // eslint-disable-next-line no-restricted-syntax + for (const match of results.matches) { + const { context, message, replacements, rule, sentence, shortMessage } = + match; + let { length, offset } = match; + let start; length += 1; - end = Number.parseInt(mapping[endOffset], 10); - } - console.error(start, end); - const [startLine, startColumn] = convertOffsetToLineAndColumn(start); - const [endLine, endColumn] = convertOffsetToLineAndColumn(end); - if (endLine < startLine) { - console.error(startLine, endLine); - throw new Error(`Line not found in source file: ${sentence}`); - } - if (endLine === startLine && endColumn < startColumn) { - console.error(startColumn, endColumn); - throw new Error(`Column not found in source file: ${sentence}`); - } - // const errorformatLine = `${markdownFile}:${startLine}:${startColumn}:${endLine}:${endColumn}: ${message}`; - let comment = ""; - if (shortMessage || rule.description) { - comment += `### ${escapeTextForMarkdown( - shortMessage || rule.description, - )}\n\n`; - } - comment += `${escapeTextForMarkdown(message)}\n${rule.category.id}/${ - rule.id - }: ${escapeTextForMarkdown(rule.description)}\n`; - // console.log(`\`${markdownFile}:${startLine}:${startColumn}\n\``); - comment += `> ${escapeTextForMarkdown( - context.text.slice(0, context.offset), - )}**${escapeTextForMarkdown( - context.text.slice(context.offset, context.offset + context.length), - )}**${escapeTextForMarkdown( - context.text.slice(context.offset + context.length), - )}`; - if (replacements?.length) { - comment += "\n\n#### Варіанти заміни\n"; + while (!start && offset <= mappingMaxOffset) { + offset += 1; + length -= 1; + start = Number.parseInt(mapping[offset], 10); + } + let endOffset = offset + length; + let end; + endOffset -= 1; + length -= 1; + while (!end && endOffset <= mappingMaxOffset) { + endOffset += 1; + length += 1; + end = Number.parseInt(mapping[endOffset], 10); + } + console.error(start, end); + const [startLine, startColumn] = convertOffsetToLineAndColumn( + markdownRunes, + start, + ); + const [endLine, endColumn] = convertOffsetToLineAndColumn( + markdownRunes, + end, + ); + if (endLine < startLine) { + console.error(startLine, endLine); + throw new Error(`Line not found in source file: ${sentence}`); + } + if (endLine === startLine && endColumn < startColumn) { + console.error(startColumn, endColumn); + throw new Error(`Column not found in source file: ${sentence}`); + } + // const errorformatLine = `${markdownFile}:${startLine}:${startColumn}:${endLine}:${endColumn}: ${message}`; + let comment = ""; + if (shortMessage || rule.description) { + comment += `### ${escapeTextForMarkdown( + shortMessage || rule.description, + )}\n\n`; + } + comment += `${escapeTextForMarkdown(message)}\n${rule.category.id}/${ + rule.id + }: ${escapeTextForMarkdown(rule.description)}\n`; + // console.log(`\`${markdownFile}:${startLine}:${startColumn}\n\``); + comment += `> ${escapeTextForMarkdown( + context.text.slice(0, context.offset), + )}**${escapeTextForMarkdown( + context.text.slice(context.offset, context.offset + context.length), + )}**${escapeTextForMarkdown( + context.text.slice(context.offset + context.length), + )}`; + if (replacements?.length) { + comment += "\n\n#### Варіанти заміни\n"; + // eslint-disable-next-line no-restricted-syntax + for (const replacement of replacements) { + // console.log(`- ${replacement.value}`); + comment += `- ${escapeTextForMarkdown(replacement.value)}\n`; + } + } + const parameters = { + body: comment, + commit_id: process.env.COMMIT_ID, + line: endLine, + path: markdownFile, + side: "RIGHT", + }; + if (startLine !== endLine) { + parameters.start_line = startLine; + parameters.start_side = "RIGHT"; + } + let command = `gh api repos/${process.env.GITHUB_REPOSITORY}/pulls/${process.env.PR_NUMBER}/comments`; // eslint-disable-next-line no-restricted-syntax - for (const replacement of replacements) { - // console.log(`- ${replacement.value}`); - comment += `- ${escapeTextForMarkdown(replacement.value)}\n`; + for (const [key, value] of Object.entries(parameters)) { + command += + typeof value === "number" + ? ` -F ${key}=${value}` + : ` -f ${key}="${value}"`; } + console.log(command); + // command = command.replaceAll("\n", "\\\n"); + console.log(`GH_TOKEN=${process.env.GH_TOKEN} ${command}`); + execSync(command, { stdio: "inherit", timeout: 60_000 }); } - const parameters = { - body: comment, - commit_id: process.env.COMMIT_ID, - line: endLine, - path: markdownFile, - side: "RIGHT", - }; - if (startLine !== endLine) { - parameters.start_line = startLine; - parameters.start_side = "RIGHT"; - } - let command = `gh api repos/${process.env.GITHUB_REPOSITORY}/pulls/${process.env.PR_NUMBER}/comments`; - // eslint-disable-next-line no-restricted-syntax - for (const [key, value] of Object.entries(parameters)) { - command += - typeof value === "number" - ? ` -F ${key}=${value}` - : ` -f ${key}="${value}"`; - } - console.log(command); - // command = command.replaceAll("\n", "\\\n"); - console.log(`GH_TOKEN=${process.env.GH_TOKEN} ${command}`); - execSync(command, { stdio: "inherit" }); + clearTimeout(timeout); +} catch (error) { + console.error(error); + process.exit(1); } +process.on("unhandledRejection", (error) => { + console.error(error); + process.exit(1); +});