'use strict'; const path = require('node:path'); const fs = require('node:fs'); const { TextDecoder } = require('node:util'); const POSIX_PATH = path.posix; const WORK_ORDER_PATTERN = /^docs\/plans\/[^/]+\/task-\d{2}-[^/]+\.md$/; const LOCK_BASENAMES = new Set([ 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'go.sum', 'Cargo.lock', ]); const SCRIPT_TEST_EXTENSIONS = new Set([ 'cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'ts', 'tsx', ]); const FRONTMATTER_FIELDS = new Set([ 'acceptance_criteria', 'created', 'depends_on', 'id', 'legacy_specs', 'migration', 'owners', 'plan', 'requirements', 'status', 'system', 'system_design', 'title', 'updated', 'last_updated', 'specification_version', 'wave', ]); const ARRAY_FIELDS = new Set([ 'acceptance_criteria', 'depends_on', 'legacy_specs', 'owners', 'requirements', 'system_design', ]); const WORK_ORDER_REQUIRED_FIELDS = [ 'depends_on', 'id', 'title', 'status', 'wave', 'plan', 'requirements', 'acceptance_criteria', 'system_design', ]; const MAX_DOCUMENT_BYTES = 256 * 1024; const MAX_DOCUMENTS = 100; const MAX_TOTAL_DOCUMENT_BYTES = 4 * 1024 * 1024; const MAX_RESPONSE_BYTES = 8 * 1024 * 1024; const MAX_CHANGED_FILES = 3000; const REQUEST_TIMEOUT_MS = 30_000; const NO_DOCS_LABEL = 'no-docs-allow'; const STATUS_CONTEXT = 'PR documentation coverage'; const GITHUB_API = 'https://api.github.com'; function normalizeRepoPath(value) { if (typeof value !== 'string' || value.length === 0) { throw new Error('repository path must be a non-empty string'); } if (value.includes('\0') || value.includes('\\') || value.startsWith('/')) { throw new Error(`unsafe repository path: ${value}`); } const normalized = POSIX_PATH.normalize(value); if ( normalized === '.' || normalized === '..' || normalized.startsWith('../') || normalized.includes('/../') ) { throw new Error(`repository path escapes the repository: ${value}`); } return normalized; } function pathExemption(pathname) { if (pathname === 'docs' || pathname.startsWith('docs/')) { return 'documentation tree'; } if (/\.(?:md|mdx|markdown)$/i.test(pathname)) { return 'Markdown file'; } if (/^\.codex\/agents\/[^/]+\.toml$/.test(pathname)) { return 'Codex agent definition'; } if (pathname === '.codex/config.toml') { return 'Codex configuration'; } if (pathname === '.claude/settings.json') { return 'Claude configuration'; } if (/^\.cursor\/rules\/[^/]+\.mdc$/.test(pathname)) { return 'Cursor rule'; } if (/_test\.go$/i.test(pathname)) { return 'Go test file'; } if (pathname.startsWith('apps/web/e2e/')) { return 'web end-to-end test'; } const basename = POSIX_PATH.basename(pathname); const extension = POSIX_PATH.extname(basename).slice(1).toLowerCase(); if ( /\.(?:test|spec)\.[^.]+$/i.test(basename) && SCRIPT_TEST_EXTENSIONS.has(extension) ) { return 'JavaScript or TypeScript test file'; } if (/^apps\/web\/src\/locales\/[^/]+\/[^/]+\.json$/.test(pathname)) { return 'web translation catalog'; } if (LOCK_BASENAMES.has(basename)) { return 'dependency lock file'; } return null; } function changedPaths(changedFiles) { const paths = []; const invalidPaths = []; const seen = new Set(); for (const change of changedFiles ?? []) { if (typeof change !== 'string' && typeof change?.filename !== 'string') { invalidPaths.push('[missing filename]'); continue; } const candidates = typeof change === 'string' ? [change] : [change.filename, change.previous_filename]; for (const candidate of candidates) { if (candidate == null) { continue; } try { const normalized = normalizeRepoPath(candidate); if (!seen.has(normalized)) { seen.add(normalized); paths.push(normalized); } } catch (error) { invalidPaths.push(String(candidate)); } } } return { paths, invalidPaths }; } function classifyChangedFiles(changedFiles) { const { paths, invalidPaths } = changedPaths(changedFiles); const exemptPaths = []; const triggeringPaths = [...invalidPaths]; const reasons = invalidPaths.map(pathname => ({ path: pathname, reason: pathname === '[missing filename]' ? 'invalid changed-file record' : 'invalid repository path', })); for (const pathname of paths) { const reason = pathExemption(pathname); if (reason == null) { triggeringPaths.push(pathname); reasons.push({ path: pathname, reason: 'not in the exemption list' }); } else { exemptPaths.push(pathname); } } return { changedPaths: paths, exemptPaths, invalidPaths, reasons, requiresCoverage: triggeringPaths.length > 0, triggeringPaths, }; } function hasContentChanges(change) { if (change?.status !== 'renamed') { return change?.status !== 'removed'; } if (Number.isFinite(change?.changes)) { return change.changes > 0; } return Number(change?.additions ?? 0) + Number(change?.deletions ?? 0) > 0; } function selectChangedWorkOrders(changedFiles) { const selected = []; const seen = new Set(); for (const change of changedFiles ?? []) { const pathname = change?.filename ?? change; if (typeof pathname !== 'string') { continue; } let normalized; try { normalized = normalizeRepoPath(pathname); } catch { continue; } if (WORK_ORDER_PATTERN.test(normalized) && hasContentChanges(change)) { if (!seen.has(normalized)) { seen.add(normalized); selected.push(normalized); } } } return selected; } function parseScalar(value, field) { const trimmed = value.trim(); if (trimmed.length === 0) { throw new Error(`frontmatter field ${field} must have a value`); } if (trimmed.startsWith('!') || trimmed.startsWith('&') || trimmed.startsWith('*')) { throw new Error(`frontmatter field ${field} uses unsupported YAML syntax`); } if (trimmed.startsWith('"')) { if (!trimmed.endsWith('"')) { throw new Error(`frontmatter field ${field} has an unterminated string`); } const parsed = JSON.parse(trimmed); if (typeof parsed !== 'string') { throw new Error(`frontmatter field ${field} must be a string`); } return parsed; } if (trimmed.startsWith("'")) { if (!trimmed.endsWith("'")) { throw new Error(`frontmatter field ${field} has an unterminated string`); } return trimmed.slice(1, -1).replaceAll("''", "'"); } if (/[[\]{}|>]/.test(trimmed)) { throw new Error(`frontmatter field ${field} uses unsupported YAML syntax`); } if (/^-?\d+$/.test(trimmed)) { return Number(trimmed); } return trimmed; } function parseInlineArray(value, field) { const trimmed = value.trim(); if (trimmed === '[]') { return []; } if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) { throw new Error(`frontmatter field ${field} must be a list`); } const inner = trimmed.slice(1, -1).trim(); if (inner.length === 0) { return []; } const values = []; let current = ''; let quote = null; let escaped = false; for (const character of inner) { if (quote !== null) { current += character; if (escaped) { escaped = false; } else if (character === '\\' && quote === '"') { escaped = true; } else if (character === quote) { quote = null; } } else if (character === '"' || character === "'") { quote = character; current += character; } else if (character === ',') { values.push(parseScalar(current, field)); current = ''; } else { current += character; } } if (quote !== null) { throw new Error(`frontmatter field ${field} has an unterminated string`); } values.push(parseScalar(current, field)); return values; } function parseFrontmatter(source) { if (typeof source !== 'string' || source.length === 0) { throw new Error('document is empty'); } const lines = source.split(/\r?\n/); if (lines[0] !== '---') { throw new Error('frontmatter is missing'); } const values = {}; let currentList = null; let closingIndex = -1; for (let index = 1; index < lines.length; index += 1) { const line = lines[index]; if (line === '---') { closingIndex = index; break; } if (line.trim().length === 0 || line.trim().startsWith('#')) { continue; } if (/^\s/.test(line)) { if (!currentList || !/^\s+-\s+/.test(line)) { throw new Error(`frontmatter line ${index + 1} is not a supported list item`); } const item = line.replace(/^\s+-\s+/, ''); values[currentList].push(parseScalar(item, currentList)); continue; } const match = /^([A-Za-z_][A-Za-z0-9_-]*):(?:[ \t]*(.*))?$/.exec(line); if (!match) { throw new Error(`frontmatter line ${index + 1} is invalid`); } const [, field, rawValue = ''] = match; if (!FRONTMATTER_FIELDS.has(field)) { throw new Error(`frontmatter field ${field} is unsupported`); } if (Object.hasOwn(values, field)) { throw new Error(`frontmatter field ${field} is duplicated`); } if (ARRAY_FIELDS.has(field)) { if (rawValue.trim().length === 0) { values[field] = []; currentList = field; } else { values[field] = parseInlineArray(rawValue, field); currentList = null; } } else { values[field] = parseScalar(rawValue, field); currentList = null; } } if (closingIndex < 0) { throw new Error('frontmatter closing delimiter is missing'); } return { body: lines.slice(closingIndex + 1).join('\n'), data: values, frontmatter: lines.slice(1, closingIndex).join('\n'), }; } function contentEntries(fileContents) { if (fileContents instanceof Map) { return [...fileContents.entries()]; } if (fileContents && typeof fileContents === 'object') { return Object.entries(fileContents); } return []; } function contentFor(fileContents, pathname, deletedPaths) { if (deletedPaths.has(pathname)) { return undefined; } if (fileContents instanceof Map) { return fileContents.has(pathname) ? fileContents.get(pathname) : undefined; } return Object.hasOwn(fileContents ?? {}, pathname) ? fileContents[pathname] : undefined; } function resolveReference(basePath, reference) { if (typeof reference !== 'string' || reference.trim().length === 0) { throw new Error(`reference from ${basePath} is empty`); } if (reference.includes('\\') || reference.startsWith('/')) { throw new Error(`reference from ${basePath} is not repository-relative: ${reference}`); } const resolved = normalizeRepoPath(POSIX_PATH.join(POSIX_PATH.dirname(basePath), reference)); return resolved; } function hasMarkdownLinkTo(planPath, body, targetPath) { const linkPattern = /\[[^\]]+\]\(\s*(?:<([^>]+)>|([^\s)]+))/g; for (const match of body.matchAll(linkPattern)) { const destination = (match[1] ?? match[2]).split(/[?#]/, 1)[0]; if (destination.length === 0) { continue; } try { if (resolveReference(planPath, destination) === targetPath) { return true; } } catch { // Invalid links are not links to the selected work order. } } return false; } function asNonEmptyString(value, field, pathname) { if (typeof value !== 'string' || value.trim().length === 0) { throw new Error(`${pathname} frontmatter field ${field} must be a non-empty string`); } return value; } function asNonEmptyStringList(value, field, pathname) { if (!Array.isArray(value) || value.length === 0) { throw new Error(`${pathname} frontmatter field ${field} must be a non-empty list`); } if (value.some(item => typeof item !== 'string' || item.trim().length === 0)) { throw new Error(`${pathname} frontmatter field ${field} contains an invalid item`); } return value; } function acceptanceCriteriaByRequirement(criteria, requirements, pathname, errors) { const referencedRequirements = new Set(requirements); const criteriaByRequirement = new Map(); for (const criterion of criteria) { const match = /^AC-(.+)\.\d+$/.exec(criterion); const requirementId = match ? `REQ-${match[1]}` : null; if (!requirementId || !referencedRequirements.has(requirementId)) { errors.push(`${criterion} does not belong to a referenced requirement in ${pathname}`); continue; } const ownedCriteria = criteriaByRequirement.get(requirementId) ?? []; ownedCriteria.push(criterion); criteriaByRequirement.set(requirementId, ownedCriteria); } return criteriaByRequirement; } function requirementHeadingPattern(requirementId) { return new RegExp(`^#{1,6}\\s+${escapeRegExp(requirementId)}(?:\\s|:|$)`, 'm'); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function requirementDefinitions(fileContents, requirementId, system) { const prefix = `docs/specs/${system}/requirements/`; return contentEntries(fileContents) .filter(([pathname, content]) => { return ( pathname.startsWith(prefix) && pathname.endsWith('.md') && typeof content === 'string' && requirementHeadingPattern(requirementId).test(content) ); }) .map(([pathname, content]) => ({ pathname, content })); } function acceptanceCriteriaForRequirement(content, requirementId) { const lines = content.split(/\r?\n/); const heading = new RegExp(`^#{1,6}\\s+${escapeRegExp(requirementId)}(?:\\s|:|$)`); const start = lines.findIndex(line => heading.test(line)); if (start < 0) { return []; } const level = (lines[start].match(/^#+/) ?? [''])[0].length; const section = []; for (let index = start + 1; index < lines.length; index += 1) { const nextHeading = lines[index].match(/^(#+)\s/); if (nextHeading && nextHeading[1].length <= level) { break; } section.push(lines[index]); } return [...new Set((section.join('\n').match(/\bAC-[A-Z0-9-]+\.\d+\b/g) ?? []))]; } function deletedRepositoryPaths(changedFiles) { const deleted = new Set(); for (const change of changedFiles ?? []) { if (change?.status === 'removed' || change?.status === 'renamed') { const pathname = change?.previous_filename; if (typeof pathname === 'string') { try { deleted.add(normalizeRepoPath(pathname)); } catch { // Invalid changed paths are reported by classifyChangedFiles. } } } } return deleted; } function validateWorkOrder(workOrderPath, fileContents, deletedPaths) { const errors = []; const acceptedReferences = []; const workOrderContent = contentFor(fileContents, workOrderPath, deletedPaths); if (typeof workOrderContent !== 'string' || workOrderContent.trim().length === 0) { return { acceptedReferences, errors: [`${workOrderPath} is missing or empty`], }; } if (Buffer.byteLength(workOrderContent, 'utf8') > MAX_DOCUMENT_BYTES) { return { acceptedReferences, errors: [`${workOrderPath} exceeds the ${MAX_DOCUMENT_BYTES}-byte document limit`], }; } let workOrder; try { workOrder = parseFrontmatter(workOrderContent); for (const field of WORK_ORDER_REQUIRED_FIELDS) { if (!Object.hasOwn(workOrder.data, field)) { throw new Error(`${workOrderPath} is missing frontmatter field ${field}`); } } asNonEmptyString(workOrder.data.id, 'id', workOrderPath); asNonEmptyString(workOrder.data.title, 'title', workOrderPath); asNonEmptyString(workOrder.data.status, 'status', workOrderPath); if (!Number.isInteger(workOrder.data.wave) || workOrder.data.wave < 0) { throw new Error(`${workOrderPath} frontmatter field wave must be a non-negative integer`); } if (!Array.isArray(workOrder.data.depends_on)) { throw new Error(`${workOrderPath} frontmatter field depends_on must be a list`); } asNonEmptyStringList(workOrder.data.requirements, 'requirements', workOrderPath); asNonEmptyStringList( workOrder.data.acceptance_criteria, 'acceptance_criteria', workOrderPath, ); asNonEmptyStringList(workOrder.data.system_design, 'system_design', workOrderPath); } catch (error) { return { acceptedReferences, errors: [error.message] }; } const criteriaByRequirement = acceptanceCriteriaByRequirement( workOrder.data.acceptance_criteria, workOrder.data.requirements, workOrderPath, errors, ); let planPath; try { planPath = resolveReference(workOrderPath, workOrder.data.plan); const expectedPlanPath = `${POSIX_PATH.dirname(workOrderPath)}/plan.md`; if (planPath !== expectedPlanPath) { throw new Error( `${workOrderPath} plan reference resolves to ${planPath}; expected sibling plan.md`, ); } } catch (error) { errors.push(error.message); } if (planPath) { const planContent = contentFor(fileContents, planPath, deletedPaths); if (typeof planContent !== 'string' || planContent.trim().length === 0) { errors.push(`${workOrderPath} references missing or empty ${planPath}`); } else if (Buffer.byteLength(planContent, 'utf8') > MAX_DOCUMENT_BYTES) { errors.push(`${planPath} exceeds the ${MAX_DOCUMENT_BYTES}-byte document limit`); } else { try { const plan = parseFrontmatter(planContent); if (!hasMarkdownLinkTo(planPath, plan.body, workOrderPath)) { errors.push(`${planPath} does not link back to ${workOrderPath}`); } const planRequirements = Array.isArray(plan.data.requirements) ? plan.data.requirements : []; for (const requirementId of workOrder.data.requirements) { if (!planRequirements.includes(requirementId)) { errors.push(`${planPath} does not cover requirement ${requirementId}`); } } const planDesignPaths = new Set(); for (const reference of Array.isArray(plan.data.system_design) ? plan.data.system_design : []) { try { planDesignPaths.add(resolveReference(planPath, reference)); } catch (error) { errors.push(error.message); } } const declaredRequirements = new Set(); for (const reference of workOrder.data.system_design) { try { const designPath = resolveReference(workOrderPath, reference); if (!planDesignPaths.has(designPath)) { errors.push(`${planPath} does not cover system design ${designPath}`); } const designContent = contentFor(fileContents, designPath, deletedPaths); if (typeof designContent !== 'string' || designContent.trim().length === 0) { errors.push(`${workOrderPath} references missing or empty ${designPath}`); continue; } if (Buffer.byteLength(designContent, 'utf8') > MAX_DOCUMENT_BYTES) { errors.push(`${designPath} exceeds the ${MAX_DOCUMENT_BYTES}-byte document limit`); continue; } const design = parseFrontmatter(designContent); const designMatch = /^docs\/specs\/([^/]+)\/system-design\/[^/]+\.md$/.exec( designPath, ); if (!designMatch) { errors.push(`${designPath} is outside the supported system-design directory`); continue; } const system = designMatch[1]; const designRequirements = Array.isArray(design.data.requirements) ? design.data.requirements : []; const ownedRequirements = workOrder.data.requirements.filter(requirementId => designRequirements.includes(requirementId) ); for (const requirementId of ownedRequirements) { declaredRequirements.add(requirementId); const definitions = requirementDefinitions(fileContents, requirementId, system); if (definitions.length === 0) { errors.push(`${requirementId} is not defined in ${system} requirements`); } else if (definitions.length > 1) { errors.push(`${requirementId} has ambiguous definitions in ${system} requirements`); } else { const definedCriteria = acceptanceCriteriaForRequirement( definitions[0].content, requirementId, ); for (const criterion of criteriaByRequirement.get(requirementId) ?? []) { if (!definedCriteria.includes(criterion)) { errors.push( `${criterion} is not defined under ${requirementId} in ${definitions[0].pathname}`, ); } } } } acceptedReferences.push({ designPath, requirements: ownedRequirements }); } catch (error) { errors.push(error.message); } } for (const requirementId of workOrder.data.requirements) { if (!declaredRequirements.has(requirementId)) { errors.push( `${workOrderPath} requirement ${requirementId} is not declared by any referenced system design`, ); } } } catch (error) { errors.push(`${planPath}: ${error.message}`); } } } return { acceptedReferences, errors }; } function validateCoverage({ changedFiles = [], fileContents = {} } = {}) { const classification = classifyChangedFiles(changedFiles); const result = { acceptedReferences: [], changedPaths: classification.changedPaths, errors: [], exemptPaths: classification.exemptPaths, ok: false, remediation: [], requiresCoverage: classification.requiresCoverage, status: 'error', triggeringPaths: classification.triggeringPaths, workOrders: [], }; if (!classification.requiresCoverage) { result.ok = true; result.status = 'exempt'; result.remediation = ['No delivery package is required for these changed paths.']; return result; } for (const pathname of classification.invalidPaths) { result.errors.push(`Changed path ${pathname} is invalid`); } result.workOrders = selectChangedWorkOrders(changedFiles); if (result.workOrders.length > MAX_DOCUMENTS) { result.errors.push(`More than ${MAX_DOCUMENTS} changed work orders were supplied`); } if (result.workOrders.length === 0) { result.status = 'missing'; result.errors.push( 'No changed work order covers the triggering paths. Add docs/plans//task-NN-.md with linked plan, requirements, acceptance criteria, and system design.', ); } const deletedPaths = deletedRepositoryPaths(changedFiles); let totalBytes = 0; for (const [pathname] of contentEntries(fileContents)) { const content = contentFor(fileContents, pathname, deletedPaths); if (typeof content === 'string') { totalBytes += Buffer.byteLength(content, 'utf8'); } } if (totalBytes > MAX_TOTAL_DOCUMENT_BYTES) { result.errors.push(`Referenced documents exceed the ${MAX_TOTAL_DOCUMENT_BYTES}-byte total limit`); } for (const workOrderPath of result.workOrders) { const validation = validateWorkOrder( workOrderPath, fileContents, deletedPaths, ); result.acceptedReferences.push(...validation.acceptedReferences); result.errors.push(...validation.errors); } if (result.errors.length === 0) { result.ok = true; result.status = 'covered'; result.remediation = ['Keep the linked work order and referenced contracts available at this revision.']; } else { result.status = result.status === 'missing' ? 'missing' : 'invalid'; result.remediation = [ 'Add or correct a changed work order and its complete plan, requirement, acceptance, and system-design references.', ]; } return result; } function requireCommitSha(value, description) { if (typeof value !== 'string' || !/^[0-9a-f]{40}$/i.test(value)) { throw new Error(`${description} is missing or invalid`); } return value; } function requireBranchName(value, description) { if (typeof value !== 'string' || value.trim().length === 0) { throw new Error(`${description} is missing or invalid`); } const branch = value.startsWith('refs/heads/') ? value.slice('refs/heads/'.length) : value; if ( branch.length === 0 || branch.includes('\0') || /[\r\n]/.test(branch) || branch.startsWith('refs/') ) { throw new Error(`${description} is missing or invalid`); } return branch; } function requirePullRequestNumber(value) { if (!Number.isInteger(value) || value < 1) { throw new Error('pull-request number is missing or invalid'); } return value; } class GitHubClient { constructor({ owner, repo, token, fetchImpl = globalThis.fetch } = {}) { if (typeof owner !== 'string' || typeof repo !== 'string' || owner === '' || repo === '') { throw new Error('GitHub repository owner and name are required'); } if (typeof token !== 'string' || token.length === 0) { throw new Error('GitHub token is required'); } if (typeof fetchImpl !== 'function') { throw new Error('fetch implementation is required'); } this.owner = owner; this.repo = repo; this.token = token; this.fetchImpl = fetchImpl; } async request(endpoint, { method = 'GET', body } = {}) { if (typeof endpoint !== 'string' || !endpoint.startsWith('/')) { throw new Error('GitHub endpoint must be an absolute API path'); } const headers = { Accept: 'application/vnd.github+json', Authorization: `Bearer ${this.token}`, 'X-GitHub-Api-Version': '2022-11-28', }; if (body !== undefined) { headers['Content-Type'] = 'application/json'; } const timeoutSignal = typeof AbortSignal === 'function' && typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(REQUEST_TIMEOUT_MS) : undefined; let response; try { response = await this.fetchImpl(`${GITHUB_API}${endpoint}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), ...(timeoutSignal ? { signal: timeoutSignal } : {}), }); } catch (error) { if (error?.name === 'TimeoutError' || error?.name === 'AbortError') { throw new Error(`GitHub API request timed out after ${REQUEST_TIMEOUT_MS}ms`); } throw error; } if (!response || typeof response.text !== 'function') { throw new Error('GitHub API returned an invalid response'); } const text = await response.text(); if (Buffer.byteLength(text, 'utf8') > MAX_RESPONSE_BYTES) { throw new Error(`GitHub API response exceeds the ${MAX_RESPONSE_BYTES}-byte limit`); } let payload = null; if (text.length > 0) { try { payload = JSON.parse(text); } catch { throw new Error('GitHub API returned invalid JSON'); } } if (!response.ok) { const detail = payload && typeof payload.message === 'string' ? `: ${payload.message.slice(0, 200)}` : ''; throw new Error(`GitHub API request failed with HTTP ${response.status}${detail}`); } return payload; } async getPullRequest(number) { requirePullRequestNumber(number); const pullRequest = await this.request( `/repos/${encodeURIComponent(this.owner)}/${encodeURIComponent(this.repo)}/pulls/${number}`, ); if (!pullRequest || pullRequest.number !== number) { throw new Error('GitHub returned a pull request with the wrong number'); } if (typeof pullRequest.head?.sha !== 'string' || typeof pullRequest.base?.sha !== 'string') { throw new Error(`pull request #${number} has incomplete revision metadata`); } requireCommitSha(pullRequest.head.sha, `pull request #${number} head revision`); requireCommitSha(pullRequest.base.sha, `pull request #${number} base revision`); if ( !Number.isInteger(pullRequest.changed_files) || pullRequest.changed_files < 0 || pullRequest.changed_files > MAX_CHANGED_FILES ) { throw new Error(`pull request #${number} has an invalid changed-file count`); } if (!Array.isArray(pullRequest.labels)) { throw new Error(`pull request #${number} has incomplete label metadata`); } return pullRequest; } async listFiles(number, expectedCount) { requirePullRequestNumber(number); const files = []; for (let page = 1; page <= 30; page += 1) { const pageFiles = await this.request( `/repos/${encodeURIComponent(this.owner)}/${encodeURIComponent(this.repo)}/pulls/${number}/files?per_page=100&page=${page}`, ); if (!Array.isArray(pageFiles)) { throw new Error('GitHub changed-file response is not a list'); } for (const file of pageFiles) { if (!file || typeof file.filename !== 'string' || file.filename.length === 0) { throw new Error('GitHub changed-file entry has no filename'); } normalizeRepoPath(file.filename); if (!['added', 'changed', 'copied', 'deleted', 'modified', 'removed', 'renamed', 'unchanged'].includes(file.status)) { throw new Error(`GitHub changed-file entry for ${file.filename} has no supported status`); } if (file.status === 'renamed') { if (typeof file.previous_filename !== 'string' || file.previous_filename.length === 0) { throw new Error(`GitHub renamed file ${file.filename} has no previous filename`); } normalizeRepoPath(file.previous_filename); } } files.push(...pageFiles); if (files.length >= MAX_CHANGED_FILES) { throw new Error("GitHub changed-file response reaches the 3,000-file limit"); } if (pageFiles.length < 100) { if (expectedCount !== undefined && expectedCount !== files.length) { throw new Error( `GitHub changed-file count ${files.length} does not match pull request metadata ${expectedCount}`, ); } return files; } } throw new Error('GitHub changed-file pagination exceeded the supported limit'); } async getFile(pathname, ref) { const normalized = normalizeRepoPath(pathname); if (typeof ref !== 'string' || ref.length === 0) { throw new Error(`content reference for ${normalized} is missing`); } const encodedPath = normalized.split('/').map(encodeURIComponent).join('/'); const response = await this.request( `/repos/${encodeURIComponent(this.owner)}/${encodeURIComponent(this.repo)}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`, ); if (typeof response?.path !== 'string' || normalizeRepoPath(response.path) !== normalized) { throw new Error(`${normalized} response has a mismatched returned path`); } if (!response || response.type !== 'file' || response.encoding !== 'base64') { throw new Error(`${normalized} is not a regular base64-encoded file`); } if (typeof response.content !== 'string') { throw new Error(`${normalized} has no file content`); } const encoded = response.content.replace(/\s/g, ''); if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) { throw new Error(`${normalized} has invalid base64 content`); } const bytes = Buffer.from(encoded, 'base64'); if (bytes.length > MAX_DOCUMENT_BYTES) { throw new Error(`${normalized} exceeds the ${MAX_DOCUMENT_BYTES}-byte document limit`); } try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { throw new Error(`${normalized} is not valid UTF-8 text`); } } async listDirectory(pathname, ref) { const normalized = normalizeRepoPath(pathname); const encodedPath = normalized.split('/').map(encodeURIComponent).join('/'); const response = await this.request( `/repos/${encodeURIComponent(this.owner)}/${encodeURIComponent(this.repo)}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`, ); if (!Array.isArray(response)) { throw new Error(`${normalized} is not a directory listing`); } return response.map(entry => { if (!entry || typeof entry.path !== 'string') { throw new Error(`${normalized} contains an invalid directory entry`); } const entryPath = normalizeRepoPath(entry.path); if (!['file', 'directory'].includes(entry.type)) { throw new Error(`${entryPath} is not a regular file or directory`); } return { path: entryPath, type: entry.type }; }); } async searchCode(text, directory) { if (typeof text !== 'string' || text.trim().length === 0) { throw new Error('GitHub code-search text is missing'); } const normalizedDirectory = normalizeRepoPath(directory); const query = `"${text}" repo:${this.owner}/${this.repo} path:${normalizedDirectory}`; const response = await this.request( `/search/code?q=${encodeURIComponent(query)}&per_page=100`, ); if ( !response || !Array.isArray(response.items) || response.incomplete_results === true || !Number.isInteger(response.total_count) || response.total_count < response.items.length || response.total_count > 100 ) { throw new Error('GitHub code-search response is incomplete or exceeds the supported limit'); } const prefix = `${normalizedDirectory}/`; const paths = new Set(); for (const item of response.items) { if (typeof item?.path !== 'string') { throw new Error('GitHub code-search entry has no path'); } const itemPath = normalizeRepoPath(item.path); if (!itemPath.startsWith(prefix) || !itemPath.endsWith('.md')) { throw new Error(`GitHub code-search returned an unsupported path: ${itemPath}`); } paths.add(itemPath); } return [...paths]; } async createCommitStatus(sha, status) { requireCommitSha(sha, 'status revision'); if (!status || !['pending', 'success', 'failure', 'error'].includes(status.state)) { throw new Error('commit status has an invalid state'); } return this.request( `/repos/${encodeURIComponent(this.owner)}/${encodeURIComponent(this.repo)}/statuses/${sha}`, { method: 'POST', body: { state: status.state, context: STATUS_CONTEXT, description: String(status.description ?? '').slice(0, 140), target_url: status.targetUrl, }, }, ); } async graphql(query, variables) { const response = await this.request('/graphql', { method: 'POST', body: { query, variables }, }); if (Array.isArray(response?.errors) && response.errors.length > 0) { throw new Error(`GitHub GraphQL request failed: ${String(response.errors[0].message ?? 'unknown error')}`); } return response?.data; } async listMergeQueueEntries(branch) { const targetBranch = requireBranchName(branch, 'merge-queue target branch'); const query = ` query($owner: String!, $repo: String!, $branch: String!, $after: String) { repository(owner: $owner, name: $repo) { mergeQueue(branch: $branch) { entries(first: 100, after: $after) { nodes { baseCommit { oid } headCommit { oid } pullRequest { number headRefOid } } pageInfo { hasNextPage endCursor } } } } } `; const entries = []; let after = null; for (let page = 0; page < 10; page += 1) { const data = await this.graphql(query, { branch: targetBranch, owner: this.owner, repo: this.repo, after, }); const connection = data?.repository?.mergeQueue?.entries; if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo) { throw new Error('GitHub merge queue response is incomplete'); } entries.push(...connection.nodes); if (!connection.pageInfo.hasNextPage) { return entries; } if (typeof connection.pageInfo.endCursor !== 'string') { throw new Error('GitHub merge queue pagination cursor is missing'); } after = connection.pageInfo.endCursor; } throw new Error('GitHub merge queue pagination exceeded the supported limit'); } } function isNoDocsOverride(pullRequest) { return Array.isArray(pullRequest?.labels) && pullRequest.labels.some(label => label?.name === NO_DOCS_LABEL); } function resultMetadata(pullRequest) { return { baseSha: pullRequest.base.sha, baseRef: pullRequest.base.ref, headSha: pullRequest.head.sha, labels: pullRequest.labels.map(label => label?.name).filter(name => typeof name === 'string').sort(), }; } function sameMetadata(left, right) { return ( left.headSha === right.headSha && left.baseSha === right.baseSha && left.baseRef === right.baseRef && JSON.stringify(left.labels) === JSON.stringify(right.labels) ); } function errorCoverageResult(message, metadata = {}) { return { acceptedReferences: [], changedPaths: [], errors: [message], exemptPaths: [], baseRef: metadata.baseRef, headSha: metadata.headSha, ok: false, remediation: ['Retry the workflow after GitHub returns complete, stable revision and label data.'], requiresCoverage: true, status: 'error', triggeringPaths: [], workOrders: [], }; } function isMissingResourceError(error) { return /\bHTTP 404\b/.test(String(error?.message ?? error)); } async function loadCoverageContents({ client, changedFiles, headSha }) { if (!classifyChangedFiles(changedFiles).requiresCoverage) { return {}; } const contents = {}; const loaded = new Set(); const requirementSearches = new Map(); const requirementDirectories = new Map(); let documentCount = 0; let totalBytes = 0; async function load(pathname) { const normalized = normalizeRepoPath(pathname); if (loaded.has(normalized)) { return contents[normalized]; } if (documentCount >= MAX_DOCUMENTS) { throw new Error(`Referenced documents exceed the ${MAX_DOCUMENTS}-document limit`); } let content; try { content = await client.getFile(normalized, headSha); } catch (error) { if (!isMissingResourceError(error)) { throw error; } } loaded.add(normalized); documentCount += 1; if (typeof content !== 'string' || content.trim().length === 0) { contents[normalized] = undefined; return undefined; } const size = Buffer.byteLength(content, 'utf8'); if (size > MAX_DOCUMENT_BYTES) { throw new Error(`${normalized} exceeds the ${MAX_DOCUMENT_BYTES}-byte document limit`); } totalBytes += size; if (totalBytes > MAX_TOTAL_DOCUMENT_BYTES) { throw new Error(`Referenced documents exceed the ${MAX_TOTAL_DOCUMENT_BYTES}-byte total limit`); } contents[normalized] = content; return content; } for (const workOrderPath of selectChangedWorkOrders(changedFiles)) { const workOrderContent = await load(workOrderPath); if (workOrderContent === undefined) { continue; } let workOrder; try { workOrder = parseFrontmatter(workOrderContent).data; } catch { continue; } let planPath; try { planPath = resolveReference(workOrderPath, workOrder.plan); } catch { continue; } const planContent = await load(planPath); if (planContent === undefined) { continue; } let plan; try { plan = parseFrontmatter(planContent).data; } catch { continue; } const designReferences = [ ...(Array.isArray(workOrder.system_design) ? workOrder.system_design : []), ...(Array.isArray(plan.system_design) ? plan.system_design : []), ]; const designPaths = new Set(); for (const reference of designReferences) { try { designPaths.add(resolveReference(workOrderPath, reference)); } catch { // The validator reports the invalid reference; do not read outside the root. } } for (const designPath of designPaths) { const designContent = await load(designPath); if (designContent === undefined) { continue; } let system; try { parseFrontmatter(designContent); const match = /^docs\/specs\/([^/]+)\/system-design\/[^/]+\.md$/.exec(designPath); if (!match) { continue; } system = match[1]; } catch { continue; } const requirementDirectory = `docs/specs/${system}/requirements`; const requirementPaths = new Set(); const changedRepositoryPaths = changedPaths(changedFiles).paths; for (const pathname of changedRepositoryPaths) { if (pathname.startsWith(`${requirementDirectory}/`) && pathname.endsWith('.md')) { requirementPaths.add(pathname); } } const unresolvedRequirementIds = []; if (typeof client.searchCode === 'function') { for (const requirementId of workOrder.requirements ?? []) { const searchKey = JSON.stringify([requirementDirectory, requirementId]); if (!requirementSearches.has(searchKey)) { requirementSearches.set( searchKey, await client.searchCode(requirementId, requirementDirectory), ); } const matches = requirementSearches.get(searchKey); if (matches.length === 0) { unresolvedRequirementIds.push(requirementId); } for (const pathname of matches) { requirementPaths.add(pathname); } } } else { unresolvedRequirementIds.push(...(workOrder.requirements ?? [])); } if (unresolvedRequirementIds.length > 0 && typeof client.listDirectory === 'function') { if (!requirementDirectories.has(requirementDirectory)) { let entries = []; try { entries = await client.listDirectory(requirementDirectory, headSha); } catch (error) { if (!isMissingResourceError(error)) { throw error; } } requirementDirectories.set(requirementDirectory, entries); } const entries = requirementDirectories.get(requirementDirectory); const candidateNames = new Set(unresolvedRequirementIds.map(requirementId => { const parts = requirementId.split('-'); return `${parts.slice(2).join('-').replace(/-\d+$/, '').toLowerCase()}.md`; })); for (const entry of entries) { if (entry.type === 'file' && candidateNames.has(POSIX_PATH.basename(entry.path))) { requirementPaths.add(entry.path); } } } for (const pathname of requirementPaths) { const requirementContent = await load(pathname); if (requirementContent === undefined) { continue; } } } } return contents; } async function evaluatePullRequestSnapshot({ client, pullRequest, expectedHeadSha }) { if (expectedHeadSha && pullRequest.head.sha !== expectedHeadSha) { return errorCoverageResult( `pull request #${pullRequest.number} head ${pullRequest.head.sha} does not match expected queue head ${expectedHeadSha}`, { headSha: pullRequest.head.sha }, ); } const metadata = { baseSha: pullRequest.base.sha, baseRef: pullRequest.base.ref, headSha: pullRequest.head.sha, pullRequestNumber: pullRequest.number, }; if (isNoDocsOverride(pullRequest)) { return { acceptedReferences: [], changedPaths: [], errors: [], exemptPaths: [], ...metadata, ok: true, override: NO_DOCS_LABEL, remediation: ['The no-docs-allow label is present. Remove it to restore normal evaluation.'], requiresCoverage: false, status: 'override', triggeringPaths: [], workOrders: [], }; } const changedFiles = await client.listFiles(pullRequest.number, pullRequest.changed_files); const fileContents = await loadCoverageContents({ changedFiles, client, headSha: pullRequest.head.sha, }); return { ...validateCoverage({ changedFiles, fileContents }), ...metadata, }; } async function evaluatePullRequest({ client, pullNumber, expectedHeadSha, maxAttempts = 3 } = {}) { requirePullRequestNumber(pullNumber); if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 5) { throw new Error('maxAttempts must be an integer from 1 through 5'); } let lastMetadata; for (let attempt = 0; attempt < maxAttempts; attempt += 1) { let pullRequest; try { pullRequest = await client.getPullRequest(pullNumber); const before = resultMetadata(pullRequest); if (expectedHeadSha && pullRequest.head.sha !== expectedHeadSha) { return errorCoverageResult( `pull request #${pullNumber} head does not match the expected revision`, before, ); } const result = await evaluatePullRequestSnapshot({ client, expectedHeadSha, pullRequest, }); const latest = await client.getPullRequest(pullNumber); const after = resultMetadata(latest); if (sameMetadata(before, after)) { return result; } lastMetadata = after; } catch (error) { return errorCoverageResult(error.message, lastMetadata ?? {}); } } return errorCoverageResult( `pull request #${pullNumber} changed during evaluation; retry limit reached`, lastMetadata ?? {}, ); } function validateMergeQueueEntries(entries) { if (!Array.isArray(entries) || entries.length === 0) { throw new Error('merge-group membership is missing'); } return entries.map((entry, index) => { const entryBaseSha = requireCommitSha(entry?.baseCommit?.oid, `merge-queue entry ${index} base`); const entryHeadSha = requireCommitSha(entry?.headCommit?.oid, `merge-queue entry ${index} head`); const number = requirePullRequestNumber(entry?.pullRequest?.number); if (entryBaseSha === entryHeadSha) { throw new Error(`merge-queue entry ${number} has identical base and head revisions`); } return { baseSha: entryBaseSha, headSha: entryHeadSha, number, pullRequest: entry.pullRequest, raw: entry, }; }); } function resolveMergeGroupMembers({ baseSha, headSha, entries } = {}) { requireCommitSha(baseSha, 'merge-group base revision'); requireCommitSha(headSha, 'merge-group head revision'); const normalizedEntries = validateMergeQueueEntries(entries); const members = []; const usedEntries = new Set(); const usedPullRequests = new Set(); let current = headSha; while (current !== baseSha) { const candidates = normalizedEntries.filter(entry => entry.headSha === current); if (candidates.length === 0) { throw new Error(`merge-group boundary ${current} is not connected to its base`); } if (candidates.length > 1) { throw new Error(`merge-group boundary ${current} has ambiguous membership`); } const entry = candidates[0]; if (usedEntries.has(entry) || usedPullRequests.has(entry.number)) { throw new Error('merge-group membership contains a cycle or duplicate pull request'); } usedEntries.add(entry); usedPullRequests.add(entry.number); members.push(entry); current = entry.baseSha; if (members.length > normalizedEntries.length) { throw new Error('merge-group membership exceeds the available queue entries'); } } if (members.length === 0) { throw new Error('merge-group contains no pull requests'); } members.reverse(); return { baseSha, headSha, members }; } async function evaluateMergeGroup({ client, baseSha, headSha, entries, maxAttempts = 3 } = {}) { const group = resolveMergeGroupMembers({ baseSha, entries, headSha }); const memberResults = []; for (const member of group.members) { const expectedHeadSha = requireCommitSha( member.pullRequest?.headRefOid, `merge-queue member ${member.number} pull-request head`, ); const result = await evaluatePullRequest({ client, expectedHeadSha, maxAttempts, pullNumber: member.number, }); memberResults.push({ ...result, baseSha: member.baseSha, expectedHeadSha, number: member.number, }); } const errors = memberResults.flatMap(result => result.errors ?? []); return { baseSha, errors, headSha, memberResults, ok: memberResults.every(result => result.ok), status: memberResults.some(result => result.status === 'error') ? 'error' : memberResults.every(result => result.ok) ? 'success' : 'failure', }; } function findAffectedMergeGroups({ entries, pullRequestNumber } = {}) { if (!Array.isArray(entries) || entries.length === 0) { return []; } if (pullRequestNumber !== undefined) { requirePullRequestNumber(pullRequestNumber); } const normalizedEntries = validateMergeQueueEntries(entries); const headRevisions = new Set(normalizedEntries.map(entry => entry.headSha)); const terminalEntries = normalizedEntries.filter(entry => !normalizedEntries.some( other => other.baseSha === entry.headSha, )); if (terminalEntries.length === 0) { throw new Error('merge-queue entries do not have a terminal group boundary'); } const groups = []; const consumed = new Set(); for (const terminal of terminalEntries) { const chain = []; const chainEntries = new Set(); let current = terminal.headSha; while (true) { const candidates = normalizedEntries.filter(entry => entry.headSha === current); if (candidates.length === 0) { throw new Error(`merge-group boundary ${current} is not connected to any queue entry`); } if (candidates.length > 1) { throw new Error(`merge-group boundary ${current} has ambiguous membership`); } const entry = candidates[0]; if (chainEntries.has(entry) || consumed.has(entry)) { throw new Error('merge-queue entries overlap or contain a cycle'); } chainEntries.add(entry); chain.unshift(entry); if (!headRevisions.has(entry.baseSha)) { break; } current = entry.baseSha; } for (const entry of chain) { consumed.add(entry); } for (let prefixLength = 1; prefixLength <= chain.length; prefixLength += 1) { const prefix = chain.slice(0, prefixLength); if ( pullRequestNumber !== undefined && !prefix.some(entry => entry.number === pullRequestNumber) ) { continue; } groups.push({ baseSha: prefix[0].baseSha, entries: prefix.map(entry => entry.raw), headSha: prefix[prefix.length - 1].headSha, }); } } if (consumed.size !== normalizedEntries.length) { throw new Error('merge-queue membership contains an unconnected entry'); } return groups; } function statusState(result) { if (result?.status === 'error') { return 'error'; } return result?.ok ? 'success' : 'failure'; } function markdownValue(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", ''') .replaceAll('`', '`') .replace(/[\r\n]/g, ' '); } function resultSummary(result) { const lines = [`## PR documentation coverage`, `Result: **${markdownValue(result.status)}**`]; if (result.override) { lines.push(`Override: \`${markdownValue(result.override)}\``); } const reportedPaths = result.triggeringPaths?.length > 0 ? result.triggeringPaths : result.changedPaths ?? []; if (reportedPaths.length > 0) { lines.push('', result.triggeringPaths?.length > 0 ? 'Triggering paths:' : 'Changed paths:'); for (const pathname of reportedPaths) { lines.push(`- \`${markdownValue(pathname)}\``); } } if (result.workOrders?.length > 0) { lines.push('', 'Accepted work orders:'); for (const pathname of result.workOrders) { lines.push(`- \`${markdownValue(pathname)}\``); } } if (result.acceptedReferences?.length > 0) { lines.push('', 'Accepted references:'); for (const reference of result.acceptedReferences) { lines.push(`- \`${markdownValue(reference.designPath)}\``); } } if (result.errors?.length > 0) { lines.push('', 'Problems:'); for (const error of result.errors) { lines.push(`- ${markdownValue(error)}`); } } if (result.remediation?.length > 0) { lines.push('', 'Next steps:'); for (const remediation of result.remediation) { lines.push(`- ${markdownValue(remediation)}`); } } if (result.memberResults?.length > 0) { lines.push('', 'Merge-group members:'); for (const member of result.memberResults) { lines.push(`- #${member.number}: **${markdownValue(member.status)}**`); } } return `${lines.join('\n')}\n`; } function statusDescription(result) { if (result.override) { return `Override: ${NO_DOCS_LABEL}`; } if (result.status === 'exempt') { return 'All changed paths are exempt'; } if (result.status === 'covered') { return 'Linked delivery package found'; } if (result.status === 'missing') { return 'Linked delivery package is missing'; } if (result.status === 'invalid') { return 'Linked delivery package is incomplete'; } if (result.status === 'success') { return 'Every merge-group member satisfies documentation coverage'; } if (result.status === 'failure') { return 'A merge-group member does not satisfy documentation coverage'; } return 'Coverage evaluation failed'; } function runUrl(env) { if (!env.GITHUB_SERVER_URL || !env.GITHUB_REPOSITORY || !env.GITHUB_RUN_ID) { return undefined; } return `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${encodeURIComponent(env.GITHUB_RUN_ID)}`; } function eventPullRequestNumber(event) { const value = event?.pull_request?.number ?? event?.inputs?.pr_number; const number = typeof value === 'string' ? Number(value) : value; return requirePullRequestNumber(number); } async function writeRunSummary(summary, env, writeSummary) { if (typeof writeSummary === 'function') { await writeSummary(summary); return; } if (typeof env.GITHUB_STEP_SUMMARY === 'string' && env.GITHUB_STEP_SUMMARY.length > 0) { fs.appendFileSync(env.GITHUB_STEP_SUMMARY, summary, 'utf8'); } } async function publishResult(client, sha, result, targetUrl) { await client.createCommitStatus(sha, { description: statusDescription(result), state: statusState(result), targetUrl, }); } async function evaluateAffectedGroups({ client, pullNumber, targetUrl, entries }) { const groups = findAffectedMergeGroups({ entries, pullRequestNumber: pullNumber }); const groupResults = []; for (const group of groups) { await client.createCommitStatus(group.headSha, { description: 'Evaluating merge-group documentation coverage', state: 'pending', targetUrl, }); const result = await evaluateMergeGroup({ baseSha: group.baseSha, client, entries: group.entries, headSha: group.headSha, }); await publishResult(client, group.headSha, result, targetUrl); groupResults.push(result); } return groupResults; } async function run({ client, env = process.env, event, eventName, writeSummary } = {}) { const effectiveEventName = eventName ?? env.GITHUB_EVENT_NAME; const effectiveEvent = event ?? (() => { if (typeof env.GITHUB_EVENT_PATH !== 'string' || env.GITHUB_EVENT_PATH.length === 0) { throw new Error('GitHub event payload path is missing'); } return JSON.parse(fs.readFileSync(env.GITHUB_EVENT_PATH, 'utf8')); })(); if (!['pull_request_target', 'workflow_dispatch', 'merge_group'].includes(effectiveEventName)) { throw new Error(`unsupported GitHub event: ${effectiveEventName}`); } let apiClient = client; if (!apiClient) { const repository = /^([^/]+)\/([^/]+)$/.exec(env.GITHUB_REPOSITORY ?? ''); if (!repository) { throw new Error('GITHUB_REPOSITORY is missing or invalid'); } apiClient = new GitHubClient({ fetchImpl: globalThis.fetch, owner: repository[1], repo: repository[2], token: env.GITHUB_TOKEN, }); } const targetUrl = runUrl(env); let pendingSha; let result; try { if (effectiveEventName === 'merge_group') { const targetBranch = requireBranchName( effectiveEvent.merge_group?.base_ref, 'merge-group target branch', ); const baseSha = requireCommitSha( effectiveEvent.merge_group?.base_sha, 'merge-group base revision', ); const headSha = requireCommitSha( effectiveEvent.merge_group?.head_sha, 'merge-group head revision', ); pendingSha = headSha; await apiClient.createCommitStatus(headSha, { description: 'Evaluating merge-group documentation coverage', state: 'pending', targetUrl, }); const entries = await apiClient.listMergeQueueEntries(targetBranch); result = await evaluateMergeGroup({ baseSha, client: apiClient, entries, headSha, }); await publishResult(apiClient, headSha, result, targetUrl); } else { const pullNumber = eventPullRequestNumber(effectiveEvent); const current = await apiClient.getPullRequest(pullNumber); pendingSha = requireCommitSha(current.head.sha, 'pull-request head revision'); await apiClient.createCommitStatus(pendingSha, { description: 'Evaluating pull-request documentation coverage', state: 'pending', targetUrl, }); result = await evaluatePullRequest({ client: apiClient, pullNumber, }); await publishResult(apiClient, result.headSha ?? pendingSha, result, targetUrl); if (effectiveEventName !== 'workflow_dispatch') { const action = effectiveEvent.action; if (action === 'labeled' || action === 'unlabeled') { const targetBranch = requireBranchName( result.baseRef ?? current.base?.ref, 'pull-request target branch', ); const entries = await apiClient.listMergeQueueEntries(targetBranch); const groupResults = await evaluateAffectedGroups({ client: apiClient, entries, pullNumber, targetUrl, }); if (groupResults.some(groupResult => !groupResult.ok)) { result = { ...result, affectedGroups: groupResults, errors: [ ...(result.errors ?? []), 'An affected merge group does not satisfy documentation coverage.', ], ok: false, status: 'failure', }; } } } } } catch (error) { result = errorCoverageResult(error.message, { headSha: pendingSha }); if (pendingSha) { try { await publishResult(apiClient, pendingSha, result, targetUrl); } catch { // The original error remains the authoritative failure for the job. } } } await writeRunSummary(resultSummary(result), env, writeSummary); return { exitCode: result.ok ? 0 : 1, result }; } if (require.main === module) { run() .then(({ exitCode }) => { process.exitCode = exitCode; }) .catch(error => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }); } module.exports = { classifyChangedFiles, evaluateMergeGroup, evaluatePullRequest, findAffectedMergeGroups, GitHubClient, isNoDocsOverride, normalizeRepoPath, parseFrontmatter, resolveMergeGroupMembers, resultSummary, run, selectChangedWorkOrders, statusState, validateCoverage, };