Skip to content
Open
12 changes: 6 additions & 6 deletions .github/workflows/check-redirects-on-rename.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,17 @@ jobs:
comment += '---\n';
comment += '_Note: `middleware.ts` is recommended for simple exact-match redirects. Use `redirects.js` for redirects with path parameters (e.g., `:path*`)._\n';

// Check for existing comments from this action
const {data: comments} = await github.rest.issues.listComments({
// Check for existing comments from this action (paginate to handle PRs with 30+ comments)
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const existingComment = comments.find(comment =>
comment.user.type === 'Bot' &&
(comment.user.login === 'github-actions[bot]' || comment.user.login.includes('bot')) &&
comment.body.includes('Missing Redirects Detected')
const existingComment = comments.find(c =>
c.user.type === 'Bot' &&
c.user.login === 'github-actions[bot]' &&
c.body.includes('Missing Redirects Detected')
);

if (existingComment) {
Expand Down
166 changes: 166 additions & 0 deletions .github/workflows/lint-redirect-chains.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: Lint Redirect Chains

on:
pull_request:
branches: [master]
paths:
- 'middleware.ts'
- 'redirects.js'
- 'docs/**/*.mdx'
- 'docs/**/*.md'
- 'develop-docs/**/*.mdx'
- 'develop-docs/**/*.md'
- 'includes/**/*.mdx'
- 'includes/**/*.md'
- 'platform-includes/**/*.mdx'
- 'platform-includes/**/*.md'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow skips linter file changes

Medium Severity

The pull-request paths filter lists middleware.ts, redirects.js, and content globs only. Changes confined to scripts/lint-redirect-chains.ts, its spec, or this workflow file do not match, so the new job will not run on PRs that touch only the linter or CI wiring—including the PR that introduces it.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 523dff1. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow intentionally only triggers on redirect/content file changes, because that's what it lints. Changes to the linter script itself are validated by the existing test suite (pnpm test / vitest). This matches the pattern of check-redirects-on-rename.yml. Adding the linter's own files to the path filter would just run the linter needlessly on PRs that only touch the linter code.


concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
lint-redirect-chains:
name: Check for redirect chains
runs-on: ubuntu-latest
continue-on-error: true # Fail the check but don't block merge
permissions:
contents: read
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
with:
node-version-file: 'package.json'

- name: Run redirect chain lint
id: lint
continue-on-error: true
run: |
set +e
OUTPUT=$(npx tsx scripts/lint-redirect-chains.ts 2>&1)
EXIT_CODE=$?
set -e

echo "$OUTPUT"

# Extract JSON output if present
HAS_JSON=false
if echo "$OUTPUT" | grep -Fq -- "---JSON_OUTPUT---"; then
JSON_OUTPUT=$(echo "$OUTPUT" | sed -n '/---JSON_OUTPUT---/,/---JSON_OUTPUT---/p' | sed '1d;$d')
echo "lint_result<<EOF" >> $GITHUB_OUTPUT
echo "$JSON_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
HAS_JSON=true
fi

echo "has_results=$HAS_JSON" >> $GITHUB_OUTPUT
echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT

- name: Post comment if redirect chains found
if: steps.lint.outputs.exit_code == '1' && steps.lint.outputs.has_results == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
script: |
const lintResultJsonString = ${{ toJSON(steps.lint.outputs.lint_result) }};
let lintResult;
try {
const jsonString = JSON.parse(lintResultJsonString);
lintResult = JSON.parse(jsonString);
} catch (e) {
console.error('Failed to parse lint result:', e);
core.setFailed('Failed to parse redirect chain lint output');
return;
}

const redirectChains = lintResult.redirectChains || [];
const contentLinkIssues = lintResult.contentLinkIssues || [];

if (redirectChains.length === 0 && contentLinkIssues.length === 0) {
return;
}

let comment = '## \u26a0\ufe0f Redirect Chain Issues Detected\n\n';
comment += 'This PR introduces or contains redirect chains that degrade SEO and add latency.\n\n';

if (redirectChains.length > 0) {
comment += '### Redirect-to-Redirect Chains\n\n';
comment += 'These redirect destinations point to another redirect, creating multi-hop chains. ';
comment += 'Update the destination to point directly to the final URL:\n\n';
comment += '| Source | Current Destination | Should Be | File |\n';
comment += '|--------|-------------------|-----------|------|\n';
for (const chain of redirectChains) {
const docLabel = chain.isDeveloperDocs ? ' (dev)' : '';
comment += `| \`${chain.source}\` | \`${chain.currentDest}\` | \`${chain.finalDest}\` | ${chain.file}${docLabel} |\n`;
}
comment += '\n';
}

if (contentLinkIssues.length > 0) {
// Group by file
const byFile = {};
for (const issue of contentLinkIssues) {
if (!byFile[issue.filePath]) byFile[issue.filePath] = [];
Comment thread
sentry[bot] marked this conversation as resolved.
byFile[issue.filePath].push(issue);
}

comment += `### Content Links Pointing to Redirects (${contentLinkIssues.length} found)\n\n`;
comment += 'These links point to URLs that redirect. Update them to point directly to the final destination:\n\n';
comment += '| File | Line | Current Link | Should Be |\n';
comment += '|------|------|--------------|-----------|\n';
for (const [file, issues] of Object.entries(byFile)) {
for (const issue of issues) {
comment += `| \`${file}\` | ${issue.line} | \`${issue.linkPath}\` | \`${issue.finalDest}\` |\n`;
}
}
comment += '\n';
}

comment += '---\n';
comment += '*Each redirect hop loses ~15% of SEO link equity and adds latency for users.*\n';

// Check for existing comments from this action (paginate to handle PRs with 30+ comments)
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const existingComment = comments.find(c =>
c.user.type === 'Bot' &&
c.user.login === 'github-actions[bot]' &&
c.body.includes('Redirect Chain Issues Detected')
);

if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: comment,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment,
});
}

- name: Report failure if redirect chains found
if: steps.lint.outputs.exit_code == '1' && steps.lint.outputs.has_results == 'true'
run: |
echo "::warning::Redirect chain issues detected. See PR comment for details."
exit 1

- name: Success
if: steps.lint.outputs.exit_code == '0'
run: |
echo "No redirect chains or content link issues found."
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"lint:prettier": "prettier --check \"./{src,app,scripts}/**/*.{md,mdx,ts,tsx,js,jsx,mjs}\"",
"lint:prettier:fix": "prettier --write \"./{src,app,scripts}/**/*.{md,mdx,ts,tsx,js,jsx,mjs}\"",
"lint:typos": "typos",
"lint:redirect-chains": "tsx scripts/lint-redirect-chains.ts",
"lint:fix": "pnpm run lint:prettier:fix && pnpm run lint:eslint:fix",
"sidecar": "spotlight-sidecar",
"test": "vitest",
Expand Down
Loading
Loading