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
24 changes: 17 additions & 7 deletions scripts/content-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("|");
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/lib/parse-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() };
Expand Down
Loading