From 262acf083c67b9eddb29e85121d9cbaea3a331b4 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:29:47 -0600 Subject: [PATCH 1/6] ci: Share one summary-comment script across the PR checks The four bash checks each carried a copy of the same comment step, and none of them minimized the summary once the findings were fixed, unlike the spell check. dev/post-check-comment.sh now posts, updates, minimizes, and reopens the summary for all of them, and never comments on a PR that had nothing to report. Also: - result=found everywhere (links and redirects said broken) - the clean message comes from the check script's report in every workflow, all saying 'This revision' - the hostnames job name says (advisory) like the other advisory checks - GH_TOKEN, PR_NUMBER, BASE_SHA, LINK_BASE, and RUN_URL are job-level env - the check scripts are checked out from the merge commit, like the workflow file itself. A branch that predates a check has no script for it, so the check crashed and posted a 'could not run' comment on a clean PR (#2118); 27 open PRs predate check-duplicates.mjs - vercel-ignore-build.sh names the Slack app manifest by its current name Amp-Thread-ID: https://ampcode.com/threads/T-01a18f4b-0163-7216-a915-372c4b6288cb Co-authored-by: Amp --- .github/workflows/check-duplicates.yml | 49 +++++++-------------- .github/workflows/check-hostnames.yml | 55 ++++++++--------------- .github/workflows/check-links.yml | 59 +++++++++---------------- .github/workflows/check-redirects.yml | 60 +++++++++----------------- AGENTS.md | 12 +++++- dev/check-links.mjs | 2 +- dev/check-redirects.mjs | 2 +- dev/post-check-comment.sh | 42 ++++++++++++++++++ dev/vercel-ignore-build.sh | 3 +- 9 files changed, 131 insertions(+), 153 deletions(-) create mode 100755 dev/post-check-comment.sh diff --git a/.github/workflows/check-duplicates.yml b/.github/workflows/check-duplicates.yml index d295a7a28..6006b6514 100644 --- a/.github/workflows/check-duplicates.yml +++ b/.github/workflows/check-duplicates.yml @@ -10,6 +10,7 @@ on: paths: - 'docs/**' - 'dev/check-duplicates.mjs' + - 'dev/post-check-comment.sh' - 'dev/sync-review-comments.sh' - '.github/workflows/check-duplicates.yml' @@ -26,6 +27,13 @@ jobs: check-duplicates: name: Duplicated content introduced by this PR (advisory) runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + # File links in the report open the file on the PR branch + LINK_BASE: ${{ github.event.pull_request.head.repo.html_url }}/blob/${{ github.event.pull_request.head.ref }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - name: Check out pull request head uses: actions/checkout@v7 @@ -33,9 +41,13 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 + # The workflow file is read from the merge commit, so run the scripts + # from there too: a branch that predates a check has no script for it, + # and the merge commit also has this PR's changes to them + - name: Check out the check scripts from the merge commit + run: git fetch --quiet origin "$GITHUB_SHA" && git checkout --quiet "$GITHUB_SHA" -- dev + - name: Check out merge base - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | merge_base=$(git merge-base "$BASE_SHA" HEAD) git worktree add "$RUNNER_TEMP/base" "$merge_base" @@ -52,9 +64,6 @@ jobs: - name: Find duplicated passages introduced by this PR id: check - env: - # File links in the report open the file on the PR branch - LINK_BASE: ${{ github.event.pull_request.head.repo.html_url }}/blob/${{ github.event.pull_request.head.ref }} run: | if ! [ -s "$RUNNER_TEMP/base-duplicates.json" ]; then echo "::warning::check-duplicates crashed on the base branch, so this PR was not checked" @@ -79,33 +88,8 @@ jobs: # Fork PRs get a read-only token; the report is still in the job log if: github.event.pull_request.head.repo.full_name == github.repository env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} RESULT: ${{ steps.check.outputs.result }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - marker='' - existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ - --paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1) - - # Comment when there is something to report, or an earlier report to resolve - case "$RESULT" in - found) - { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; - crashed) - printf '%s\n### ⚠️ The duplicated content check could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ - "$marker" "$RUN_URL" > "$RUNNER_TEMP/comment.md" ;; - *) - [ -n "$existing_comment" ] || exit 0 - { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; - esac - - if [ -n "$existing_comment" ]; then - gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing_comment" \ - --field body=@"$RUNNER_TEMP/comment.md" - else - gh pr comment "$PR_NUMBER" --body-file "$RUNNER_TEMP/comment.md" - fi + run: dev/post-check-comment.sh '' "$RESULT" "$RUNNER_TEMP/report.md" - name: Point out the duplicates as review comments # One comment per added passage, kept in sync with the findings; @@ -113,7 +97,4 @@ jobs: if: >- github.event.pull_request.head.repo.full_name == github.repository && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} run: dev/sync-review-comments.sh '' - existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ - --paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1) - - # Comment when there is something to report, or an earlier report to resolve - case "$RESULT" in - found) - { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; - crashed) - printf '%s\n### ⚠️ The example hostnames check could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ - "$marker" "$RUN_URL" > "$RUNNER_TEMP/comment.md" ;; - *) - [ -n "$existing_comment" ] || exit 0 - { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; - esac - - if [ -n "$existing_comment" ]; then - gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing_comment" \ - --field body=@"$RUNNER_TEMP/comment.md" - else - gh pr comment "$PR_NUMBER" --body-file "$RUNNER_TEMP/comment.md" - fi + run: dev/post-check-comment.sh '' "$RESULT" "$RUNNER_TEMP/report.md" - name: Suggest fixes as review comments # One suggested change per flagged line, kept in sync with the @@ -95,7 +81,4 @@ jobs: if: >- github.event.pull_request.head.repo.full_name == github.repository && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} run: dev/sync-review-comments.sh '' - existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ - --paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1) - - # Comment when there is something to report, or an earlier report to resolve - case "$RESULT" in - broken) - { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; - crashed) - printf '%s\n### ⚠️ The links check could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ - "$marker" "$RUN_URL" > "$RUNNER_TEMP/comment.md" ;; - *) - [ -n "$existing_comment" ] || exit 0 - printf '%s\n### ✅ This revision introduces no broken links\n' \ - "$marker" > "$RUNNER_TEMP/comment.md" ;; - esac - - if [ -n "$existing_comment" ]; then - gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing_comment" \ - --field body=@"$RUNNER_TEMP/comment.md" - else - gh pr comment "$PR_NUMBER" --body-file "$RUNNER_TEMP/comment.md" - fi + run: dev/post-check-comment.sh '' "$RESULT" "$RUNNER_TEMP/report.md" - name: Suggest fixes as review comments # One suggested change per finding with a fix, kept in sync with the # findings; see dev/sync-review-comments.sh if: >- github.event.pull_request.head.repo.full_name == github.repository - && contains(fromJSON('["clean", "broken"]'), steps.check.outputs.result) - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} + && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) run: dev/sync-review-comments.sh '' - existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ - --paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1) - - # Comment when there is something to report, or an earlier report to resolve - case "$RESULT" in - broken) - { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; - crashed) - printf '%s\n### ⚠️ The redirects check could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ - "$marker" "$RUN_URL" > "$RUNNER_TEMP/comment.md" ;; - *) - [ -n "$existing_comment" ] || exit 0 - printf '%s\n### ✅ This revision breaks no redirects\n' \ - "$marker" > "$RUNNER_TEMP/comment.md" ;; - esac - - if [ -n "$existing_comment" ]; then - gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing_comment" \ - --field body=@"$RUNNER_TEMP/comment.md" - else - gh pr comment "$PR_NUMBER" --body-file "$RUNNER_TEMP/comment.md" - fi + run: dev/post-check-comment.sh '' "$RESULT" "$RUNNER_TEMP/report.md" - name: Suggest fixes as review comments # One suggested change per fixable entry this PR added, kept in sync # with the findings; see dev/sync-review-comments.sh if: >- github.event.pull_request.head.repo.full_name == github.repository - && contains(fromJSON('["clean", "broken"]'), steps.check.outputs.result) - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} + && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) run: dev/sync-review-comments.sh '' report.md +# The report is the Markdown the check script wrote, used as the comment body +# for clean and found. Needs GH_TOKEN, GITHUB_REPOSITORY, GITHUB_WORKFLOW, +# PR_NUMBER, and RUN_URL. +set -eu +marker=$1 +result=$2 +report=$3 +issues="repos/$GITHUB_REPOSITORY/issues" + +existing=$(gh api "$issues/$PR_NUMBER/comments" --paginate \ + --jq ".[] | select(.body | startswith(\"$marker\")) | \"\(.id) \(.node_id)\"" | head -n 1) + +if [ "$result" = crashed ]; then + body=$(printf '%s\n### ⚠️ %s could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ + "$marker" "$GITHUB_WORKFLOW" "$RUN_URL") +else + body=$(printf '%s\n' "$marker"; cat "$report") +fi + +if [ -z "$existing" ]; then + [ "$result" != clean ] || exit 0 + gh api --method POST "$issues/$PR_NUMBER/comments" --raw-field body="$body" --silent + exit 0 +fi + +gh api --method PATCH "$issues/comments/${existing% *}" --raw-field body="$body" --silent + +# Minimizing is GraphQL-only. Both mutations are idempotent +if [ "$result" = clean ]; then + mutation="minimizeComment(input: {subjectId: \$id, classifier: RESOLVED})" +else + mutation="unminimizeComment(input: {subjectId: \$id})" +fi +gh api graphql --raw-field id="${existing#* }" \ + --raw-field query="mutation(\$id: ID!) { $mutation { clientMutationId } }" --silent diff --git a/dev/vercel-ignore-build.sh b/dev/vercel-ignore-build.sh index 18d5a7360..03f6f6689 100755 --- a/dev/vercel-ignore-build.sh +++ b/dev/vercel-ignore-build.sh @@ -17,9 +17,10 @@ exec git diff --quiet HEAD^ HEAD -- . \ ':(exclude)dev/check-redirects.mjs' \ ':(exclude)dev/check-spelling.mjs' \ ':(exclude)dev/example-hostnames.json' \ + ':(exclude)dev/post-check-comment.sh' \ ':(exclude)dev/post-spelling-review.mjs' \ - ':(exclude)dev/slack-app-vercel-build-report.json' \ ':(exclude)dev/sync-review-comments.sh' \ + ':(exclude)dev/vercel-build-log-slack-app-manifest.json' \ ':(exclude)dev/vercel-build-log.mjs' \ ':(exclude)dev/vercel-ignore-build.sh' \ ':(exclude)dev/verify-links-live.mjs' \ From 5a952863dc78be1a6ed6959f3108867f021e1b8c Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:29:55 -0600 Subject: [PATCH 2/6] ci/spellcheck: Report through the same scripts as the other checks check-spelling.mjs takes --diff, --format markdown, --link-base, and --review like the other check scripts, and the workflow posts its summary with dev/post-check-comment.sh and its inline comments with dev/sync-review-comments.sh. That retires dev/post-spelling-review.mjs, which carried its own GitHub client, summary rendering, and review-comment sync. The comment markers stay cspell-report and cspell-finding, so comments already on open PRs keep being updated. The workflow also gets the concurrency block the other checks have, so two pushes in a row stop racing each other's comments. Amp-Thread-ID: https://ampcode.com/threads/T-01a18f4b-0163-7216-a915-372c4b6288cb Co-authored-by: Amp --- .github/workflows/spellcheck.yml | 80 ++++--- AGENTS.md | 4 +- dev/check-spelling.mjs | 262 ++++++++++++++++++--- dev/post-spelling-review.mjs | 388 ------------------------------- dev/vercel-ignore-build.sh | 1 - 5 files changed, 283 insertions(+), 452 deletions(-) delete mode 100644 dev/post-spelling-review.mjs diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 328c3f2a8..744b69ba5 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -1,15 +1,21 @@ name: Spell check # Reports spelling errors on lines added by a pull request, as a summary comment -# plus inline review comments on the flagged lines. Existing spelling errors and -# errors on unchanged lines are not included. The pull request's title and -# description are checked too, and reported in the summary comment. +# plus an inline suggested change on each flagged line. Existing spelling errors +# and errors on unchanged lines are not included. The pull request's title and +# description are checked too, and reported in the summary comment. Advisory: +# never fails the PR. on: pull_request: # `edited` fires when the title or description changes types: [opened, synchronize, reopened, edited] +# A new push supersedes the run for the previous one +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: contents: read pull-requests: write @@ -18,6 +24,13 @@ jobs: spellcheck: name: CSpell (advisory) runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + # File links in the report open the file on the PR branch + LINK_BASE: ${{ github.event.pull_request.head.repo.html_url }}/blob/${{ github.event.pull_request.head.ref }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - name: Check out pull request head uses: actions/checkout@v7 @@ -25,6 +38,12 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 + # The workflow file is read from the merge commit, so run the scripts + # from there too: a branch that predates a check has no script for it, + # and the merge commit also has this PR's changes to them + - name: Check out the check scripts from the merge commit + run: git fetch --quiet origin "$GITHUB_SHA" && git checkout --quiet "$GITHUB_SHA" -- dev + # Node 24 is pre-cached on ubuntu-latest, avoiding the download # incurred by node-version: latest. - name: Set up Node.js @@ -41,39 +60,40 @@ jobs: # Title and description come in through env, never inline in the # script, so a crafted pull request cannot inject shell commands env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} + # The script exits 1 both with findings and when it crashes. It writes + # its output in one go at the end, so a crash leaves it empty. A crash + # does not fail the check, since it says nothing about the PR; the + # comment step reports it instead of posting the empty report. run: | - base=$(git merge-base "$BASE_SHA" HEAD) + git diff -U0 "$(git merge-base "$BASE_SHA" HEAD)" HEAD > "$RUNNER_TEMP/changes.diff" printf '%s\n\n%s\n' "$PR_TITLE" "$PR_BODY" > "$RUNNER_TEMP/pull-request.md" - # The script exits 1 both with findings and when it crashes at import - # time. It writes its output in one go at the end, so a crash leaves - # it empty. A crash does not fail the check, since it says nothing - # about the PR; the report step says so instead of posting the - # empty report - node dev/check-spelling.mjs --base "$base" \ - --pull-request "$RUNNER_TEMP/pull-request.md" --format json \ - > "$RUNNER_TEMP/spelling.json" || true - if ! [ -s "$RUNNER_TEMP/spelling.json" ]; then + if node dev/check-spelling.mjs --format markdown \ + --diff "$RUNNER_TEMP/changes.diff" \ + --pull-request "$RUNNER_TEMP/pull-request.md" \ + --review "$RUNNER_TEMP/review.json" \ + --link-base "$LINK_BASE" > "$RUNNER_TEMP/report.md"; then + echo "result=clean" >> "$GITHUB_OUTPUT" + elif [ -s "$RUNNER_TEMP/report.md" ]; then + echo "result=found" >> "$GITHUB_OUTPUT" + else echo "::warning::check-spelling crashed, so this PR was not checked" - echo "crashed=true" >> "$GITHUB_OUTPUT" + echo "result=crashed" >> "$GITHUB_OUTPUT" fi - cat "$RUNNER_TEMP/spelling.json" + cat "$RUNNER_TEMP/report.md" - - name: Report on the pull request - # Fork PRs get a read-only token; the findings are still in the job log + - name: Comment on the pull request + # Fork PRs get a read-only token; the report is still in the job log if: github.event.pull_request.head.repo.full_name == github.repository env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - HEAD_REF: ${{ github.event.pull_request.head.ref }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - CRASHED: ${{ steps.check.outputs.crashed }} - run: | - if [ "$CRASHED" = true ]; then - node dev/post-spelling-review.mjs --crashed - else - node dev/post-spelling-review.mjs --findings "$RUNNER_TEMP/spelling.json" - fi + RESULT: ${{ steps.check.outputs.result }} + run: dev/post-check-comment.sh '' "$RESULT" "$RUNNER_TEMP/report.md" + + - name: Suggest fixes as review comments + # One suggested change per flagged line, kept in sync with the + # findings; see dev/sync-review-comments.sh + if: >- + github.event.pull_request.head.repo.full_name == github.repository + && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) + run: dev/sync-review-comments.sh '`, ...explanation].join( + '\n' + ) + }; +} + +// Body for POST /repos/{owner}/{repo}/pulls/{n}/reviews: one comment per +// finding in a file; title and description findings have no line to comment +// on. No review body: a submitted review cannot be deleted, so a body would +// outlive the comments once the words are fixed. +function reviewRequest(findings) { + const comments = findings + .filter(finding => !finding.field) + .slice(0, MAX_INLINE_COMMENTS) + .map(reviewComment); + return {event: 'COMMENT', body: '', comments}; +} + +const FORMATTERS = { + text: formatText, + json: findings => JSON.stringify(findings, null, '\t') + '\n', + markdown: formatMarkdown +}; + +function main() { + if (!DIFF_FILE) { + throw new Error('Missing required --diff '); + } + const format = FORMATTERS[FORMAT]; + if (!format) { + throw new Error( + `Unknown --format "${FORMAT}"; use text, json, or markdown` + ); + } + + const ranges = addedLineRanges(readFileSync(DIFF_FILE, 'utf8')); const files = [...ranges.keys()]; const findings = [ ...findingsOnAddedLines(ranges, [ @@ -204,15 +398,19 @@ async function main() { ]), ...(PULL_REQUEST_FILE ? pullRequestFindings(PULL_REQUEST_FILE) : []) ]; - process.stdout.write( - FORMAT === 'json' - ? JSON.stringify(findings, null, '\t') + '\n' - : formatText(findings) - ); + if (REVIEW_FILE) { + writeFileSync( + REVIEW_FILE, + JSON.stringify(reviewRequest(findings), null, '\t') + '\n' + ); + } + process.stdout.write(format(findings)); process.exit(findings.length === 0 ? 0 : 1); } -main().catch(error => { +try { + main(); +} catch (error) { console.error(error); process.exit(2); -}); +} diff --git a/dev/post-spelling-review.mjs b/dev/post-spelling-review.mjs deleted file mode 100644 index 109f1e711..000000000 --- a/dev/post-spelling-review.mjs +++ /dev/null @@ -1,388 +0,0 @@ -#!/usr/bin/env node - -/** - * Reports CSpell findings on a pull request: one summary comment in the - * discussion, plus an inline review comment on each flagged line. Findings in - * the pull request's title or description (those with a `field`) have no line - * to comment on, so they appear in the summary only. Once the findings are - * fixed, the summary is minimized as resolved and the inline comments are - * deleted; the review that carried them has no body, so nothing of it remains - * visible. - * - * Usage: node dev/post-spelling-review.mjs --findings [--dry-run] - * node dev/post-spelling-review.mjs --crashed [--dry-run] - * - * Reads the JSON written by `dev/check-spelling.mjs --format json`. With - * `--crashed`, the summary says the check could not run and links the job log - * (RUN_URL) instead; the inline comments are left as they are. - * Requires GH_TOKEN, GITHUB_REPOSITORY, PR_NUMBER, HEAD_SHA and HEAD_REF. - */ - -import {readFileSync} from 'fs'; - -const args = process.argv.slice(2); -const FINDINGS_FILE = args[args.indexOf('--findings') + 1]; -const CRASHED = args.includes('--crashed'); -const DRY_RUN = args.includes('--dry-run'); -const MAX_INLINE_COMMENTS = 25; - -const API_URL = process.env.GITHUB_API_URL ?? 'https://api.github.com'; -const REPOSITORY = process.env.GITHUB_REPOSITORY; -const PR_NUMBER = process.env.PR_NUMBER; -const HEAD_SHA = process.env.HEAD_SHA; -const HEAD_REF = process.env.HEAD_REF; -const RUN_URL = process.env.RUN_URL; - -// Link to the PR branch, not the commit, so GitHub's edit button works from it -const ALLOW_LIST_LINK = `[\`cspell-allow-list.txt\`](https://github.com/${REPOSITORY}/blob/${HEAD_REF}/cspell-allow-list.txt)`; - -const SUMMARY_MARKER = ''; -const INLINE_MARKER = '/)?.[1]; - return word && findingKey({file: comment.path, line: comment.line, word}); -} - -// CSpell suggests case-insensitively, so prefer a suggestion whose first -// letter matches the case of the flagged word. -function bestSuggestion({word, suggestions}) { - const isUpper = letter => letter === letter.toUpperCase(); - return ( - suggestions.find( - suggestion => isUpper(suggestion[0]) === isUpper(word[0]) - ) ?? suggestions[0] - ); -} - -// One GitHub suggestion block with an apply button. A four-backtick fence so -// lines containing ``` cannot break out of the block. -function suggestionBlock(finding) { - const suggestion = bestSuggestion(finding); - if (!suggestion) { - return []; - } - const {text, column, word} = finding; - const start = column - 1; - return [ - `Did you mean \`${suggestion}\`?`, - '', - '````suggestion', - text.slice(0, start) + suggestion + text.slice(start + word.length), - '````', - '' - ]; -} - -function inlineBody(finding) { - const explanation = finding.message - ? [messageText(finding)] - : [ - `\`${finding.word}\` is not in the dictionary.`, - '', - ...suggestionBlock(finding), - `Please correct the spelling, or add words which are correct to ${ALLOW_LIST_LINK}.`, - '', - `Comment on this PR with \`/spell ${finding.word}\`, and a bot will push a commit to your branch adding it to ${ALLOW_LIST_LINK} for you.` - ]; - return [`${INLINE_MARKER} ${finding.word} -->`, ...explanation].join('\n'); -} - -async function syncInlineComments(findings) { - const wanted = new Map( - findings - .filter(finding => !finding.field) - .map(finding => [findingKey(finding), finding]) - ); - const comments = await githubList( - `/repos/${REPOSITORY}/pulls/${PR_NUMBER}/comments` - ); - - for (const comment of comments) { - const key = existingCommentKey(comment); - if (!key) { - continue; - } - if (wanted.has(key)) { - const body = inlineBody(wanted.get(key)); - if (body !== comment.body) { - await githubWrite( - 'PATCH', - `/repos/${REPOSITORY}/pulls/comments/${comment.id}`, - {body} - ); - } - wanted.delete(key); - } else { - await githubWrite( - 'DELETE', - `/repos/${REPOSITORY}/pulls/comments/${comment.id}` - ); - } - } - - const fresh = [...wanted.values()]; - if (fresh.length === 0) { - return; - } - // No review body: the inline comments say it all, and a submitted review - // cannot be deleted, so a body would outlive the comments once fixed. - await githubWrite( - 'POST', - `/repos/${REPOSITORY}/pulls/${PR_NUMBER}/reviews`, - { - commit_id: HEAD_SHA, - event: 'COMMENT', - body: '', - comments: fresh.slice(0, MAX_INLINE_COMMENTS).map(finding => ({ - path: finding.file, - line: finding.line, - side: 'RIGHT', - body: inlineBody(finding) - })) - } - ); -} - -async function main() { - for (const name of [ - 'GH_TOKEN', - 'GITHUB_REPOSITORY', - 'PR_NUMBER', - 'HEAD_SHA', - 'HEAD_REF' - ]) { - if (!process.env[name]) { - throw new Error(`Missing required environment variable ${name}`); - } - } - if (CRASHED) { - if (!RUN_URL) { - throw new Error('Missing required environment variable RUN_URL'); - } - console.log('Reporting that the check crashed'); - await upsertSummaryComment(crashedBody(), false); - return; - } - if (!FINDINGS_FILE) { - throw new Error('Missing required --findings '); - } - - const findings = JSON.parse(readFileSync(FINDINGS_FILE, 'utf8')); - console.log(`${findings.length} finding(s) to report`); - const resolved = findings.length === 0; - await upsertSummaryComment( - resolved ? RESOLVED_BODY : summaryBody(findings), - resolved - ); - await syncInlineComments(findings); -} - -main().catch(error => { - console.error(error); - process.exit(2); -}); diff --git a/dev/vercel-ignore-build.sh b/dev/vercel-ignore-build.sh index 03f6f6689..4dc8b33a5 100755 --- a/dev/vercel-ignore-build.sh +++ b/dev/vercel-ignore-build.sh @@ -18,7 +18,6 @@ exec git diff --quiet HEAD^ HEAD -- . \ ':(exclude)dev/check-spelling.mjs' \ ':(exclude)dev/example-hostnames.json' \ ':(exclude)dev/post-check-comment.sh' \ - ':(exclude)dev/post-spelling-review.mjs' \ ':(exclude)dev/sync-review-comments.sh' \ ':(exclude)dev/vercel-build-log-slack-app-manifest.json' \ ':(exclude)dev/vercel-build-log.mjs' \ From d1809f3fe7900226e0154d1dccba0eae5804ee73 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:32:45 -0600 Subject: [PATCH 3/6] cspell: Allow shellcheck Amp-Thread-ID: https://ampcode.com/threads/T-01a18f4b-0163-7216-a915-372c4b6288cb Co-authored-by: Amp --- cspell-allow-list.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell-allow-list.txt b/cspell-allow-list.txt index 2177b4e74..de0595cd8 100644 --- a/cspell-allow-list.txt +++ b/cspell-allow-list.txt @@ -487,6 +487,7 @@ sgquery sgtest sgviolet Shadcn +shellcheck shellsession shiki showcerts From f3abe48440204ebd54b866abaa0f8772cbf4bf13 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:04:35 -0600 Subject: [PATCH 4/6] ci/spellcheck: Rename the workflow to check-spelling.yml like the other checks Amp-Thread-ID: https://ampcode.com/threads/T-01a18f4b-0163-7216-a915-372c4b6288cb Co-authored-by: Amp --- .github/workflows/{spellcheck.yml => check-spelling.yml} | 2 +- AGENTS.md | 2 +- dev/check-spelling.mjs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename .github/workflows/{spellcheck.yml => check-spelling.yml} (98%) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/check-spelling.yml similarity index 98% rename from .github/workflows/spellcheck.yml rename to .github/workflows/check-spelling.yml index 744b69ba5..3a0f1f363 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/check-spelling.yml @@ -22,7 +22,7 @@ permissions: jobs: spellcheck: - name: CSpell (advisory) + name: Spelling errors introduced by this PR (advisory) runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} diff --git a/AGENTS.md b/AGENTS.md index 4b71219ce..cb51a4116 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ - **Prove changed links resolve on a deploy**: `node dev/verify-links-live.mjs --site ` prints a Markdown table for the PR description -- **Spell check**: `.github/workflows/spellcheck.yml` runs +- **Spell check**: `.github/workflows/check-spelling.yml` runs `dev/check-spelling.mjs` on the lines a PR adds plus its title and description, and comments the findings (advisory, never fails the PR). Add product names and identifiers to `cspell-allow-list.txt` with diff --git a/dev/check-spelling.mjs b/dev/check-spelling.mjs index 71075bca8..302adbb06 100644 --- a/dev/check-spelling.mjs +++ b/dev/check-spelling.mjs @@ -4,7 +4,7 @@ * Reports CSpell findings on lines added by a Git diff, and dictionary entries * added out of alphabetical order. Optionally also checks a pull request's * title and description. The pull request workflow - * (.github/workflows/spellcheck.yml) runs this on the lines a PR adds and + * (.github/workflows/check-spelling.yml) runs this on the lines a PR adds and * comments the findings; it never fails the PR. * * Usage: node dev/check-spelling.mjs --diff [options] From 9770113430bdde3c09abc222a37d513bbdb15a40 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:24:24 -0600 Subject: [PATCH 5/6] ci: Run the PR checks through one composite action and shared scripts The five check workflows repeated the same checkout, diff, baseline, run, and comment steps. .github/actions/pr-check now holds them once, and each workflow sets only what differs. dev/check-shared.mjs holds the flag, diff, file listing, baseline, and report code the five check scripts each carried a copy of, and dev/report-check.sh replaces post-check-comment.sh and sync-review-comments.sh. github-slugger is installed by the action instead of by each workflow. Amp-Thread-ID: https://ampcode.com/threads/T-01a18f4b-0163-7216-a915-372c4b6288cb Co-authored-by: Amp --- .github/actions/pr-check/action.yml | 137 ++++++++++++++++++++++ .github/workflows/check-duplicates.yml | 79 ++----------- .github/workflows/check-hostnames.yml | 62 ++-------- .github/workflows/check-links.yml | 89 ++------------- .github/workflows/check-redirects.yml | 89 ++------------- .github/workflows/check-spelling.yml | 64 ++--------- AGENTS.md | 21 ++-- dev/check-duplicates.mjs | 150 ++++++------------------- dev/check-hostnames.mjs | 116 +++---------------- dev/check-links.mjs | 105 ++--------------- dev/check-redirects.mjs | 97 ++++------------ dev/check-shared.mjs | 135 ++++++++++++++++++++++ dev/check-spelling.mjs | 111 ++++-------------- dev/post-check-comment.sh | 42 ------- dev/report-check.sh | 88 +++++++++++++++ dev/sync-review-comments.sh | 40 ------- dev/vercel-ignore-build.sh | 4 +- 17 files changed, 524 insertions(+), 905 deletions(-) create mode 100644 .github/actions/pr-check/action.yml create mode 100644 dev/check-shared.mjs delete mode 100755 dev/post-check-comment.sh create mode 100755 dev/report-check.sh delete mode 100755 dev/sync-review-comments.sh diff --git a/.github/actions/pr-check/action.yml b/.github/actions/pr-check/action.yml new file mode 100644 index 000000000..836192192 --- /dev/null +++ b/.github/actions/pr-check/action.yml @@ -0,0 +1,137 @@ +name: PR check +description: >- + Runs one dev/check-.mjs on the lines a pull request adds and reports + its findings on the PR: a summary comment plus a review comment on each + flagged line (dev/report-check.sh). The job must check out the repository + first, with fetch-depth 0; the default ref, the merge commit, is right. + +inputs: + script: + description: The check script, e.g. dev/check-links.mjs + required: true + name: + description: >- + Marks the comments the check posts, e.g. check-links; see + dev/report-check.sh + required: true + arguments: + description: Flags for every run of the script + default: '' + head-only-arguments: + description: >- + Flags for the run on the pull request only, not for recording the + baseline on the merge base + default: '' + diff-paths: + description: Paths the diff of the pull request is limited to; all by default + default: '' + baseline: + description: >- + Record the script's findings on the merge base too (--format json + --root) and pass them as --baseline, so only the findings this PR + introduces are reported + default: 'false' + dependency: + description: >- + An npm package the script imports, installed at the version in + package.json without the rest of the site's dependencies + default: '' + fail-on-findings: + description: Fail the job when the script reports findings; advisory otherwise + default: 'false' + +outputs: + result: + description: clean, found, or crashed + value: ${{ steps.check.outputs.result }} + +runs: + using: composite + steps: + # The workflow and this action are read from the merge commit, so run + # the scripts from there too: a branch that predates a check has no + # script for it, and the merge commit also has this PR's changes to + # them. Everything else is checked as the PR branch has it. + - name: Check out the pull request head, with the check scripts from the merge commit + shell: bash + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + git checkout --quiet "$HEAD_SHA" + git checkout --quiet "$GITHUB_SHA" -- dev + + - name: Install ${{ inputs.dependency }} + if: inputs.dependency != '' + shell: bash + env: + DEPENDENCY: ${{ inputs.dependency }} + # Into a scratch prefix, not the repo: `npm install ` next to + # package.json would install every dependency of the site + run: | + npm install --prefix "$RUNNER_TEMP/deps" --no-package-lock --no-audit --no-fund \ + "$DEPENDENCY@$(node -p "require('./package.json').dependencies['$DEPENDENCY']")" + ln -s "$RUNNER_TEMP/deps/node_modules" node_modules + + # The script exits 1 both with findings and when it crashes. It writes + # its output in one go at the end, so a crash leaves it empty. A crash + # does not fail the check, since it says nothing about the PR; the + # report step says so instead of posting the empty report. + - name: Run ${{ inputs.script }} on the lines this PR adds + id: check + shell: bash + env: + SCRIPT: ${{ inputs.script }} + ARGUMENTS: ${{ inputs.arguments }} + HEAD_ONLY_ARGUMENTS: ${{ inputs.head-only-arguments }} + DIFF_PATHS: ${{ inputs.diff-paths }} + BASELINE: ${{ inputs.baseline }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + # File links in the report open the file on the PR branch + LINK_BASE: ${{ github.event.pull_request.head.repo.html_url }}/blob/${{ github.event.pull_request.head.ref }} + run: | + # The arguments are lists, so they are left unquoted on purpose + # shellcheck disable=SC2086 + merge_base=$(git merge-base "$BASE_SHA" HEAD) + git diff -U0 "$merge_base" HEAD -- $DIFF_PATHS > "$RUNNER_TEMP/changes.diff" + + baseline_flag= + if [ "$BASELINE" = true ]; then + git worktree add --quiet "$RUNNER_TEMP/base" "$merge_base" + node "$SCRIPT" $ARGUMENTS --format json --root "$RUNNER_TEMP/base" > "$RUNNER_TEMP/baseline.json" || true + if ! [ -s "$RUNNER_TEMP/baseline.json" ]; then + echo "::warning::$SCRIPT crashed on the base branch, so this PR was not checked" + echo "result=crashed" >> "$GITHUB_OUTPUT" + exit 0 + fi + baseline_flag="--baseline $RUNNER_TEMP/baseline.json" + fi + + if node "$SCRIPT" $ARGUMENTS $HEAD_ONLY_ARGUMENTS --format markdown $baseline_flag \ + --diff "$RUNNER_TEMP/changes.diff" \ + --review "$RUNNER_TEMP/review.json" \ + --link-base "$LINK_BASE" > "$RUNNER_TEMP/report.md"; then + echo "result=clean" >> "$GITHUB_OUTPUT" + elif [ -s "$RUNNER_TEMP/report.md" ]; then + echo "result=found" >> "$GITHUB_OUTPUT" + else + echo "::warning::$SCRIPT crashed, so this PR was not checked" + echo "result=crashed" >> "$GITHUB_OUTPUT" + fi + cat "$RUNNER_TEMP/report.md" + + - name: Report on the pull request + # Fork PRs get a read-only token; the report is still in the job log + if: github.event.pull_request.head.repo.full_name == github.repository + shell: bash + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + NAME: ${{ inputs.name }} + RESULT: ${{ steps.check.outputs.result }} + run: dev/report-check.sh "$NAME" "$RESULT" "$RUNNER_TEMP/report.md" "$RUNNER_TEMP/review.json" + + - name: Fail when this PR introduces findings + if: inputs.fail-on-findings == 'true' && steps.check.outputs.result == 'found' + shell: bash + run: exit 1 diff --git a/.github/workflows/check-duplicates.yml b/.github/workflows/check-duplicates.yml index 6006b6514..8fcf36eec 100644 --- a/.github/workflows/check-duplicates.yml +++ b/.github/workflows/check-duplicates.yml @@ -10,8 +10,9 @@ on: paths: - 'docs/**' - 'dev/check-duplicates.mjs' - - 'dev/post-check-comment.sh' - - 'dev/sync-review-comments.sh' + - 'dev/check-shared.mjs' + - 'dev/report-check.sh' + - '.github/actions/pr-check/action.yml' - '.github/workflows/check-duplicates.yml' # A new push supersedes the run for the previous one @@ -27,74 +28,14 @@ jobs: check-duplicates: name: Duplicated content introduced by this PR (advisory) runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - # File links in the report open the file on the PR branch - LINK_BASE: ${{ github.event.pull_request.head.repo.html_url }}/blob/${{ github.event.pull_request.head.ref }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - - name: Check out pull request head - uses: actions/checkout@v7 + - uses: actions/checkout@v7 with: - ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - # The workflow file is read from the merge commit, so run the scripts - # from there too: a branch that predates a check has no script for it, - # and the merge commit also has this PR's changes to them - - name: Check out the check scripts from the merge commit - run: git fetch --quiet origin "$GITHUB_SHA" && git checkout --quiet "$GITHUB_SHA" -- dev - - - name: Check out merge base - run: | - merge_base=$(git merge-base "$BASE_SHA" HEAD) - git worktree add "$RUNNER_TEMP/base" "$merge_base" - git diff -U0 "$merge_base" HEAD -- docs > "$RUNNER_TEMP/changes.diff" - - # The script exits 1 both with findings and when it crashes. It writes - # its output in one go at the end, so a crash leaves it empty. A crash - # does not fail the check, since it says nothing about the PR; the - # comment step reports it instead of posting the empty report. - - name: Record duplicated passages already present on the base branch - run: | - node dev/check-duplicates.mjs --format json \ - --root "$RUNNER_TEMP/base" > "$RUNNER_TEMP/base-duplicates.json" || true - - - name: Find duplicated passages introduced by this PR - id: check - run: | - if ! [ -s "$RUNNER_TEMP/base-duplicates.json" ]; then - echo "::warning::check-duplicates crashed on the base branch, so this PR was not checked" - echo "result=crashed" >> "$GITHUB_OUTPUT" - exit 0 - fi - if node dev/check-duplicates.mjs --format markdown \ - --baseline "$RUNNER_TEMP/base-duplicates.json" \ - --diff "$RUNNER_TEMP/changes.diff" \ - --review "$RUNNER_TEMP/review.json" \ - --link-base "$LINK_BASE" > "$RUNNER_TEMP/report.md"; then - echo "result=clean" >> "$GITHUB_OUTPUT" - elif [ -s "$RUNNER_TEMP/report.md" ]; then - echo "result=found" >> "$GITHUB_OUTPUT" - else - echo "::warning::check-duplicates crashed, so this PR was not checked" - echo "result=crashed" >> "$GITHUB_OUTPUT" - fi - cat "$RUNNER_TEMP/report.md" - - - name: Comment on the pull request - # Fork PRs get a read-only token; the report is still in the job log - if: github.event.pull_request.head.repo.full_name == github.repository - env: - RESULT: ${{ steps.check.outputs.result }} - run: dev/post-check-comment.sh '' "$RESULT" "$RUNNER_TEMP/report.md" - - - name: Point out the duplicates as review comments - # One comment per added passage, kept in sync with the findings; - # see dev/sync-review-comments.sh - if: >- - github.event.pull_request.head.repo.full_name == github.repository - && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) - run: dev/sync-review-comments.sh '' "$RESULT" "$RUNNER_TEMP/report.md" - - - name: Suggest fixes as review comments - # One suggested change per flagged line, kept in sync with the - # findings; see dev/sync-review-comments.sh - if: >- - github.event.pull_request.head.repo.full_name == github.repository - && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) - run: dev/sync-review-comments.sh '' "$RESULT" "$RUNNER_TEMP/report.md" - - - name: Suggest fixes as review comments - # One suggested change per finding with a fix, kept in sync with the - # findings; see dev/sync-review-comments.sh - if: >- - github.event.pull_request.head.repo.full_name == github.repository - && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) - run: dev/sync-review-comments.sh '' "$RESULT" "$RUNNER_TEMP/report.md" - - - name: Suggest fixes as review comments - # One suggested change per fixable entry this PR added, kept in sync - # with the findings; see dev/sync-review-comments.sh - if: >- - github.event.pull_request.head.repo.full_name == github.repository - && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) - run: dev/sync-review-comments.sh '' "$RESULT" "$RUNNER_TEMP/report.md" + run: printf '%s\n\n%s\n' "$PR_TITLE" "$PR_BODY" > "$RUNNER_TEMP/pull-request.md" - - name: Suggest fixes as review comments - # One suggested change per flagged line, kept in sync with the - # findings; see dev/sync-review-comments.sh - if: >- - github.event.pull_request.head.repo.full_name == github.repository - && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) - run: dev/sync-review-comments.sh '' report.md -# The report is the Markdown the check script wrote, used as the comment body -# for clean and found. Needs GH_TOKEN, GITHUB_REPOSITORY, GITHUB_WORKFLOW, -# PR_NUMBER, and RUN_URL. -set -eu -marker=$1 -result=$2 -report=$3 -issues="repos/$GITHUB_REPOSITORY/issues" - -existing=$(gh api "$issues/$PR_NUMBER/comments" --paginate \ - --jq ".[] | select(.body | startswith(\"$marker\")) | \"\(.id) \(.node_id)\"" | head -n 1) - -if [ "$result" = crashed ]; then - body=$(printf '%s\n### ⚠️ %s could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ - "$marker" "$GITHUB_WORKFLOW" "$RUN_URL") -else - body=$(printf '%s\n' "$marker"; cat "$report") -fi - -if [ -z "$existing" ]; then - [ "$result" != clean ] || exit 0 - gh api --method POST "$issues/$PR_NUMBER/comments" --raw-field body="$body" --silent - exit 0 -fi - -gh api --method PATCH "$issues/comments/${existing% *}" --raw-field body="$body" --silent - -# Minimizing is GraphQL-only. Both mutations are idempotent -if [ "$result" = clean ]; then - mutation="minimizeComment(input: {subjectId: \$id, classifier: RESOLVED})" -else - mutation="unminimizeComment(input: {subjectId: \$id})" -fi -gh api graphql --raw-field id="${existing#* }" \ - --raw-field query="mutation(\$id: ID!) { $mutation { clientMutationId } }" --silent diff --git a/dev/report-check.sh b/dev/report-check.sh new file mode 100755 index 000000000..c57aa17c1 --- /dev/null +++ b/dev/report-check.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env sh +# Report a check's findings on a PR: a summary comment, plus one review +# comment per finding in the review JSON. Both are found again on the next +# run by the marker on their first line, so each run leaves the PR showing +# the findings of the current revision and nothing else. +# +# Usage: dev/report-check.sh report.md review.json +# name the marker prefix: `check-links` marks the summary +# `` and each review comment +# `` +# report the Markdown the check script wrote, the summary body for clean +# and found; crashed gets a message linking the job log instead +# review {comments: [{path, line, start_line?, body}]} from the script's +# --review, not read when crashed +# +# The summary is posted or reopened for findings and a crash; a clean result +# only updates and minimizes the summary of an earlier run, so a PR that +# never had findings never gets a comment. Review comments are posted for the +# new findings, updated when their text changed, and deleted when their +# finding is gone or GitHub could not carry them to the new revision (line is +# null). Needs GH_TOKEN, GITHUB_REPOSITORY, GITHUB_WORKFLOW, PR_NUMBER, and +# RUN_URL. +set -eu +name=$1 +result=$2 +report=$3 +review=$4 +repository="repos/$GITHUB_REPOSITORY" + +# Summary comment + +marker="" +existing=$(gh api "$repository/issues/$PR_NUMBER/comments" --paginate \ + --jq ".[] | select(.body | startswith(\"$marker\")) | \"\(.id) \(.node_id)\"" | head -n 1) + +if [ "$result" = crashed ]; then + body=$(printf '%s\n### ⚠️ %s could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ + "$marker" "$GITHUB_WORKFLOW" "$RUN_URL") +else + body=$(printf '%s\n' "$marker"; cat "$report") +fi + +if [ -z "$existing" ]; then + [ "$result" = clean ] || gh api --method POST "$repository/issues/$PR_NUMBER/comments" --raw-field body="$body" --silent +else + gh api --method PATCH "$repository/issues/comments/${existing% *}" --raw-field body="$body" --silent + # Minimizing is GraphQL-only. Both mutations are idempotent + if [ "$result" = clean ]; then + mutation="minimizeComment(input: {subjectId: \$id, classifier: RESOLVED})" + else + mutation="unminimizeComment(input: {subjectId: \$id})" + fi + gh api graphql --raw-field id="${existing#* }" \ + --raw-field query="mutation(\$id: ID!) { $mutation { clientMutationId } }" --silent +fi + +[ "$result" != crashed ] || exit 0 + +# Review comments, matched to the findings by file, line, and marker line + +marker="". -# -# Usage: dev/sync-review-comments.sh '` and each review comment # `` diff --git a/dev/check-spelling.mjs b/dev/check-spelling.mjs index 8b6b27f9b..7ddf0f68f 100644 --- a/dev/check-spelling.mjs +++ b/dev/check-spelling.mjs @@ -266,7 +266,7 @@ function formatMarkdown(findings) { // First line of a review comment, so the workflow can match the comments it // posted earlier to the findings still present and delete the rest -const REVIEW_MARKER = '