mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
Add CI check for broken mattermost.com links in webapp (#35093)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* Add CI check for broken mattermost.com links in webapp Add a script that scans the webapp source files for links to mattermost.com domains and tests each unique URL for 404s. This helps detect broken documentation and marketing links early. - New script: webapp/scripts/check-external-links.mjs - New npm target: check-external-links - New CI job in webapp-ci.yml to run on every commit * Add --markdown flag for GitHub Actions job summary * Fix job summary: use pipefail and suppress progress output * Require mattermost.com links to use /pl/ permalink format * Require all mattermost.com links (including subdomains) to use /pl/ * Allow exceptions for push servers and root domain * Make non-permalink URLs warnings instead of errors * Add User-Agent header and retry GET on 403 * Follow redirects when checking URLs Check the final destination of redirects to catch broken links that redirect to error pages. If a redirect response has the Cloudflare cf-mitigated header, assume the URL is OK and stop following. * Simplify link checker code - Combine PUSH_SERVER_PATTERN and HPNS_PATTERN into single regex - Simplify validatePermalink to return boolean (reason was unused) - Consolidate Cloudflare header checks in processResponse * replace broken links with valid ones * updates
This commit is contained in:
parent
baf2bcb6f5
commit
5aefff30cf
7 changed files with 428 additions and 6 deletions
17
.github/workflows/webapp-ci.yml
vendored
17
.github/workflows/webapp-ci.yml
vendored
|
|
@ -45,6 +45,23 @@ jobs:
|
|||
run: |
|
||||
npm run i18n-extract:check
|
||||
|
||||
check-external-links:
|
||||
needs: check-lint
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: webapp
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/setup
|
||||
uses: ./.github/actions/webapp-setup
|
||||
- name: ci/check-external-links
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm run check-external-links -- --markdown | tee -a $GITHUB_STEP_SUMMARY
|
||||
|
||||
check-types:
|
||||
needs: check-lint
|
||||
runs-on: ubuntu-24.04
|
||||
|
|
|
|||
|
|
@ -3261,7 +3261,7 @@ const AdminDefinition: AdminDefinitionType = {
|
|||
featureName: 'burn_on_read',
|
||||
title: defineMessage({id: 'admin.burn_on_read_feature_discovery.title', defaultMessage: 'Send burn-on-read messages that are automatically deleted after being read'}),
|
||||
description: defineMessage({id: 'admin.burn_on_read_feature_discovery.description', defaultMessage: 'With Mattermost Enterprise Advanced, users can send transient messages that are automatically deleted a fixed time after they are read by a recipient.'}),
|
||||
learnMoreURL: 'https://docs.mattermost.com/deployment/burn-on-read-messages.html',
|
||||
learnMoreURL: 'https://docs.mattermost.com/end-user-guide/collaborate/send-messages.html#send-burn-on-read-messages',
|
||||
svgImage: BurnOnReadSVG,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const AttributeBasedAccessControlFeatureDiscovery: React.FC = () => {
|
|||
id: 'admin.attribute_based_access_control_feature_discovery.desc',
|
||||
defaultMessage: 'Create policies containing access rules based on user attributes and apply them to channels and other resources within Mattermost.',
|
||||
})}
|
||||
learnMoreURL='https://docs.mattermost.com/deployment/'
|
||||
learnMoreURL='https://docs.mattermost.com/administration-guide/manage/admin/attribute-based-access-control.html'
|
||||
featureDiscoveryImage={
|
||||
<SystemRolesSVG
|
||||
width={294}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const HelpCommands = (): JSX.Element => {
|
|||
values={{
|
||||
link: (chunks: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href='https://docs.mattermost.com/integrations/slash-commands-built-in.html'
|
||||
href='https://docs.mattermost.com/integrations-guide/built-in-slash-commands.html'
|
||||
location='help_commands'
|
||||
>
|
||||
{chunks}
|
||||
|
|
|
|||
|
|
@ -1026,7 +1026,7 @@ export const AboutLinks = {
|
|||
};
|
||||
|
||||
export const CloudLinks = {
|
||||
BILLING_DOCS: 'https://docs.mattermost.com/pl/cloud-billing',
|
||||
BILLING_DOCS: 'https://docs.mattermost.com/product-overview/cloud-subscriptions.html',
|
||||
PRICING: 'https://mattermost.com/pl/pricing/',
|
||||
PRORATED_PAYMENT: 'https://mattermost.com/pl/mattermost-cloud-prorate-documentation',
|
||||
DEPLOYMENT_OPTIONS: 'https://mattermost.com/deploy/',
|
||||
|
|
@ -1074,7 +1074,7 @@ export const DocLinks = {
|
|||
SETUP_LDAP: 'https://mattermost.com/pl/setup-ldap',
|
||||
SETUP_PERFORMANCE_MONITORING: 'https://mattermost.com/pl/setup-performance-monitoring',
|
||||
SETUP_PUSH_NOTIFICATIONS: 'https://mattermost.com/pl/setup-push-notifications',
|
||||
SETUP_SAML: 'https://docs.mattermost.com/pl/setup-saml',
|
||||
SETUP_SAML: 'https://docs.mattermost.com/administration-guide/onboard/sso-saml.html',
|
||||
SHARE_LINKS_TO_MESSAGES: 'https://mattermost.com/pl/share-links-to-messages',
|
||||
SITE_URL: 'https://mattermost.com/pl/configure-site-url',
|
||||
SSL_CERTIFICATE: 'https://mattermost.com/pl/setup-ssl-client-certificate',
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
"i18n-extract": "npm run i18n-extract --workspaces --if-present",
|
||||
"i18n-extract:check": "npm run i18n-extract:check --workspaces --if-present",
|
||||
"clean": "npm run clean --workspaces --if-present && rm -rf node_modules .parcel-cache",
|
||||
"gen-lang-imports": "node scripts/gen_lang_imports.mjs"
|
||||
"gen-lang-imports": "node scripts/gen_lang_imports.mjs",
|
||||
"check-external-links": "node scripts/check-external-links.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mattermost/compass-icons": "0.1.53",
|
||||
|
|
|
|||
404
webapp/scripts/check-external-links.mjs
Normal file
404
webapp/scripts/check-external-links.mjs
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
const MATTERMOST_URL_PATTERN = /https?:\/\/(?:[a-z0-9-]+\.)*mattermost\.com(?:[/?#][^"'\s<>()]*)?/gi;
|
||||
|
||||
const PERMALINK_PATTERN = /^https?:\/\/(www\.)?mattermost\.com\/pl\//;
|
||||
|
||||
const PUSH_SERVER_PATTERN = /^https?:\/\/(([a-z0-9-]+\.)?push|hpns-[a-z]+)\.mattermost\.com(\/|$)/;
|
||||
const ROOT_DOMAIN_PATTERN = /^https?:\/\/(www\.)?mattermost\.com\/?$/;
|
||||
|
||||
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
||||
|
||||
const DIRECTORIES_TO_SCAN = [
|
||||
'channels/src',
|
||||
'platform/client/src',
|
||||
'platform/components/src',
|
||||
'platform/mattermost-redux/src',
|
||||
];
|
||||
|
||||
function getAllSourceFiles(dir, excludeTests = true) {
|
||||
const files = [];
|
||||
|
||||
function walk(currentDir) {
|
||||
if (!fs.existsSync(currentDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(currentDir, {withFileTypes: true});
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'coverage') {
|
||||
continue;
|
||||
}
|
||||
walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name);
|
||||
if (!SOURCE_EXTENSIONS.includes(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (excludeTests && (entry.name.includes('.test.') || entry.name.includes('.spec.'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
function extractUrls(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const matches = content.match(MATTERMOST_URL_PATTERN) || [];
|
||||
|
||||
return matches.map((url) => {
|
||||
let cleanUrl = url;
|
||||
cleanUrl = cleanUrl.replace(/[',;)}\]]+$/, '');
|
||||
cleanUrl = cleanUrl.replace(/\\n$/, '');
|
||||
return cleanUrl;
|
||||
});
|
||||
}
|
||||
|
||||
function findAllMattermostUrls(rootDir, excludeTests = true) {
|
||||
const urlMap = new Map();
|
||||
|
||||
for (const dir of DIRECTORIES_TO_SCAN) {
|
||||
const fullDir = path.join(rootDir, dir);
|
||||
const files = getAllSourceFiles(fullDir, excludeTests);
|
||||
|
||||
for (const file of files) {
|
||||
const urls = extractUrls(file);
|
||||
for (const url of urls) {
|
||||
if (!urlMap.has(url)) {
|
||||
urlMap.set(url, []);
|
||||
}
|
||||
urlMap.get(url).push(path.relative(rootDir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urlMap;
|
||||
}
|
||||
|
||||
function isValidPermalink(url) {
|
||||
return PERMALINK_PATTERN.test(url) ||
|
||||
ROOT_DOMAIN_PATTERN.test(url) ||
|
||||
PUSH_SERVER_PATTERN.test(url);
|
||||
}
|
||||
|
||||
function findNonPermalinkUrls(urlMap) {
|
||||
const invalid = [];
|
||||
for (const [url, files] of urlMap) {
|
||||
if (!isValidPermalink(url)) {
|
||||
invalid.push({url, files});
|
||||
}
|
||||
}
|
||||
return invalid;
|
||||
}
|
||||
|
||||
const FETCH_HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MattermostLinkChecker/1.0; +https://github.com/mattermost/mattermost)',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
};
|
||||
|
||||
const MAX_REDIRECTS = 10;
|
||||
|
||||
async function checkUrl(url, retries = 2) {
|
||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||
try {
|
||||
return await checkUrlWithRedirects(url);
|
||||
} catch (error) {
|
||||
if (attempt === retries) {
|
||||
return {
|
||||
url,
|
||||
status: 0,
|
||||
ok: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUrlWithRedirects(originalUrl) {
|
||||
let currentUrl = originalUrl;
|
||||
|
||||
for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++) {
|
||||
const response = await fetch(currentUrl, {
|
||||
method: 'HEAD',
|
||||
redirect: 'manual',
|
||||
headers: FETCH_HEADERS,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (response.status === 405 || response.status === 403) {
|
||||
const getResponse = await fetch(currentUrl, {
|
||||
method: 'GET',
|
||||
redirect: 'manual',
|
||||
headers: FETCH_HEADERS,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
return processResponse(originalUrl, getResponse);
|
||||
}
|
||||
|
||||
const result = processResponse(originalUrl, response);
|
||||
|
||||
if (!result.redirect) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const location = response.headers.get('location');
|
||||
if (!location) {
|
||||
return {
|
||||
url: originalUrl,
|
||||
status: response.status,
|
||||
ok: false,
|
||||
error: 'Redirect without Location header',
|
||||
};
|
||||
}
|
||||
|
||||
currentUrl = new URL(location, currentUrl).href;
|
||||
}
|
||||
|
||||
return {
|
||||
url: originalUrl,
|
||||
status: 0,
|
||||
ok: false,
|
||||
error: 'Too many redirects',
|
||||
};
|
||||
}
|
||||
|
||||
function processResponse(originalUrl, response) {
|
||||
const {status} = response;
|
||||
const cfHeader = response.headers.get('cf-mitigated');
|
||||
const isRedirect = status >= 300 && status < 400;
|
||||
|
||||
if (cfHeader && (isRedirect || status === 403 || status === 503)) {
|
||||
return {url: originalUrl, status, ok: true};
|
||||
}
|
||||
|
||||
if (status === 301) {
|
||||
return {url: originalUrl, status, ok: true};
|
||||
}
|
||||
|
||||
if (isRedirect) {
|
||||
return {url: originalUrl, status, redirect: true};
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return {url: originalUrl, status, ok: true};
|
||||
}
|
||||
|
||||
return {url: originalUrl, status, ok: false};
|
||||
}
|
||||
|
||||
function shouldSkipUrlCheck(url) {
|
||||
return PUSH_SERVER_PATTERN.test(url);
|
||||
}
|
||||
|
||||
async function checkUrls(urls, concurrency = 5, silentProgress = false) {
|
||||
const results = [];
|
||||
const urlList = Array.from(urls.keys()).filter((url) => !shouldSkipUrlCheck(url));
|
||||
|
||||
for (let i = 0; i < urlList.length; i += concurrency) {
|
||||
const batch = urlList.slice(i, i + concurrency);
|
||||
const batchResults = await Promise.all(batch.map((url) => checkUrl(url)));
|
||||
results.push(...batchResults);
|
||||
|
||||
if (!silentProgress) {
|
||||
const completed = Math.min(i + concurrency, urlList.length);
|
||||
process.stderr.write(`\rChecking URLs: ${completed}/${urlList.length}`);
|
||||
}
|
||||
}
|
||||
if (!silentProgress) {
|
||||
process.stderr.write('\n');
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function printResults(results, urlMap, nonPermalinkUrls) {
|
||||
const broken = results.filter((r) => !r.ok);
|
||||
const working = results.filter((r) => r.ok);
|
||||
|
||||
console.log('\n' + chalk.bold('=== External Link Check Results ===\n'));
|
||||
|
||||
console.log(chalk.green(`✓ ${working.length} URLs are accessible`));
|
||||
if (broken.length > 0) {
|
||||
console.log(chalk.red(`✗ ${broken.length} URLs are broken`));
|
||||
}
|
||||
if (nonPermalinkUrls.length > 0) {
|
||||
console.log(chalk.yellow(`⚠ ${nonPermalinkUrls.length} URLs are not using permalink format (warning)`));
|
||||
}
|
||||
console.log();
|
||||
|
||||
if (broken.length > 0) {
|
||||
console.log(chalk.red.bold('Broken URLs:\n'));
|
||||
for (const result of broken) {
|
||||
const statusText = result.error ? `Error: ${result.error}` : `HTTP ${result.status}`;
|
||||
console.log(chalk.red(` ${result.url}`));
|
||||
console.log(chalk.gray(` Status: ${statusText}`));
|
||||
console.log(chalk.gray(` Found in:`));
|
||||
for (const file of urlMap.get(result.url)) {
|
||||
console.log(chalk.gray(` - ${file}`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
if (nonPermalinkUrls.length > 0) {
|
||||
console.log(chalk.yellow.bold('URLs not using permalink format (warning):\n'));
|
||||
console.log(chalk.gray(' All mattermost.com links should route via https://mattermost.com/pl/\n'));
|
||||
for (const item of nonPermalinkUrls) {
|
||||
console.log(chalk.yellow(` ${item.url}`));
|
||||
console.log(chalk.gray(` Found in:`));
|
||||
for (const file of item.files) {
|
||||
console.log(chalk.gray(` - ${file}`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
if (broken.length === 0 && nonPermalinkUrls.length === 0) {
|
||||
console.log(chalk.green.bold(`✓ All URLs are valid and accessible\n`));
|
||||
}
|
||||
|
||||
return broken.length > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
function generateMarkdownSummary(results, urlMap, nonPermalinkUrls) {
|
||||
const broken = results.filter((r) => !r.ok);
|
||||
const working = results.filter((r) => r.ok);
|
||||
|
||||
const lines = [];
|
||||
|
||||
lines.push('## External Link Check Results\n');
|
||||
|
||||
if (broken.length === 0 && nonPermalinkUrls.length === 0) {
|
||||
lines.push(`✅ **All ${working.length} mattermost.com URLs are valid and accessible**\n`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
lines.push(`| Status | Count |`);
|
||||
lines.push(`|--------|-------|`);
|
||||
lines.push(`| ✅ Working | ${working.length} |`);
|
||||
if (broken.length > 0) {
|
||||
lines.push(`| ❌ Broken | ${broken.length} |`);
|
||||
}
|
||||
if (nonPermalinkUrls.length > 0) {
|
||||
lines.push(`| ⚠️ Missing /pl/ prefix (warning) | ${nonPermalinkUrls.length} |`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
if (broken.length > 0) {
|
||||
lines.push('### Broken URLs\n');
|
||||
lines.push('| URL | Status | Files |');
|
||||
lines.push('|-----|--------|-------|');
|
||||
|
||||
for (const result of broken) {
|
||||
const statusText = result.error ? `Error: ${result.error}` : `HTTP ${result.status}`;
|
||||
const files = urlMap.get(result.url).map((f) => `\`${f}\``).join(', ');
|
||||
lines.push(`| ${result.url} | ${statusText} | ${files} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (nonPermalinkUrls.length > 0) {
|
||||
lines.push('### ⚠️ URLs Missing Permalink Format (warning)\n');
|
||||
lines.push('> All mattermost.com links should route via `https://mattermost.com/pl/`\n');
|
||||
lines.push('<details>\n<summary>Show URLs</summary>\n');
|
||||
lines.push('| URL | Files |');
|
||||
lines.push('|-----|-------|');
|
||||
|
||||
for (const item of nonPermalinkUrls) {
|
||||
const files = item.files.map((f) => `\`${f}\``).join(', ');
|
||||
lines.push(`| ${item.url} | ${files} |`);
|
||||
}
|
||||
lines.push('\n</details>');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const includeTests = args.includes('--include-tests');
|
||||
const jsonOutput = args.includes('--json');
|
||||
const markdownOutput = args.includes('--markdown');
|
||||
|
||||
const rootDir = process.cwd();
|
||||
|
||||
if (!markdownOutput) {
|
||||
console.log(chalk.inverse.bold(' Checking mattermost.com links in webapp... ') + '\n');
|
||||
|
||||
if (includeTests) {
|
||||
console.log(chalk.yellow('Including test files in scan\n'));
|
||||
}
|
||||
}
|
||||
|
||||
const urlMap = findAllMattermostUrls(rootDir, !includeTests);
|
||||
|
||||
if (!markdownOutput) {
|
||||
console.log(`Found ${chalk.bold(urlMap.size)} unique mattermost.com URLs\n`);
|
||||
}
|
||||
|
||||
if (urlMap.size === 0) {
|
||||
if (markdownOutput) {
|
||||
console.log('## External Link Check Results\n\n⚠️ No URLs found to check');
|
||||
} else {
|
||||
console.log(chalk.yellow('No URLs found to check'));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const nonPermalinkUrls = findNonPermalinkUrls(urlMap);
|
||||
const results = await checkUrls(urlMap, 5, markdownOutput);
|
||||
const brokenCount = results.filter((r) => !r.ok).length;
|
||||
|
||||
if (markdownOutput) {
|
||||
console.log(generateMarkdownSummary(results, urlMap, nonPermalinkUrls));
|
||||
return brokenCount > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
if (jsonOutput) {
|
||||
const output = {
|
||||
total: results.length,
|
||||
working: results.filter((r) => r.ok).length,
|
||||
broken: results.filter((r) => !r.ok).map((r) => ({
|
||||
url: r.url,
|
||||
status: r.status,
|
||||
error: r.error,
|
||||
files: urlMap.get(r.url),
|
||||
})),
|
||||
nonPermalink: nonPermalinkUrls,
|
||||
};
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
return brokenCount > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
return printResults(results, urlMap, nonPermalinkUrls);
|
||||
}
|
||||
|
||||
main().then((exitCode) => {
|
||||
process.exitCode = exitCode;
|
||||
}).catch((error) => {
|
||||
console.error(chalk.red('Error:'), error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Loading…
Reference in a new issue