diff --git a/scripts/content-validators.ts b/scripts/content-validators.ts index 9a5e80d5..2405df12 100644 --- a/scripts/content-validators.ts +++ b/scripts/content-validators.ts @@ -51,25 +51,33 @@ function findCloseMatch(name: string, authors: Author[]): string | null { } /** - * Validate the authors array from frontmatter or an issue body. + * Validate the authors field from a GitHub issue body or a content markdown file. * - * Errors (blocks): name not in authors.json and no close match found, OR empty name before '|' - * Warnings (non-blocking): name not found but a close match exists (likely a typo) + * Two contexts behave differently: + * - 'issue' : Only checks format (empty name before '|'). Does NOT check authors.json + * because the publish script adds new authors automatically. + * - 'content' : Checks every name exists in authors.json. Used when validating PR files + * where authors.json must already be up to date. + * + * Errors (blocks): empty name before '|', OR (content only) name not in authors.json with no close match + * Warnings (non-blocking): (content only) name not found but a close match exists (likely a typo) * * @param authors Array of author name strings to validate - * @param rawLines Optional raw textarea lines for '|' format parsing (used by issue validator) + * @param rawLines Raw textarea lines in 'Name | https://social.url' format (issue context only) * @param errors Array to push error messages into * @param warnings Array to push warning messages into + * @param context 'issue' | 'content' — controls which checks run */ export function validateAuthors( authors: string[] | undefined, rawLines: string[] | undefined, errors: string[], warnings: string[], + context: "issue" | "content", ): void { if (!authors || authors.length === 0) return; - // Check for empty names before '|' (only relevant when rawLines are provided) + // Format check: catch empty name before '|' (e.g. " | https://twitter.com/foo") if (rawLines) { for (const line of rawLines) { const pipeIdx = line.indexOf("|"); @@ -81,13 +89,15 @@ export function validateAuthors( } } + // Issue context: publish script handles syncing new authors to authors.json — no further checks + if (context === "issue") return; + + // Content context: every name must already exist in authors.json const knownAuthors = loadAuthors(); for (const name of authors) { if (!name) continue; - // Exact match if (knownAuthors.some((a) => a.name === name)) continue; - // Close match → warning const close = findCloseMatch(name, knownAuthors); if (close) { warnings.push( diff --git a/scripts/validate-content.ts b/scripts/validate-content.ts index 6368bdf2..67213923 100644 --- a/scripts/validate-content.ts +++ b/scripts/validate-content.ts @@ -206,7 +206,7 @@ function validateFile(filePath: string, contentType: ContentDir): { errors: stri if (data.authors === undefined || !Array.isArray(data.authors) || (data.authors as string[]).length === 0) { errors.push("authors: required — must list at least one author"); } else { - validateAuthors(data.authors as string[], undefined, errors, warnings); + validateAuthors(data.authors as string[], undefined, errors, warnings, "content"); } return { errors, warnings }; diff --git a/scripts/validate-issue.ts b/scripts/validate-issue.ts index 3bdb8eae..b1a49db2 100644 --- a/scripts/validate-issue.ts +++ b/scripts/validate-issue.ts @@ -113,7 +113,7 @@ if (!metadata.authors || metadata.authors.length === 0) { const rawLines = metadata.authors.map((a) => a.social ? `${a.name} | ${a.social}` : a.name, ); - validateAuthors(authorNames, rawLines, errors, warnings); + validateAuthors(authorNames, rawLines, errors, warnings, "issue"); } // Warn if team-only fields were set by the submitter diff --git a/src/lib/parse-issue.ts b/src/lib/parse-issue.ts index 8be86e91..ae4b6747 100644 --- a/src/lib/parse-issue.ts +++ b/src/lib/parse-issue.ts @@ -185,7 +185,10 @@ export function parseAuthorEntries(raw: string): Array<{ name: string; social?: const pipeIdx = line.indexOf("|"); if (pipeIdx !== -1) { const name = line.slice(0, pipeIdx).trim(); - const social = line.slice(pipeIdx + 1).trim(); + const rawSocial = line.slice(pipeIdx + 1).trim(); + // Strip markdown link syntax [text](url) → url + const mdLink = rawSocial.match(/^\[.*?\]\((https?:\/\/[^)]+)\)$/); + const social = mdLink ? mdLink[1] : rawSocial; return { name, social: social || undefined }; } return { name: line.trim() };