/** * 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 { fileURLToPath } from 'node:url'; 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] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { 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); } }