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
50 changes: 33 additions & 17 deletions .github/workflows/check-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,37 @@ jobs:
git worktree add "$RUNNER_TEMP/base" "$merge_base"
git diff -U0 "$merge_base" HEAD > "$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 broken links already present on the base branch
# Exit 1 means findings, which is expected here
run: |
node dev/check-links.mjs --check-anchors --check-self-links --format json \
--root "$RUNNER_TEMP/base" > "$RUNNER_TEMP/base-links.json" \
|| [ $? -eq 1 ]
--root "$RUNNER_TEMP/base" > "$RUNNER_TEMP/base-links.json" || true

- name: Find broken links 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-links.json" ]; then
echo "::warning::check-links crashed on the base branch, so this PR was not checked"
echo "result=crashed" >> "$GITHUB_OUTPUT"
exit 0
fi
if node dev/check-links.mjs --check-anchors --check-self-links --check-external --format markdown \
--baseline "$RUNNER_TEMP/base-links.json" \
--diff "$RUNNER_TEMP/changes.diff" \
--review "$RUNNER_TEMP/review.json" \
--link-base "$LINK_BASE" > "$RUNNER_TEMP/report.md"; then
echo "broken=false" >> "$GITHUB_OUTPUT"
echo "result=clean" >> "$GITHUB_OUTPUT"
elif [ -s "$RUNNER_TEMP/report.md" ]; then
echo "result=broken" >> "$GITHUB_OUTPUT"
else
echo "broken=true" >> "$GITHUB_OUTPUT"
echo "::warning::check-links crashed, so this PR was not checked"
echo "result=crashed" >> "$GITHUB_OUTPUT"
fi
cat "$RUNNER_TEMP/report.md"

Expand All @@ -73,21 +83,25 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BROKEN: ${{ steps.check.outputs.broken }}
RESULT: ${{ steps.check.outputs.result }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
marker='<!-- check-links-report -->'
existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
--paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)

# Comment only when there is something to report, or an earlier report to resolve
if [ "$BROKEN" = true ]; then
{ echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md"
elif [ -n "$existing_comment" ]; then
printf '%s\n### ✅ The broken links an earlier revision of this PR introduced are fixed\n' \
"$marker" > "$RUNNER_TEMP/comment.md"
else
exit 0
fi
# 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" \
Expand All @@ -99,12 +113,14 @@ jobs:
- 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
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 }}
run: dev/sync-review-comments.sh '<!-- check-links-finding:' "$RUNNER_TEMP/review.json"

- name: Fail when this PR introduces broken links
if: steps.check.outputs.broken == 'true'
if: steps.check.outputs.result == 'broken'
run: exit 1
51 changes: 34 additions & 17 deletions .github/workflows/check-redirects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,38 @@ jobs:
git worktree add "$RUNNER_TEMP/base" "$merge_base"
git diff -U0 "$merge_base" HEAD -- src/data/redirects.ts > "$RUNNER_TEMP/changes.diff"

# The script exits 1 both with findings and when it crashes, e.g. because
# this PR's copy of it cannot read the base branch's redirects.ts. 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 broken redirects already present on the base branch
# Exit 1 means findings, which is expected here
run: |
node dev/check-redirects.mjs --format json \
--root "$RUNNER_TEMP/base" > "$RUNNER_TEMP/base-redirects.json" \
|| [ $? -eq 1 ]
--root "$RUNNER_TEMP/base" > "$RUNNER_TEMP/base-redirects.json" || true

- name: Find redirects broken by this PR
id: check
env:
# Line links in the report open redirects.ts 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-redirects.json" ]; then
echo "::warning::check-redirects crashed on the base branch, so this PR was not checked"
echo "result=crashed" >> "$GITHUB_OUTPUT"
exit 0
fi
if node dev/check-redirects.mjs --format markdown \
--baseline "$RUNNER_TEMP/base-redirects.json" \
--diff "$RUNNER_TEMP/changes.diff" \
--review "$RUNNER_TEMP/review.json" \
--link-base "$LINK_BASE" > "$RUNNER_TEMP/report.md"; then
echo "broken=false" >> "$GITHUB_OUTPUT"
echo "result=clean" >> "$GITHUB_OUTPUT"
elif [ -s "$RUNNER_TEMP/report.md" ]; then
echo "result=broken" >> "$GITHUB_OUTPUT"
else
echo "broken=true" >> "$GITHUB_OUTPUT"
echo "::warning::check-redirects crashed, so this PR was not checked"
echo "result=crashed" >> "$GITHUB_OUTPUT"
fi
cat "$RUNNER_TEMP/report.md"

Expand All @@ -74,21 +85,25 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BROKEN: ${{ steps.check.outputs.broken }}
RESULT: ${{ steps.check.outputs.result }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
marker='<!-- check-redirects-report -->'
existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
--paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)

# Comment only when there is something to report, or an earlier report to resolve
if [ "$BROKEN" = true ]; then
{ echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md"
elif [ -n "$existing_comment" ]; then
printf '%s\n### ✅ The redirects an earlier revision of this PR broke are fixed\n' \
"$marker" > "$RUNNER_TEMP/comment.md"
else
exit 0
fi
# 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" \
Expand All @@ -100,12 +115,14 @@ jobs:
- 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
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 }}
run: dev/sync-review-comments.sh '<!-- check-redirects-finding:' "$RUNNER_TEMP/review.json"

