diff --git a/aio/scripts/verify-guide-codeownership.js b/aio/scripts/verify-guide-codeownership.js new file mode 100644 index 0000000000..1632072970 --- /dev/null +++ b/aio/scripts/verify-guide-codeownership.js @@ -0,0 +1,87 @@ +'use strict'; + +// Imports +const fs = require('fs'); +const path = require('path'); + +// Constants +const PROJECT_ROOT_DIR = path.resolve(__dirname, '../..'); +const CODEOWNERS_PATH = path.resolve(PROJECT_ROOT_DIR, '.github/CODEOWNERS'); +const AIO_CONTENT_DIR = path.resolve(PROJECT_ROOT_DIR, 'aio/content'); +const AIO_GUIDES_DIR = path.resolve(AIO_CONTENT_DIR, 'guide'); +const AIO_GUIDE_IMAGES_DIR = path.resolve(AIO_CONTENT_DIR, 'images/guide'); +const AIO_GUIDE_EXAMPLES_DIR = path.resolve(AIO_CONTENT_DIR, 'examples'); + +// Run +_main(); + +// Functions - Definitions +function _main() { + const {guides: acGuidePaths, images: acGuideImagesPaths, examples: acExamplePaths} = getPathsFromAioContent(); + const {guides: coGuidePaths, images: coGuideImagesPaths, examples: coExamplePaths} = getPathsFromCodeowners(); + + const guidesDiff = arrayDiff(acGuidePaths, coGuidePaths); + const imagesDiff = arrayDiff(acGuideImagesPaths, coGuideImagesPaths); + const examplesDiff = arrayDiff(acExamplePaths, coExamplePaths); + const hasDiff = !!(guidesDiff.diffCount || imagesDiff.diffCount || examplesDiff.diffCount); + + if (hasDiff) { + const expectedGuidesSrc = path.relative(PROJECT_ROOT_DIR, AIO_GUIDES_DIR); + const expectedImagesSrc = path.relative(PROJECT_ROOT_DIR, AIO_GUIDE_IMAGES_DIR); + const expectedExamplesSrc = path.relative(PROJECT_ROOT_DIR, AIO_GUIDE_EXAMPLES_DIR); + const actualSrc = path.relative(PROJECT_ROOT_DIR, CODEOWNERS_PATH); + + reportDiff(guidesDiff, expectedGuidesSrc, actualSrc); + reportDiff(imagesDiff, expectedImagesSrc, actualSrc); + reportDiff(examplesDiff, expectedExamplesSrc, actualSrc); + } + + process.exit(hasDiff ? 1 : 0); +} + +function arrayDiff(expected, actual) { + const missing = expected.filter(x => !actual.includes(x)).sort(); + const extra = actual.filter(x => !expected.includes(x)).sort(); + + return {missing, extra, diffCount: missing.length + extra.length}; +} + +function getPathsFromAioContent() { + return { + guides: fs.readdirSync(AIO_GUIDES_DIR), + images: fs.readdirSync(AIO_GUIDE_IMAGES_DIR), + examples: fs.readdirSync(AIO_GUIDE_EXAMPLES_DIR). + filter(name => fs.statSync(`${AIO_GUIDE_EXAMPLES_DIR}/${name}`).isDirectory()), + }; +} + +function getPathsFromCodeowners() { + const guidesOrImagesPathRe = /^\/aio\/content\/(?:(images\/)?guide|(examples))\/([^\s/]+)/; + + return fs. + readFileSync(CODEOWNERS_PATH, 'utf8'). + split('\n'). + map(l => l.trim().match(guidesOrImagesPathRe)). + filter(m => m). + reduce((aggr, [, isImage, isExample, path]) => { + const list = isExample ? aggr.examples : + isImage ? aggr.images : + aggr.guides; + list.push(path); + return aggr; + }, {guides: [], images: [], examples: []}); +} + +function reportDiff(diff, expectedSrc, actualSrc) { + if (diff.missing.length) { + console.error( + `\nEntries in '${expectedSrc}' but not in '${actualSrc}':\n` + + diff.missing.map(x => ` - ${x}`).join('\n')); + } + + if (diff.extra.length) { + console.error( + `\nEntries in '${actualSrc}' but not in '${expectedSrc}':\n` + + diff.extra.map(x => ` - ${x}`).join('\n')); + } +}