feat: Add automated MPL-2.0 license header validation (#2995)

improve the suggestion

improve the code
This commit is contained in:
Shubham Kumar
2026-01-15 10:15:27 -06:00
committed by GitHub
parent 56a965e87d
commit 4b1bcb4146
5 changed files with 579 additions and 3 deletions
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env node
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
const fs = require('fs');
const path = require('path');
// Headers to add based on file type
const HEADERS = {
code: `/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
`,
html: `<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
`
};
// File extensions
const FILE_EXTENSIONS = {
code: [
'.ts',
'.js',
'.scss',
'.css'
],
html: ['.html']
};
const EXCLUDE_PATTERNS = [
'node_modules',
'dist',
'coverage',
'.angular',
'e2e',
'cypress',
'playwright',
'.husky',
'setup-jest.ts',
'jest.config.ts',
'cypress.config.ts',
'playwright.config.ts',
'tailwind.config.js',
'proxy.conf.js',
'proxy.localhost.conf.js',
'version.js',
'environment.ts',
'environment.prod.ts',
'polyfills.ts',
'typings.d.ts',
'global.d.ts',
'.spec.ts',
'.spec.js',
'tsconfig'
];
function shouldExclude(filePath) {
const normalizedPath = filePath.replace(/\\/g, '/');
return EXCLUDE_PATTERNS.some((pattern) => {
if (pattern.startsWith('.') && pattern.lastIndexOf('.') > 0) {
return normalizedPath.endsWith(pattern);
}
return normalizedPath.includes(pattern);
});
}
function getHeaderType(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (FILE_EXTENSIONS.html.includes(ext)) {
return 'html';
}
if (FILE_EXTENSIONS.code.includes(ext)) {
return 'code';
}
return null;
}
/**
* Check if file already has a header
*/
function hasHeader(content, headerType) {
if (headerType === 'code') {
return content.includes('Copyright since 2025 Mifos Initiative') && content.includes('Mozilla Public License');
}
if (headerType === 'html') {
return content.includes('Copyright since 2025 Mifos Initiative') && content.includes('Mozilla Public License');
}
return false;
}
/**
* Add header to a file
*/
function addHeaderToFile(filePath, dryRun = false) {
// Skip excluded files
if (shouldExclude(filePath)) {
console.log(`⏭️ Skipping ${filePath} (excluded)`);
return false;
}
const headerType = getHeaderType(filePath);
if (!headerType) {
console.log(`⏭️ Skipping ${filePath} (unsupported file type)`);
return false;
}
let content;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch (error) {
console.error(`❌ Error reading ${filePath}: ${error.message}`);
return false;
}
// Skip if file already has a header
if (hasHeader(content, headerType)) {
console.log(`✓ ${filePath} (already has header)`);
return false;
}
// Skip empty files
if (content.trim().length === 0) {
console.log(`⏭️ Skipping ${filePath} (empty file)`);
return false;
}
const header = HEADERS[headerType];
const newContent = header + content;
if (dryRun) {
console.log(`🔍 Would add header to: ${filePath}`);
return true;
}
try {
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`✅ Added header to: ${filePath}`);
return true;
} catch (error) {
console.error(`❌ Error writing ${filePath}: ${error.message}`);
return false;
}
}
/**
* Main function
*/
function main() {
const args = process.argv.slice(2);
// Parse flags
const dryRun = args.includes('--dry-run') || args.includes('-d');
const files = args.filter((arg) => !arg.startsWith('--') && !arg.startsWith('-'));
if (files.length === 0) {
console.log('Usage: node add-file-headers.js [options] <file1> <file2> ...');
console.log('\nOptions:');
console.log(' --dry-run, -d Show what would be done without making changes');
console.log('\nExample:');
console.log(' node add-file-headers.js --dry-run src/app/app.component.ts');
console.log(' node add-file-headers.js src/app/**/*.ts');
process.exit(0);
}
if (dryRun) {
console.log('🔍 DRY RUN MODE - No files will be modified\n');
}
let modifiedCount = 0;
let skippedCount = 0;
files.forEach((filePath) => {
if (!fs.existsSync(filePath)) {
console.error(`❌ File not found: ${filePath}`);
skippedCount++;
return;
}
const modified = addHeaderToFile(filePath, dryRun);
if (modified) {
modifiedCount++;
} else {
skippedCount++;
}
});
console.log('\n' + '='.repeat(50));
console.log(`Summary: ${modifiedCount} file(s) ${dryRun ? 'would be' : ''} modified, ${skippedCount} skipped`);
if (dryRun && modifiedCount > 0) {
console.log('\n💡 Run without --dry-run to apply changes');
}
}
main();
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env node
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
const fs = require('fs');
const path = require('path');
// Expected header patterns for different file types
const HEADERS = {
mpl: [
'/**',
' * Copyright since 2025 Mifos Initiative',
' *',
' * This Source Code Form is subject to the terms of the Mozilla Public',
' * License, v. 2.0. If a copy of the MPL was not distributed with this',
' * file, You can obtain one at http://mozilla.org/MPL/2.0/.',
' */'
],
html: [
'<!--',
' Copyright since 2025 Mifos Initiative',
'',
' This Source Code Form is subject to the terms of the Mozilla Public',
' License, v. 2.0. If a copy of the MPL was not distributed with this',
' file, You can obtain one at http://mozilla.org/MPL/2.0/.',
'-->'
]
};
// File extensions that should have headers
const FILE_EXTENSIONS = {
code: [
'.ts',
'.js',
'.scss',
'.css'
],
html: ['.html']
};
// Files/patterns to exclude from header checks
const EXCLUDE_PATTERNS = [
'node_modules',
'dist',
'coverage',
'.angular',
'e2e',
'cypress',
'playwright',
'.husky',
'setup-jest.ts',
'jest.config.ts',
'cypress.config.ts',
'playwright.config.ts',
'tailwind.config.js',
'proxy.conf.js',
'proxy.localhost.conf.js',
'version.js',
'environment.ts',
'environment.prod.ts',
'polyfills.ts',
'typings.d.ts',
'global.d.ts',
'.spec.ts',
'.spec.js',
'tsconfig'
];
/**
* Check if a file should be excluded from header validation
*/
function shouldExclude(filePath) {
const normalizedPath = filePath.replace(/\\/g, '/');
return EXCLUDE_PATTERNS.some((pattern) => {
if (pattern.startsWith('.') && pattern.lastIndexOf('.') > 0) {
return normalizedPath.endsWith(pattern);
}
return normalizedPath.includes(pattern);
});
}
/**
* Get the expected header for a file based on its extension
*/
function getExpectedHeader(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (FILE_EXTENSIONS.html.includes(ext)) {
return HEADERS.html;
}
if (FILE_EXTENSIONS.code.includes(ext)) {
return HEADERS.mpl;
}
return null;
}
/**
* Normalize whitespace for comparison
*/
function normalizeWhitespace(str) {
return str.trim().replace(/\s+/g, ' ');
}
/**
* Check if file has the correct header
*/
function hasValidHeader(filePath) {
if (shouldExclude(filePath)) {
return true; // Skip excluded files
}
const expectedHeader = getExpectedHeader(filePath);
if (!expectedHeader) {
return true; // Skip files without defined header requirements
}
let content;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch (error) {
console.error(`Error reading file ${filePath}: ${error.message}`);
return false;
}
// Handle empty files
if (content.trim().length === 0) {
return true; // Allow empty files
}
const lines = content.split(/\r?\n/);
const headerLineCount = expectedHeader.length;
let startLine = 0;
if (lines[0] && lines[0].startsWith('#!')) {
startLine = 1;
if (lines[1] !== undefined && lines[1].trim() === '') {
startLine = 2;
}
}
// Check if file has enough lines
if (lines.length < startLine + headerLineCount) {
return false;
}
// Compare header lines
for (let i = 0; i < headerLineCount; i++) {
const actualLine = normalizeWhitespace(lines[startLine + i]);
const expectedLine = normalizeWhitespace(expectedHeader[i]);
if (actualLine !== expectedLine) {
return false;
}
}
return true;
}
/**
* Format the expected header for display
*/
function formatExpectedHeader(header) {
return '\n' + header.join('\n') + '\n';
}
/**
* Main function
*/
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: node check-file-headers.js <file1> <file2> ...');
process.exit(1);
}
const filesWithoutHeaders = [];
args.forEach((filePath) => {
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
return;
}
if (!hasValidHeader(filePath)) {
filesWithoutHeaders.push(filePath);
}
});
if (filesWithoutHeaders.length > 0) {
console.error('\n❌ Header validation failed!\n');
console.error('The following files are missing the required MPL-2.0 license header:\n');
filesWithoutHeaders.forEach((file) => {
console.error(` - ${file}`);
const expectedHeader = getExpectedHeader(file);
if (expectedHeader) {
console.error(`\n Expected header:${formatExpectedHeader(expectedHeader)}`);
}
});
console.error('\n💡 Please add the MPL-2.0 license header to these files.');
console.error(' Refer to existing files for the correct format.\n');
process.exit(1);
}
console.log('✅ All files have valid headers');
process.exit(0);
}
main();