Skip to content

AIG

AIG #751

Workflow file for this run

name: Check Links
on:
pull_request:
jobs:
check-links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.14.0
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Check for broken links
id: check-links
run: |
set +e
pnpm test-links > links-output.txt 2>&1
set -e
if grep -q "broken links" links-output.txt; then
echo "⚠️ Found broken links:"
echo "has_broken_links=true" >> $GITHUB_OUTPUT
cat links-output.txt
else
echo "✅ No broken links found"
echo "has_broken_links=false" >> $GITHUB_OUTPUT
fi
- name: Check for redirect conflicts
id: check-redirects
run: |
set +e
pnpm check-redirect-conflicts > redirects-output.txt 2>&1
REDIRECTS_EXIT=$?
set -e
if [ $REDIRECTS_EXIT -ne 0 ]; then
echo "⚠️ Found redirects that conflict with each other:"
echo "has_conflicts=true" >> $GITHUB_OUTPUT
cat redirects-output.txt
else
echo "✅ No redirect conflicts found"
echo "has_conflicts=false" >> $GITHUB_OUTPUT
fi
- name: Filter exceptions and determine result
if: steps.check-links.outputs.has_broken_links == 'true' || steps.check-redirects.outputs.has_conflicts == 'true'
run: |
node << 'EOF'
const fs = require('fs');
// Load exceptions
const exceptions = JSON.parse(fs.readFileSync('custom/link-exceptions.json', 'utf8'));
// Read outputs
const linksOutput = fs.existsSync('links-output.txt')
? fs.readFileSync('links-output.txt', 'utf8')
: '';
const redirectsOutput = fs.existsSync('redirects-output.txt')
? fs.readFileSync('redirects-output.txt', 'utf8')
: '';
// Extract broken links from output
const brokenLinks = [];
const redirectConflicts = [];
// Parse links output - mint broken-links outputs like " ⎿ /bad-path/thing"
linksOutput.split('\n').forEach(line => {
const trimmed = line.trim();
// Look for patterns like "⎿ /path" or direct "/path" or "https://url"
if (trimmed && (trimmed.startsWith('/') || trimmed.startsWith('http') || trimmed.includes('⎿'))) {
// Extract the actual path after the ⎿ symbol if present
const match = trimmed.match(/⎿\s+(.+)/) || [null, trimmed];
const link = match[1];
if (link && (link.startsWith('/') || link.startsWith('http'))) {
brokenLinks.push(link);
}
}
});
// Parse redirects output - format is "source -> destination"
redirectsOutput.split('\n').forEach(line => {
const trimmed = line.trim();
// Skip pnpm metadata lines (starting with > or empty)
if (trimmed && !trimmed.startsWith('>') && trimmed.includes('->')) {
redirectConflicts.push(trimmed);
}
});
// Check if a link matches an exception pattern
function matchesException(link, pattern) {
// No wildcards - exact match with trailing slash normalization
if (!pattern.includes('*')) {
// Normalize both by removing trailing slashes
const normalizedLink = link.replace(/\/$/, '');
const normalizedPattern = pattern.replace(/\/$/, '');
return normalizedLink === normalizedPattern;
}
// Convert pattern to regex
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except *
.replace(/\*/g, '.*'); // Replace * with .*
const regex = new RegExp('^' + regexPattern + '$');
return regex.test(link);
}
// Filter exceptions only for links (not redirects)
const actualLinkErrors = brokenLinks.filter(link => {
return !exceptions.some(exception => matchesException(link, exception));
});
const hasErrors = actualLinkErrors.length > 0 || redirectConflicts.length > 0;
if (hasErrors) {
console.log('❌ Found issues:');
if (actualLinkErrors.length > 0) {
console.log('\nBroken links not in exceptions:');
actualLinkErrors.forEach(link => console.log(` - ${link}`));
}
if (redirectConflicts.length > 0) {
console.log('\nRedirect conflicts:');
redirectConflicts.forEach(conflict => console.log(` - ${conflict}`));
}
process.exit(1);
} else if (brokenLinks.length > 0) {
console.log('✅ All broken links are in exceptions list');
process.exit(0);
} else {
console.log('✅ No broken links or redirect conflicts found');
process.exit(0);
}
EOF