- name: Fail when this PR breaks redirects
if: steps.check.outputs.broken == 'true'
if: steps.check.outputs.result == 'broken'
run: exit 1
22 changes: 19 additions & 3 deletions .github/workflows/spellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
run: npm install --global cspell@10

- name: Find spelling errors introduced by this PR
id: check
# Title and description come in through env, never inline in the
# script, so a crafted pull request cannot inject shell commands
env:
Expand All @@ -46,10 +47,18 @@ jobs:
run: |
base=$(git merge-base "$BASE_SHA" HEAD)
printf '%s\n\n%s\n' "$PR_TITLE" "$PR_BODY" > "$RUNNER_TEMP/pull-request.md"
# Exit 1 means findings; anything else is an operational error
# 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" || [ "$?" -eq 1 ]
> "$RUNNER_TEMP/spelling.json" || true
if ! [ -s "$RUNNER_TEMP/spelling.json" ]; then
echo "::warning::check-spelling crashed, so this PR was not checked"
echo "crashed=true" >> "$GITHUB_OUTPUT"
fi
cat "$RUNNER_TEMP/spelling.json"

- name: Report on the pull request
Expand All @@ -60,4 +69,11 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: node dev/post-spelling-review.mjs --findings "$RUNNER_TEMP/spelling.json"
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
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
- **Check redirects**: `node dev/check-redirects.mjs` reports broken entries in
`src/data/redirects.ts` (CI comments on PRs that break redirects; see the
script header for what it checks). Not part of `pnpm run check`: main has
hundreds of pre-existing findings, and CI only reports the ones a PR adds
hundreds of pre-existing findings, and CI only reports the ones a PR adds.
When a check script crashes, CI comments that the check could not run,
linking the job log, instead of failing the PR: an empty report says nothing
about the PR
- **Prove changed links resolve on a deploy**:
`node dev/verify-links-live.mjs --site <vercel-preview-url>` prints a
Markdown table for the PR description
Expand Down
1 change: 1 addition & 0 deletions cspell-allow-list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ entrycommand
envsubst
errgroup
errorf
esac
esbenp
Eswatini
etcdctl
Expand Down
3 changes: 3 additions & 0 deletions dev/check-redirects.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,12 @@ function loadRedirects() {
/TECHNICAL_CHANGELOG_RSS_URL\s*=\s*['"]([^'"]+)['"]/
)?.[1] ?? '';

// CI runs this revision of the script against the base branch too, so
// accept both export styles the file has used
const script = source
.replace(/^import .*$/gm, '')
.replace(/^export const /gm, 'const ')
.replace(/^export \{.*$/gm, '')
.replace(/module\.exports\s*=\s*\{[\s\S]*?\};?/g, '');

const sandbox = {TECHNICAL_CHANGELOG_RSS_URL: rssUrl};
Expand Down
52 changes: 39 additions & 13 deletions dev/post-spelling-review.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@
* visible.
*
* Usage: node dev/post-spelling-review.mjs --findings <json-file> [--dry-run]
* node dev/post-spelling-review.mjs --crashed [--dry-run]
*
* Reads the JSON written by `dev/check-spelling.mjs --format json`.
* 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;

Expand All @@ -27,6 +31,7 @@ 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)`;
Expand Down Expand Up @@ -168,7 +173,22 @@ function summaryBody(findings) {
return lines.join('\n') + '\n';
}

async function upsertSummaryComment(findings) {
function crashedBody() {
return [
SUMMARY_MARKER,
'### ⚠️ The spell check could not run on this revision',
'',
`This is a problem with the check, not with this PR; see the [job log](${RUN_URL}).`,
''
].join('\n');
}

const RESOLVED_BODY = `${SUMMARY_MARKER}\n### ✅ This revision introduces no spelling errors\n`;

// Post the summary, or replace the earlier one. A resolved summary is kept,
// collapsed, so the discussion still shows what was flagged and fixed; it is
// reopened when there is something to say again.
async function upsertSummaryComment(body, resolved) {
const comments = await githubList(
`/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments`
);
Expand All @@ -177,27 +197,21 @@ async function upsertSummaryComment(findings) {
);

if (!existing) {
if (findings.length > 0) {
// Nothing to resolve
if (!resolved) {
await githubWrite(
'POST',
`/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments`,
{body: summaryBody(findings)}
{body}
);
}
return;
}

// Keep the earlier report, collapsed as resolved, so the discussion still
// shows what was flagged and fixed. Reopen it when new findings appear.
const resolved = findings.length === 0;
await githubWrite(
'PATCH',
`/repos/${REPOSITORY}/issues/comments/${existing.id}`,
{
body: resolved
? `${SUMMARY_MARKER}\n### ✅ The spelling errors reported on an earlier revision are fixed\n`
: summaryBody(findings)
}
{body}
);
await setCommentMinimized(existing.node_id, resolved);
}
Expand Down Expand Up @@ -323,13 +337,25 @@ async function main() {
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 <json-file>');
}

const findings = JSON.parse(readFileSync(FINDINGS_FILE, 'utf8'));
console.log(`${findings.length} finding(s) to report`);
await upsertSummaryComment(findings);
const resolved = findings.length === 0;
await upsertSummaryComment(
resolved ? RESOLVED_BODY : summaryBody(findings),
resolved
);
await syncInlineComments(findings);
}

Expand Down
Loading