Files
awesome-copilot/eng/add-missing-contributors.mjs
Ashley Childress 1322aa2dde feat(eng): add contributor reporting and management scripts
- Add eng/README.md documenting maintainer utilities
- Add eng/contributor-report.mjs for generating contributor reports
- Add eng/add-missing-contributors.mjs for automating contributor additions
- Add eng/utils/graceful-shutdown.mjs for script lifecycle management
- Update eng/update-readme.mjs with minor fixes
- Update package.json with new contributor scripts

Generated-by: GitHub Copilot <copilot@github.com>
Signed-off-by: Ashley Childress <6563688+anchildress1@users.noreply.github.com>
2025-12-19 20:43:27 -05:00

307 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* One-time contributor detection and addition script.
* Discovers missing contributors, determines their contribution types from repo history,
* and updates .all-contributorsrc via the all-contributors CLI.
*
* Usage: node add-missing-contributors.mjs
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import {
getContributionTypes,
getMissingContributors,
fetchContributorMergedPrs
} from './contributor-report.mjs';
import { setupGracefulShutdown } from './utils/graceful-shutdown.mjs';
const DEFAULT_CMD_TIMEOUT = 30_000; // 30 seconds
setupGracefulShutdown('add-missing-contributors');
/**
* Get all files touched by a contributor from their merged PRs.
* @param {string} username
* @returns {string[]}
*/
const getContributorFiles = (username) => {
try {
console.log(`📁 Getting files for contributor: ${username}`);
const prs = fetchContributorMergedPrs(username, { includeAllFiles: true });
if (prs.length === 0) {
console.log(`📭 No merged PRs found for ${username}`);
return [];
}
const files = new Set();
for (const pr of prs) {
for (const file of pr.files || []) {
if (file?.path) {
files.add(file.path);
}
}
}
const fileList = Array.from(files);
console.log(`📄 Found ${fileList.length} unique files for ${username}: ${fileList.slice(0, 3).join(', ')}${fileList.length > 3 ? '...' : ''}`);
return fileList;
} catch (error) {
console.error(`❌ Error getting files for ${username}:`, error.message);
return [];
}
};
/**
* Determine contribution types from a contributor's files.
* @param {string} username
* @returns {string}
*/
const analyzeContributor = (username) => {
try {
console.log(`🔍 Analyzing contribution types for: ${username}`);
const files = getContributorFiles(username);
if (files.length === 0) {
console.log(`💡 No files found for ${username}, using 'code' fallback`);
return 'code';
}
const contributionTypes = getContributionTypes(files);
if (!contributionTypes || contributionTypes.trim() === '') {
console.log(`💡 No matching types found for ${username}, using 'code' fallback`);
return 'code';
}
console.log(`✅ Determined types for ${username}: ${contributionTypes}`);
return contributionTypes;
} catch (error) {
console.error(`❌ Error analyzing files for ${username}:`, error.message);
return 'code';
}
};
/**
* Add a username to the ignore list in .all-contributorsrc.
* @param {string} username
* @returns {boolean}
*/
const addToIgnoreList = (username) => {
try {
const configPath = path.join(process.cwd(), '.all-contributorsrc');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
const ignoreList = config.ignoreList || config.ignore || [];
if (!ignoreList.includes(username)) {
ignoreList.push(username);
config.ignoreList = ignoreList;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
console.warn(`⚠️ Added ${username} to ignore list (user not found on GitHub)`);
return true;
}
return false;
} catch (error) {
console.error(`❌ Failed to add ${username} to ignore list:`, error.message);
return false;
}
};
/**
* Run the all-contributors CLI to add a contributor to the project.
* @param {string} username
* @param {string} types
* @returns {boolean}
*/
const addContributor = (username, types) => {
try {
console.log(` Adding contributor: ${username} with types: ${types}`);
const command = `npx all-contributors add ${username} ${types}`;
execSync(command, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: DEFAULT_CMD_TIMEOUT
});
return true;
} catch (error) {
// System-level errors that should propagate up
if (error.message.includes('rate limit') || error.message.includes('403')) {
console.error(`⏱️ Rate limit encountered while adding ${username}.`);
throw error;
}
if (error.message.includes('network') || error.message.includes('timeout')) {
console.error(`🌐 Network error while adding ${username}.`);
throw error;
}
// User-specific errors that can be skipped
if (error.message.includes('404') || error.message.includes('not found')) {
addToIgnoreList(username);
console.error(`❌ User ${username} not found, added to ignore list`);
return false;
}
// Unknown error - log and skip
console.error(`❌ Failed to add contributor ${username}:`, error.message);
return false;
}
};
/**
* Process a single missing contributor: detect types and add via all-contributors CLI.
* @param {string} username
* @returns {{added:number, failed:number}}
*/
const processContributor = async (username) => {
let added = 0;
let failed = 0;
try {
console.log(`📊 Step 2: Analyzing contribution types for ${username}...`);
const contributionTypes = analyzeContributor(username);
console.log(` Step 3: Adding ${username} with types: ${contributionTypes}...`);
const success = addContributor(username, contributionTypes);
if (success) {
added++;
console.log(`✅ Successfully processed ${username}`);
} else {
failed++;
console.log(`❌ Failed to process ${username}`);
}
} catch (error) {
failed++;
console.error(`💥 Error processing ${username}:`, error.message);
}
return { added, failed };
};
/**
* Main entry point: detect and add missing contributors.
*/
const main = async () => {
console.log('🚀 Starting add missing contributors script');
console.log('='.repeat(50));
try {
console.log('\n📋 Step 1: Detecting missing contributors...');
const missingContributors = getMissingContributors();
if (missingContributors.length === 0) {
console.log('🎉 No missing contributors found! All contributors are properly recognized.');
return { processed: 0, added: 0, failed: 0 };
}
console.log(`\n🔄 Processing ${missingContributors.length} missing contributors...`);
let processed = 0;
let added = 0;
let failed = 0;
for (const username of missingContributors) {
console.log(`\n${'─'.repeat(30)}`);
console.log(`👤 Processing contributor: ${username}`);
processed++;
try {
const { added: deltaAdded, failed: deltaFailed } = await processContributor(username);
added += deltaAdded;
failed += deltaFailed;
} catch (error) {
// Re-throw system-level errors (rate limit, network, SIGINT)
console.error(`💥 System error processing ${username}:`, error.message);
throw error;
}
}
return { processed, added, failed };
} catch (error) {
console.error('\n💥 Fatal error in main execution:', error.message);
console.error('🛑 Script execution stopped');
throw error;
}
};
/**
* Print a summary report of the run.
* @param {{processed:number, added:number, failed:number}} results
*/
const printSummaryReport = (results) => {
const { processed, added, failed } = results;
console.log('\n' + '='.repeat(50));
console.log('📊 EXECUTION SUMMARY');
console.log('='.repeat(50));
console.log(`📋 Total contributors processed: ${processed}`);
console.log(`✅ Successfully added: ${added}`);
console.log(`❌ Failed to add: ${failed}`);
if (processed === 0) {
console.log('\n🎉 SUCCESS: No missing contributors found - all contributors are properly recognized!');
} else if (failed === 0) {
console.log('\n🎉 SUCCESS: All missing contributors have been successfully added!');
console.log('💡 Next steps: Review the updated .all-contributorsrc file and commit the changes.');
} else if (added > 0) {
console.log('\n⚠ PARTIAL SUCCESS: Some contributors were added, but some failed.');
console.log(`💡 ${added} contributors were successfully added.`);
console.log(`🔄 ${failed} contributors failed - check the error messages above for details.`);
console.log('💡 You may want to run the script again to retry failed contributors.');
} else {
console.log('\n❌ FAILURE: No contributors could be added.');
console.log('💡 Check the error messages above for troubleshooting guidance.');
console.log('💡 Common issues: missing GITHUB_TOKEN, network problems, or API rate limits.');
}
console.log('\n📝 ACTIONABLE NEXT STEPS:');
if (added > 0) {
console.log('• Review the updated .all-contributorsrc file');
console.log('• Commit and push the changes to update the README');
console.log('• Consider running "npm run contributors:generate" to update the README');
}
if (failed > 0) {
console.log('• Check error messages above for specific failure reasons');
console.log('• Verify GITHUB_TOKEN is set and has appropriate permissions');
console.log('• Consider running the script again after resolving issues');
}
if (processed === 0) {
console.log('• No action needed - all contributors are already recognized!');
}
console.log('\n' + '='.repeat(50));
};
if (process.argv[1] === (new URL(import.meta.url)).pathname) {
try {
const results = await main();
printSummaryReport(results);
if (results.failed > 0 && results.added === 0) {
process.exit(1);
} else if (results.failed > 0) {
process.exit(2);
} else {
process.exit(0);
}
} catch (error) {
console.error('\n💥 Script execution failed:', error.message);
console.log('\n📝 TROUBLESHOOTING TIPS:');
console.log('• Ensure you are in a git repository');
console.log('• Verify all-contributors-cli is installed');
console.log('• Check that .all-contributorsrc file exists');
console.log('• Ensure GITHUB_TOKEN environment variable is set');
process.exit(1);
}
}