|
| 1 | +import fs from 'fs'; |
| 2 | +import path from 'path'; |
| 3 | + |
| 4 | +const BASE_URL = 'https://api.github.com'; |
| 5 | +const ORG_NAME = 'torrust'; |
| 6 | +const CONSTANTS_FILE = 'src/lib/constants/constants.ts'; |
| 7 | + |
| 8 | +type Contributor = { |
| 9 | + login: string; |
| 10 | + avatar_url: string; |
| 11 | + html_url: string; |
| 12 | +}; |
| 13 | + |
| 14 | +type Repo = { |
| 15 | + name: string; |
| 16 | +}; |
| 17 | + |
| 18 | +async function fetchWithAuth(url: string): Promise<Response> { |
| 19 | + const headers: HeadersInit = { |
| 20 | + Accept: 'application/vnd.github.v3+json', |
| 21 | + 'User-Agent': 'Torrust-Website-Contributors-Script' |
| 22 | + }; |
| 23 | + |
| 24 | + // Use GITHUB_TOKEN if available for higher rate limits |
| 25 | + const token = process.env.GITHUB_TOKEN; |
| 26 | + if (token) { |
| 27 | + // GitHub supports both formats: 'token xxx' for classic, 'Bearer xxx' for fine-grained |
| 28 | + headers.Authorization = `token ${token}`; |
| 29 | + console.log('✓ Using GitHub token for authentication'); |
| 30 | + } else { |
| 31 | + console.log('Using anonymous GitHub API (rate limit: 60 requests/hour)'); |
| 32 | + console.log(' Set GITHUB_TOKEN environment variable for higher limits'); |
| 33 | + } |
| 34 | + |
| 35 | + return fetch(url, { headers }); |
| 36 | +} |
| 37 | + |
| 38 | +async function fetchRepos(): Promise<string[]> { |
| 39 | + console.log(`\nFetching repositories from ${ORG_NAME} organization...`); |
| 40 | + const response = await fetchWithAuth(`${BASE_URL}/orgs/${ORG_NAME}/repos?per_page=100`); |
| 41 | + |
| 42 | + if (!response.ok) { |
| 43 | + const errorBody = await response.text(); |
| 44 | + console.error(`Response status: ${response.status} ${response.statusText}`); |
| 45 | + console.error(`Error details: ${errorBody}`); |
| 46 | + throw new Error(`Failed to fetch repos: ${response.statusText}`); |
| 47 | + } |
| 48 | + |
| 49 | + const repos: Repo[] = await response.json(); |
| 50 | + console.log(`✓ Found ${repos.length} repositories`); |
| 51 | + |
| 52 | + return repos.map((repo) => repo.name); |
| 53 | +} |
| 54 | + |
| 55 | +async function fetchContributorsForRepo(repoName: string): Promise<Contributor[]> { |
| 56 | + try { |
| 57 | + const response = await fetchWithAuth( |
| 58 | + `${BASE_URL}/repos/${ORG_NAME}/${repoName}/contributors?per_page=100` |
| 59 | + ); |
| 60 | + |
| 61 | + if (!response.ok) { |
| 62 | + console.error(`✗ Failed to fetch contributors for ${repoName}: ${response.statusText}`); |
| 63 | + return []; |
| 64 | + } |
| 65 | + |
| 66 | + // Check if response has content before parsing |
| 67 | + const text = await response.text(); |
| 68 | + if (!text || text.trim().length === 0) { |
| 69 | + console.error(`✗ Empty response for ${repoName}`); |
| 70 | + return []; |
| 71 | + } |
| 72 | + |
| 73 | + try { |
| 74 | + return JSON.parse(text); |
| 75 | + } catch (parseError) { |
| 76 | + console.error(`✗ Invalid JSON for ${repoName}:`, parseError instanceof Error ? parseError.message : parseError); |
| 77 | + return []; |
| 78 | + } |
| 79 | + } catch (error) { |
| 80 | + console.error(`✗ Error fetching contributors for ${repoName}:`, error instanceof Error ? error.message : error); |
| 81 | + return []; |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +async function fetchAllContributors(): Promise<Contributor[]> { |
| 86 | + const repos = await fetchRepos(); |
| 87 | + |
| 88 | + console.log('\nFetching contributors from all repositories...'); |
| 89 | + const contributorPromises = repos.map(async (repo) => { |
| 90 | + const contributors = await fetchContributorsForRepo(repo); |
| 91 | + console.log(` ✓ ${repo}: ${contributors.length} contributors`); |
| 92 | + return contributors; |
| 93 | + }); |
| 94 | + |
| 95 | + const contributorArrays = await Promise.all(contributorPromises); |
| 96 | + const allContributors = contributorArrays.flat(); |
| 97 | + |
| 98 | + // Deduplicate by login |
| 99 | + const uniqueContributors = Array.from( |
| 100 | + new Map(allContributors.map((c) => [c.login, c])).values() |
| 101 | + ); |
| 102 | + |
| 103 | + console.log(`\n✓ Total unique contributors: ${uniqueContributors.length}`); |
| 104 | + return uniqueContributors; |
| 105 | +} |
| 106 | + |
| 107 | +function updateConstantsFile(contributors: Contributor[]): void { |
| 108 | + const filePath = path.resolve(CONSTANTS_FILE); |
| 109 | + |
| 110 | + if (!fs.existsSync(filePath)) { |
| 111 | + throw new Error(`Constants file not found: ${CONSTANTS_FILE}`); |
| 112 | + } |
| 113 | + |
| 114 | + let content = fs.readFileSync(filePath, 'utf-8'); |
| 115 | + |
| 116 | + // Find the defaultContributorsList array |
| 117 | + const startMarker = 'export const defaultContributorsList = ['; |
| 118 | + const startIndex = content.indexOf(startMarker); |
| 119 | + |
| 120 | + if (startIndex === -1) { |
| 121 | + throw new Error('Could not find defaultContributorsList in constants.ts'); |
| 122 | + } |
| 123 | + |
| 124 | + // Find the closing bracket for the array |
| 125 | + let bracketCount = 0; |
| 126 | + let endIndex = startIndex + startMarker.length; |
| 127 | + let inString = false; |
| 128 | + let stringChar = ''; |
| 129 | + |
| 130 | + for (let i = endIndex; i < content.length; i++) { |
| 131 | + const char = content[i]; |
| 132 | + |
| 133 | + if ((char === '"' || char === "'") && content[i - 1] !== '\\') { |
| 134 | + if (!inString) { |
| 135 | + inString = true; |
| 136 | + stringChar = char; |
| 137 | + } else if (char === stringChar) { |
| 138 | + inString = false; |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + if (!inString) { |
| 143 | + if (char === '[') bracketCount++; |
| 144 | + if (char === ']') { |
| 145 | + if (bracketCount === 0) { |
| 146 | + endIndex = i + 2; // Include '];' |
| 147 | + break; |
| 148 | + } |
| 149 | + bracketCount--; |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + // Format contributors list |
| 155 | + const contributorEntries = contributors |
| 156 | + .map( |
| 157 | + (c) => |
| 158 | + `\t{\n\t\thtml_url: '${c.login}',\n\t\tavatar_url: '${c.avatar_url}'\n\t}` |
| 159 | + ) |
| 160 | + .join(',\n'); |
| 161 | + |
| 162 | + const newList = `export const defaultContributorsList = [\n${contributorEntries}\n];`; |
| 163 | + |
| 164 | + // Replace the old list with the new one |
| 165 | + const newContent = content.substring(0, startIndex) + newList + content.substring(endIndex); |
| 166 | + |
| 167 | + fs.writeFileSync(filePath, newContent, 'utf-8'); |
| 168 | + console.log(`\n✅ Updated ${CONSTANTS_FILE}`); |
| 169 | +} |
| 170 | + |
| 171 | +async function main() { |
| 172 | + try { |
| 173 | + console.log('🚀 Updating contributors list...'); |
| 174 | + const contributors = await fetchAllContributors(); |
| 175 | + updateConstantsFile(contributors); |
| 176 | + console.log('✅ Contributors list updated successfully!'); |
| 177 | + } catch (error) { |
| 178 | + console.error('❌ Error updating contributors:', error); |
| 179 | + process.exit(1); |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +main(); |
0 commit comments