New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

codeowners-audit

Package Overview
Dependencies
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

codeowners-audit - npm Package Compare versions

Comparing version
2.3.0
to
2.4.0
+1
-1
package.json
{
"name": "codeowners-audit",
"version": "2.3.0",
"version": "2.4.0",
"description": "Generate an HTML report for CODEOWNERS ownership gaps and run in CI or from the CLI to fail when files are not covered.",

@@ -5,0 +5,0 @@ "type": "module",

@@ -30,4 +30,5 @@ <p align="center">

- Team ownership explorer with quick team chips and owned-file filtering
- Supports multiple `CODEOWNERS` files in nested directories
- Matches GitHub `CODEOWNERS` discovery precedence: `.github/`, repository root, then `docs/`
- Detects CODEOWNERS patterns that match no repository paths
- Warns when extra or unsupported `CODEOWNERS` files will be ignored by GitHub
- Optional upload to [zenbin.org](https://zenbin.org) for easy sharing

@@ -98,2 +99,3 @@

| `--upload` | Upload to zenbin and print a public URL |
| `-y, --yes` | Automatically answer yes to interactive prompts |
| `--no-open` | Do not prompt to open the report in your browser |

@@ -200,3 +202,5 @@ | `--verbose` | Enable verbose progress output |

- Within a single `CODEOWNERS` file, the **last matching rule wins**.
- If multiple `CODEOWNERS` files exist, they are applied from broader scope to narrower scope (nested files can override broader files).
- GitHub only considers `CODEOWNERS` at `.github/CODEOWNERS`, `CODEOWNERS`, and `docs/CODEOWNERS`, using the first file found in that order.
- Patterns are always resolved from the repository root, regardless of which supported `CODEOWNERS` location is active.
- Extra `CODEOWNERS` files in supported locations and any `CODEOWNERS` files outside those locations are reported as ignored by GitHub.
- Directory rules match descendant files whether they are written as `/path/to/dir` or `/path/to/dir/`.

@@ -221,3 +225,4 @@ - `CODEOWNERS` negation patterns (`!pattern`) are ignored.

- team ownership explorer for filtering files by `@org/team`
- detected `CODEOWNERS` files and rule counts
- active `CODEOWNERS` file and rule count
- warnings for extra or unsupported `CODEOWNERS` files that GitHub will ignore
- warnings for CODEOWNERS patterns that match no repository paths

@@ -224,0 +229,0 @@

+219
-126

@@ -25,2 +25,4 @@ #!/usr/bin/env node

const FILE_ANALYSIS_PROGRESS_INTERVAL = 20000
const SUPPORTED_CODEOWNERS_PATHS = ['.github/CODEOWNERS', 'CODEOWNERS', 'docs/CODEOWNERS']
const SUPPORTED_CODEOWNERS_PATHS_LABEL = SUPPORTED_CODEOWNERS_PATHS.join(', ')
const EXIT_CODE_UNCOVERED = 1

@@ -91,3 +93,3 @@ const EXIT_CODE_RUNTIME_ERROR = 2

console.log('Full repository clone required for --suggest-teams (this may take longer for large repositories).')
if (interactiveStdin) {
if (interactiveStdin && !options.yes) {
const confirmed = await promptForFullClone(cloneUrl)

@@ -129,12 +131,11 @@ if (!confirmed) {

const allRepoFiles = listRepoFiles(options.includeUntracked, repoRoot)
const codeownersFilePaths = allRepoFiles.filter(isCodeownersFile)
if (codeownersFilePaths.length === 0) {
throw new Error('No CODEOWNERS files found in this repository.')
const discoveredCodeownersPaths = listDiscoveredCodeownersPaths(allRepoFiles)
const codeownersPath = resolveActiveCodeownersPath(discoveredCodeownersPaths)
if (!codeownersPath) {
throw new Error(buildMissingSupportedCodeownersError(discoveredCodeownersPaths))
}
const codeownersDescriptors = codeownersFilePaths
.map(codeownersPath => loadCodeownersDescriptor(repoRoot, codeownersPath))
.sort(compareCodeownersDescriptor)
const missingPathWarnings = collectMissingCodeownersPathWarnings(codeownersDescriptors, allRepoFiles)
const codeownersDescriptor = loadCodeownersDescriptor(repoRoot, codeownersPath)
const discoveryWarnings = collectCodeownersDiscoveryWarnings(discoveredCodeownersPaths, codeownersPath)
const missingPathWarnings = collectMissingCodeownersPathWarnings(codeownersDescriptor, allRepoFiles)

@@ -152,4 +153,6 @@ const scopeFilteredFiles = filterFilesByCliGlobs(allRepoFiles, options.checkGlobs)

progress('Scanning %d files against CODEOWNERS rules...', filesToAnalyze.length)
const report = buildReport(repoRoot, filesToAnalyze, codeownersDescriptors, options, progress)
const report = buildReport(repoRoot, filesToAnalyze, codeownersDescriptor, options, progress)
report.codeownersValidationMeta = {
discoveryWarnings,
discoveryWarningCount: discoveryWarnings.length,
missingPathWarnings,

@@ -203,3 +206,3 @@ missingPathWarningCount: missingPathWarnings.length,

if (options.open) {
const shouldOpen = await promptForReportOpen(reportLocation)
const shouldOpen = options.yes ? true : await promptForReportOpen(reportLocation)
if (shouldOpen) {

@@ -254,2 +257,3 @@ try {

* upload: boolean,
* yes: boolean,
* open: boolean,

@@ -287,2 +291,3 @@ * verbose: boolean,

let upload = false
let yes = false
let open = true

@@ -458,2 +463,7 @@ let verbose = false

if (arg === '--yes' || arg === '-y') {
yes = true
continue
}
if (arg === '--no-open') {

@@ -561,2 +571,3 @@ open = false

upload,
yes,
open,

@@ -751,2 +762,3 @@ verbose,

['--upload', `Upload to ${UPLOAD_PROVIDER} and print a public URL`],
['-y, --yes', 'Automatically answer yes to interactive prompts'],
['--no-open', 'Do not prompt to open the report in your browser'],

@@ -935,5 +947,10 @@ ['--verbose', 'Enable verbose progress output'],

* codeownersValidationMeta?: {
* discoveryWarnings?: {
* path: string,
* type: 'unused-supported-location'|'unsupported-location',
* referencePath?: string,
* message: string
* }[],
* missingPathWarnings?: {
* codeownersPath: string,
* scopedDir: string,
* pattern: string,

@@ -958,2 +975,6 @@ * owners: string[]

: JSON.stringify(options.checkGlobs)
const discoveryWarnings = Array.isArray(report.codeownersValidationMeta?.discoveryWarnings)
? report.codeownersValidationMeta.discoveryWarnings
: []
const locationWarningCount = discoveryWarnings.length
const missingPathWarnings = Array.isArray(report.codeownersValidationMeta?.missingPathWarnings)

@@ -995,2 +1016,16 @@ ? report.codeownersValidationMeta.missingPathWarnings

if (options.noReport && locationWarningCount > 0) {
console.error(
colorizeCliText(
`CODEOWNERS location warnings (${locationWarningCount}):`,
[ANSI_BOLD, ANSI_YELLOW],
colorStderr
)
)
for (const warning of discoveryWarnings) {
console.error('%s', formatCodeownersDiscoveryWarningForCli(warning, colorStderr))
}
console.error('')
}
if (options.showCoverageSummary !== false) {

@@ -1004,2 +1039,3 @@ console.log(

`${colorizeCliText('missing path warnings:', [ANSI_DIM], colorStdout)} ${colorizeCliText(String(missingPathWarningCount), missingPathWarningCount > 0 ? [ANSI_BOLD, ANSI_YELLOW] : [ANSI_BOLD, ANSI_GREEN], colorStdout)}`,
`${colorizeCliText('location warnings:', [ANSI_DIM], colorStdout)} ${colorizeCliText(String(locationWarningCount), locationWarningCount > 0 ? [ANSI_BOLD, ANSI_YELLOW] : [ANSI_BOLD, ANSI_GREEN], colorStdout)}`,
].join('\n')

@@ -1049,3 +1085,3 @@ )

const matchers = patterns.map(pattern => createPatternMatcher(pattern))
return (filePath) => matchers.some(matches => matches(filePath, filePath))
return (filePath) => matchers.some(matches => matches(filePath))
}

@@ -1196,3 +1232,34 @@

/**
* Determine if a path points to a CODEOWNERS file.
* Format a CODEOWNERS discovery warning for CLI output.
* @param {{
* path: string,
* type: 'unused-supported-location'|'unsupported-location',
* referencePath?: string,
* message: string
* }} warning
* @param {boolean} useColor
* @returns {string}
*/
function formatCodeownersDiscoveryWarningForCli (warning, useColor) {
const bullet = colorizeCliText('- ', [ANSI_DIM], useColor)
const warningPath = colorizeCliText(warning.path, [ANSI_YELLOW], useColor)
const warningText = colorizeCliText(
warning.type === 'unused-supported-location'
? ' is unused because GitHub selects '
: ' is in an unsupported location and is ignored by GitHub.',
[ANSI_DIM],
useColor
)
if (warning.type === 'unused-supported-location' && warning.referencePath) {
const referencePath = colorizeCliText(warning.referencePath, [ANSI_CYAN], useColor)
const trailingText = colorizeCliText(' first.', [ANSI_DIM], useColor)
return bullet + warningPath + warningText + referencePath + trailingText
}
return bullet + warningPath + warningText
}
/**
* Determine if a path points to any CODEOWNERS file.
* @param {string} filePath

@@ -1206,19 +1273,50 @@ * @returns {boolean}

/**
* Resolve the scope base for a CODEOWNERS file.
* GitHub treats top-level CODEOWNERS files in root, .github/, and docs/
* as repository-wide files.
* @param {string} codeownersPath
* Determine if a path points to a supported GitHub CODEOWNERS location.
* @param {string} filePath
* @returns {boolean}
*/
function isSupportedCodeownersFile (filePath) {
return SUPPORTED_CODEOWNERS_PATHS.includes(filePath)
}
/**
* List all discovered CODEOWNERS file paths in the repository.
* @param {string[]} repoFiles
* @returns {string[]}
*/
function listDiscoveredCodeownersPaths (repoFiles) {
return repoFiles.filter(isCodeownersFile)
}
/**
* Resolve the active CODEOWNERS file using GitHub's precedence rules.
* GitHub only considers top-level CODEOWNERS files in `.github/`, the
* repository root, and `docs/`, using the first file it finds in that order.
* @param {string[]} discoveredCodeownersPaths
* @returns {string|undefined}
*/
function resolveActiveCodeownersPath (discoveredCodeownersPaths) {
return SUPPORTED_CODEOWNERS_PATHS.find(codeownersPath => discoveredCodeownersPaths.includes(codeownersPath))
}
/**
* Build a clear error when no supported CODEOWNERS file is available.
* @param {string[]} discoveredCodeownersPaths
* @returns {string}
*/
function resolveCodeownersScopeBase (codeownersPath) {
if (
codeownersPath === 'CODEOWNERS' ||
codeownersPath === '.github/CODEOWNERS' ||
codeownersPath === 'docs/CODEOWNERS'
) {
return ''
function buildMissingSupportedCodeownersError (discoveredCodeownersPaths) {
if (discoveredCodeownersPaths.length === 0) {
return 'No CODEOWNERS files found in this repository.'
}
const codeownersDir = path.posix.dirname(codeownersPath)
return codeownersDir === '.' ? '' : codeownersDir
const unsupportedPaths = discoveredCodeownersPaths.filter((filePath) => !isSupportedCodeownersFile(filePath))
if (unsupportedPaths.length === discoveredCodeownersPaths.length) {
return [
'No supported CODEOWNERS files found in this repository.',
`GitHub only supports ${SUPPORTED_CODEOWNERS_PATHS_LABEL}.`,
`Unsupported CODEOWNERS files were found at: ${unsupportedPaths.join(', ')}.`,
].join(' ')
}
return 'No CODEOWNERS files found in this repository.'
}

@@ -1241,7 +1339,6 @@

* path: string,
* dir: string,
* rules: {
* pattern: string,
* owners: string[],
* matches: (scopePath: string, repoPath: string) => boolean
* matches: (repoPath: string) => boolean
* }[]

@@ -1251,3 +1348,2 @@ * }}

function loadCodeownersDescriptor (repoRoot, codeownersPath) {
const descriptorDir = resolveCodeownersScopeBase(codeownersPath)
const fileContent = readFileSync(path.join(repoRoot, codeownersPath), 'utf8')

@@ -1258,3 +1354,2 @@ const rules = parseCodeowners(fileContent)

path: codeownersPath,
dir: descriptorDir,
rules,

@@ -1267,3 +1362,3 @@ }

* @param {string} fileContent
* @returns {{ pattern: string, owners: string[], matches: (scopePath: string, repoPath: string) => boolean }[]}
* @returns {{ pattern: string, owners: string[], matches: (repoPath: string) => boolean }[]}
*/

@@ -1338,3 +1433,3 @@ function parseCodeowners (fileContent) {

* @param {string} rawPattern
* @returns {(scopePath: string, repoPath: string) => boolean}
* @returns {(repoPath: string) => boolean}
*/

@@ -1356,7 +1451,7 @@ function createPatternMatcher (rawPattern, options = {}) {

const anchoredRegex = new RegExp(`^${patternSource}${descendantSuffix}$`)
return (scopePath) => anchoredRegex.test(scopePath)
return (repoPath) => anchoredRegex.test(repoPath)
}
const unanchoredRegex = new RegExp(`(?:^|/)${patternSource}${descendantSuffix}$`)
return (scopePath, repoPath) => unanchoredRegex.test(scopePath) || unanchoredRegex.test(repoPath)
return (repoPath) => unanchoredRegex.test(repoPath)
}

@@ -1403,29 +1498,14 @@

/**
* Sort CODEOWNERS files from broadest to narrowest scope.
* @param {{ dir: string, path: string }} first
* @param {{ dir: string, path: string }} second
* @returns {number}
*/
function compareCodeownersDescriptor (first, second) {
const firstDepth = first.dir ? first.dir.split('/').length : 0
const secondDepth = second.dir ? second.dir.split('/').length : 0
if (firstDepth !== secondDepth) return firstDepth - secondDepth
return first.path.localeCompare(second.path)
}
/**
* Build missing-path warnings for CODEOWNERS rules that match no repository files.
* @param {{
* path: string,
* dir: string,
* rules: {
* pattern: string,
* owners: string[],
* matches: (scopePath: string, repoPath: string) => boolean
* matches: (repoPath: string) => boolean
* }[]
* }[]} codeownersDescriptors
* }} codeownersDescriptor
* @param {string[]} repoFiles
* @returns {{
* codeownersPath: string,
* scopedDir: string,
* pattern: string,

@@ -1435,6 +1515,5 @@ * owners: string[]

*/
function collectMissingCodeownersPathWarnings (codeownersDescriptors, repoFiles) {
function collectMissingCodeownersPathWarnings (codeownersDescriptor, repoFiles) {
/** @type {{
* codeownersPath: string,
* scopedDir: string,
* pattern: string,

@@ -1445,23 +1524,10 @@ * owners: string[]

for (const descriptor of codeownersDescriptors) {
const scopedFiles = descriptor.dir
? repoFiles.filter((filePath) => pathIsInside(filePath, descriptor.dir))
: repoFiles
for (const rule of descriptor.rules) {
const hasMatch = scopedFiles.some((filePath) => {
const scopePath = descriptor.dir
? filePath.slice(descriptor.dir.length + 1)
: filePath
return rule.matches(scopePath, filePath)
for (const rule of codeownersDescriptor.rules) {
const hasMatch = repoFiles.some((filePath) => rule.matches(filePath))
if (!hasMatch) {
warnings.push({
codeownersPath: codeownersDescriptor.path,
pattern: rule.pattern,
owners: rule.owners,
})
if (!hasMatch) {
warnings.push({
codeownersPath: descriptor.path,
scopedDir: descriptor.dir || '.',
pattern: rule.pattern,
owners: rule.owners,
})
}
}

@@ -1479,2 +1545,46 @@ }

/**
* Build discovery warnings for extra or unsupported CODEOWNERS files.
* @param {string[]} discoveredCodeownersPaths
* @param {string} activeCodeownersPath
* @returns {{
* path: string,
* type: 'unused-supported-location'|'unsupported-location',
* referencePath?: string,
* message: string
* }[]}
*/
function collectCodeownersDiscoveryWarnings (discoveredCodeownersPaths, activeCodeownersPath) {
/** @type {{
* path: string,
* type: 'unused-supported-location'|'unsupported-location',
* referencePath?: string,
* message: string
* }[]} */
const warnings = []
for (const codeownersPath of discoveredCodeownersPaths) {
if (codeownersPath === activeCodeownersPath) continue
if (isSupportedCodeownersFile(codeownersPath)) {
warnings.push({
path: codeownersPath,
type: 'unused-supported-location',
referencePath: activeCodeownersPath,
message: `${codeownersPath} is unused because GitHub selects ${activeCodeownersPath} first.`,
})
continue
}
warnings.push({
path: codeownersPath,
type: 'unsupported-location',
message: `${codeownersPath} is in an unsupported location and is ignored by GitHub.`,
})
}
warnings.sort((first, second) => first.path.localeCompare(second.path))
return warnings
}
/**
* Build the report payload consumed by the HTML page.

@@ -1485,9 +1595,8 @@ * @param {string} repoRoot

* path: string,
* dir: string,
* rules: {
* pattern: string,
* owners: string[],
* matches: (scopePath: string, repoPath: string) => boolean
* matches: (repoPath: string) => boolean
* }[]
* }[]} codeownersDescriptors
* }} codeownersDescriptor
* @param {{

@@ -1506,3 +1615,3 @@ * includeUntracked: boolean,

* totals: { files: number, owned: number, unowned: number, coverage: number },
* codeownersFiles: { path: string, dir: string, rules: number }[],
* codeownersFiles: { path: string, rules: number }[],
* directories: { path: string, total: number, owned: number, unowned: number, coverage: number }[],

@@ -1512,5 +1621,11 @@ * unownedFiles: string[],

* codeownersValidationMeta: {
* discoveryWarnings: {
* path: string,
* type: 'unused-supported-location'|'unsupported-location',
* referencePath?: string,
* message: string
* }[],
* discoveryWarningCount: number,
* missingPathWarnings: {
* codeownersPath: string,
* scopedDir: string,
* pattern: string,

@@ -1542,3 +1657,3 @@ * owners: string[]

*/
function buildReport (repoRoot, files, codeownersDescriptors, options, progress = () => {}) {
function buildReport (repoRoot, files, codeownersDescriptor, options, progress = () => {}) {
/** @type {Map<string, { total: number, owned: number, unowned: number }>} */

@@ -1556,3 +1671,3 @@ const directoryStats = new Map()

const filePath = files[fileIndex]
const owners = resolveOwners(filePath, codeownersDescriptors)
const owners = resolveOwners(filePath, codeownersDescriptor.rules)
const isOwned = Array.isArray(owners) && owners.length > 0

@@ -1631,9 +1746,6 @@ const teamOwners = collectTeamOwners(owners)

totals,
codeownersFiles: codeownersDescriptors.map((descriptor) => {
return {
path: descriptor.path,
dir: descriptor.dir || '.',
rules: descriptor.rules.length,
}
}),
codeownersFiles: [{
path: codeownersDescriptor.path,
rules: codeownersDescriptor.rules.length,
}],
directories,

@@ -1643,2 +1755,4 @@ unownedFiles,

codeownersValidationMeta: {
discoveryWarnings: [],
discoveryWarningCount: 0,
missingPathWarnings: [],

@@ -1690,52 +1804,25 @@ missingPathWarningCount: 0,

/**
* Resolve matching owners for a file by applying CODEOWNERS files from broad to narrow.
* Resolve matching owners for a file using the active CODEOWNERS rules.
* @param {string} filePath
* @param {{
* dir: string,
* rules: {
* owners: string[],
* matches: (scopePath: string, repoPath: string) => boolean
* }[]
* }[]} codeownersDescriptors
* owners: string[],
* matches: (repoPath: string) => boolean
* }[]} codeownersRules
* @returns {string[]|undefined}
*/
function resolveOwners (filePath, codeownersDescriptors) {
/** @type {string[]|undefined} */
let owners
for (const descriptor of codeownersDescriptors) {
if (descriptor.dir && !pathIsInside(filePath, descriptor.dir)) continue
const scopePath = descriptor.dir ? filePath.slice(descriptor.dir.length + 1) : filePath
const matchedOwners = findMatchingOwners(scopePath, filePath, descriptor.rules)
if (matchedOwners) {
owners = matchedOwners
}
}
return owners
function resolveOwners (filePath, codeownersRules) {
return findMatchingOwners(filePath, codeownersRules)
}
/**
* Check whether filePath is inside dirPath (POSIX relative paths).
* @param {string} filePath
* @param {string} dirPath
* @returns {boolean}
*/
function pathIsInside (filePath, dirPath) {
return filePath === dirPath || filePath.startsWith(`${dirPath}/`)
}
/**
* Find the last matching owners in a ruleset.
* @param {string} scopePath
* @param {string} repoPath
* @param {{ owners: string[], matches: (scopePath: string, repoPath: string) => boolean }[]} rules
* @param {{ owners: string[], matches: (repoPath: string) => boolean }[]} rules
* @returns {string[]|undefined}
*/
function findMatchingOwners (scopePath, repoPath, rules) {
function findMatchingOwners (repoPath, rules) {
/** @type {string[]|undefined} */
let owners
for (const rule of rules) {
if (rule.matches(scopePath, repoPath)) {
if (rule.matches(repoPath)) {
owners = rule.owners

@@ -1814,3 +1901,3 @@ }

* totals: { files: number, owned: number, unowned: number, coverage: number },
* codeownersFiles: { path: string, dir: string, rules: number }[],
* codeownersFiles: { path: string, rules: number }[],
* directories: { path: string, total: number, owned: number, unowned: number, coverage: number }[],

@@ -1820,5 +1907,11 @@ * unownedFiles: string[],

* codeownersValidationMeta: {
* discoveryWarnings: {
* path: string,
* type: 'unused-supported-location'|'unsupported-location',
* referencePath?: string,
* message: string
* }[],
* discoveryWarningCount: number,
* missingPathWarnings: {
* codeownersPath: string,
* scopedDir: string,
* pattern: string,

@@ -1825,0 +1918,0 @@ * owners: string[]

@@ -246,2 +246,13 @@ <!doctype html>

}
.warning-path {
color: #fbbf24;
font-weight: 600;
}
.warning-text {
color: var(--muted);
}
.warning-reference {
color: #67e8f9;
font-weight: 600;
}

@@ -457,3 +468,3 @@ table {

<div class="header">
<h2>Detected CODEOWNERS Files</h2>
<h2>Active CODEOWNERS File</h2>
</div>

@@ -464,3 +475,2 @@ <table>

<th>Path</th>
<th>Scope Base</th>
<th>Rules</th>

@@ -472,6 +482,9 @@ </tr>

<div id="missing-path-warnings" class="validation-warnings" hidden>
<h3>Patterns With No Matching Repository Paths</h3>
<p class="muted" id="missing-path-warnings-summary"></p>
<h3 id="missing-path-warnings-heading">Patterns With No Matching Repository Paths</h3>
<ul id="missing-path-warnings-list"></ul>
</div>
<div id="codeowners-discovery-warnings" class="validation-warnings" hidden>
<h3 id="codeowners-discovery-warnings-heading">CODEOWNERS Location Warnings</h3>
<ul id="codeowners-discovery-warnings-list"></ul>
</div>
</section>

@@ -501,2 +514,4 @@ </div>

const codeownersValidationMeta = report.codeownersValidationMeta || {
discoveryWarnings: [],
discoveryWarningCount: 0,
missingPathWarnings: [],

@@ -555,2 +570,3 @@ missingPathWarningCount: 0,

renderMissingPathWarnings(codeownersValidationMeta)
renderCodeownersDiscoveryWarnings(codeownersValidationMeta)
directoryController = setupDirectoryTable(

@@ -579,8 +595,6 @@ report.directories,

'<td class="path"></td>',
'<td class="path"></td>',
'<td></td>'
].join('')
tr.children[0].textContent = row.path
tr.children[1].textContent = row.dir
tr.children[2].textContent = fmt.format(row.rules)
tr.children[1].textContent = fmt.format(row.rules)
body.appendChild(tr)

@@ -592,3 +606,3 @@ }

const container = document.getElementById('missing-path-warnings')
const summary = document.getElementById('missing-path-warnings-summary')
const heading = document.getElementById('missing-path-warnings-heading')
const list = document.getElementById('missing-path-warnings-list')

@@ -606,9 +620,26 @@ const warningRows = Array.isArray(validationMeta && validationMeta.missingPathWarnings)

const warningCount = Number(validationMeta && validationMeta.missingPathWarningCount) || warningRows.length
summary.textContent = fmt.format(warningCount) +
' CODEOWNERS pattern(s) do not match any repository file in the scanned repository scope.'
heading.textContent = 'Patterns With No Matching Repository Paths (' + fmt.format(warningCount) + ')'
for (const warning of warningRows) {
const item = document.createElement('li')
item.textContent =
warning.pattern + ' (from ' + warning.codeownersPath + ')'
const patternSpan = document.createElement('span')
patternSpan.className = 'warning-path'
patternSpan.textContent = warning.pattern
item.appendChild(patternSpan)
const textSpan = document.createElement('span')
textSpan.className = 'warning-text'
textSpan.textContent = ' (from '
item.appendChild(textSpan)
const sourceSpan = document.createElement('span')
sourceSpan.className = 'warning-reference'
sourceSpan.textContent = warning.codeownersPath
item.appendChild(sourceSpan)
const trailingSpan = document.createElement('span')
trailingSpan.className = 'warning-text'
trailingSpan.textContent = ')'
item.appendChild(trailingSpan)
list.appendChild(item)

@@ -620,2 +651,53 @@ }

function renderCodeownersDiscoveryWarnings (validationMeta) {
const container = document.getElementById('codeowners-discovery-warnings')
const heading = document.getElementById('codeowners-discovery-warnings-heading')
const list = document.getElementById('codeowners-discovery-warnings-list')
const warningRows = Array.isArray(validationMeta && validationMeta.discoveryWarnings)
? validationMeta.discoveryWarnings
: []
list.innerHTML = ''
if (warningRows.length === 0) {
container.hidden = true
return
}
const warningCount = Number(validationMeta && validationMeta.discoveryWarningCount) || warningRows.length
heading.textContent = 'CODEOWNERS Location Warnings (' + fmt.format(warningCount) + ')'
for (const warning of warningRows) {
const item = document.createElement('li')
const pathSpan = document.createElement('span')
pathSpan.className = 'warning-path'
pathSpan.textContent = warning.path
item.appendChild(pathSpan)
const textSpan = document.createElement('span')
textSpan.className = 'warning-text'
if (warning.type === 'unused-supported-location' && warning.referencePath) {
textSpan.textContent = ' is unused because GitHub selects '
item.appendChild(textSpan)
const referenceSpan = document.createElement('span')
referenceSpan.className = 'warning-reference'
referenceSpan.textContent = warning.referencePath
item.appendChild(referenceSpan)
const trailingSpan = document.createElement('span')
trailingSpan.className = 'warning-text'
trailingSpan.textContent = ' first.'
item.appendChild(trailingSpan)
} else {
textSpan.textContent = ' is in an unsupported location and is ignored by GitHub.'
item.appendChild(textSpan)
}
list.appendChild(item)
}
container.hidden = false
}
function setupDirectoryTable (allRows, suggestionLookup, suggestionContext, getScope, onScopeChange, getScopeDirectStats) {

@@ -622,0 +704,0 @@ const body = document.getElementById('directory-table-body')