|
| 1 | +name: Build Repositories Database |
| 2 | + |
| 3 | +on: |
| 4 | + schedule: |
| 5 | + - cron: '0 0 * * 0' # Runs every Sunday at midnight UTC |
| 6 | + workflow_dispatch: |
| 7 | + |
| 8 | +permissions: |
| 9 | + contents: write |
| 10 | + |
| 11 | +jobs: |
| 12 | + build-json: |
| 13 | + runs-on: ubuntu-latest |
| 14 | + |
| 15 | + steps: |
| 16 | + - name: Checkout main repository |
| 17 | + uses: actions/checkout@v4 |
| 18 | + |
| 19 | + - name: Setup Node.js |
| 20 | + uses: actions/setup-node@v4 |
| 21 | + with: |
| 22 | + node-version: '20' |
| 23 | + |
| 24 | + - name: Fetch data and build db.json |
| 25 | + uses: actions/github-script@v7 |
| 26 | + with: |
| 27 | + script: | |
| 28 | + const fs = require('fs'); |
| 29 | +
|
| 30 | + // Utility to handle API rate limits via delay |
| 31 | + const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); |
| 32 | +
|
| 33 | + async function generateDatabase() { |
| 34 | + const username = 'Pro-bandey'; |
| 35 | +
|
| 36 | + // Repositories to exclude |
| 37 | + const excludedRepos = [ |
| 38 | + 'pro-bandey', |
| 39 | + 'pro-bandey.github.io', |
| 40 | + '.github' |
| 41 | + ].map(repo => repo.toLowerCase().trim()); |
| 42 | +
|
| 43 | + console.log(`Fetching repositories for ${username}...`); |
| 44 | +
|
| 45 | + // Fetch all public repositories |
| 46 | + const reposResponse = await github.rest.repos.listForUser({ |
| 47 | + username: username, |
| 48 | + type: 'public', |
| 49 | + per_page: 100 |
| 50 | + }); |
| 51 | +
|
| 52 | + // Filter repositories BEFORE processing |
| 53 | + const repositories = reposResponse.data.filter(repo => { |
| 54 | + const repoName = repo.name.toLowerCase().trim(); |
| 55 | +
|
| 56 | + // Skip private repositories |
| 57 | + if (repo.private) { |
| 58 | + console.log(`Skipping private repository: ${repo.name}`); |
| 59 | + return false; |
| 60 | + } |
| 61 | +
|
| 62 | + // Skip excluded repositories |
| 63 | + if (excludedRepos.includes(repoName)) { |
| 64 | + console.log(`Skipping excluded repository: ${repo.name}`); |
| 65 | + return false; |
| 66 | + } |
| 67 | +
|
| 68 | + return true; |
| 69 | + }); |
| 70 | +
|
| 71 | + console.log( |
| 72 | + 'Repositories after filtering:', |
| 73 | + repositories.map(r => r.name) |
| 74 | + ); |
| 75 | +
|
| 76 | + const db = []; |
| 77 | + const repoDetails = {}; |
| 78 | +
|
| 79 | + try { |
| 80 | + console.log('Fetching repository GraphQL details (batched)...'); |
| 81 | + |
| 82 | + // Batch GraphQL query to avoid secondary rate limits |
| 83 | + const batchSize = 10; |
| 84 | + for (let i = 0; i < repositories.length; i += batchSize) { |
| 85 | + const batch = repositories.slice(i, i + batchSize); |
| 86 | + |
| 87 | + // Construct dynamic aliases for the batched query |
| 88 | + let queryFragments = batch.map((repo, idx) => ` |
| 89 | + repo_${idx}: repository(owner: "${username}", name: "${repo.name}") { |
| 90 | + name |
| 91 | + openGraphImageUrl |
| 92 | + defaultBranchRef { |
| 93 | + target { |
| 94 | + ... on Commit { |
| 95 | + history { |
| 96 | + totalCount |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + `).join('\n'); |
| 103 | + |
| 104 | + const query = `query { ${queryFragments} }`; |
| 105 | + |
| 106 | + const result = await github.graphql(query); |
| 107 | + |
| 108 | + // Extract results back to our map |
| 109 | + Object.values(result).forEach(node => { |
| 110 | + if (node && node.name) { |
| 111 | + repoDetails[node.name] = { |
| 112 | + ogImage: node.openGraphImageUrl, |
| 113 | + commits: node.defaultBranchRef?.target?.history?.totalCount || 0 |
| 114 | + }; |
| 115 | + } |
| 116 | + }); |
| 117 | + |
| 118 | + // Sleep between batches to prevent secondary rate limiting |
| 119 | + console.log(`Batch fetched. Sleeping for 1000ms...`); |
| 120 | + await sleep(1000); |
| 121 | + } |
| 122 | + } catch (err) { |
| 123 | + console.warn( |
| 124 | + 'Could not fetch GraphQL details:', |
| 125 | + err.message |
| 126 | + ); |
| 127 | + } |
| 128 | +
|
| 129 | + // Process repositories sequentially with deliberate pacing |
| 130 | + for (const repo of repositories) { |
| 131 | + console.log(`Processing: ${repo.name}`); |
| 132 | +
|
| 133 | + // Fetch languages |
| 134 | + let languagesData = {}; |
| 135 | + let languageBytes = {}; |
| 136 | +
|
| 137 | + try { |
| 138 | + const langResponse = await github.rest.repos.listLanguages({ |
| 139 | + owner: username, |
| 140 | + repo: repo.name |
| 141 | + }); |
| 142 | +
|
| 143 | + const languages = langResponse.data; |
| 144 | +
|
| 145 | + const totalBytes = Object.values(languages).reduce( |
| 146 | + (a, b) => a + b, |
| 147 | + 0 |
| 148 | + ); |
| 149 | +
|
| 150 | + if (totalBytes > 0) { |
| 151 | + for (const [lang, bytes] of Object.entries(languages)) { |
| 152 | + languagesData[lang] = parseFloat( |
| 153 | + ((bytes / totalBytes) * 100).toFixed(1) |
| 154 | + ); |
| 155 | + languageBytes[lang] = bytes; |
| 156 | + } |
| 157 | + } |
| 158 | + } catch (err) { |
| 159 | + console.warn( |
| 160 | + `Could not fetch languages for ${repo.name}:`, |
| 161 | + err.message |
| 162 | + ); |
| 163 | + } |
| 164 | +
|
| 165 | + // Check README existence |
| 166 | + let readmeExists = false; |
| 167 | +
|
| 168 | + try { |
| 169 | + await github.rest.repos.getReadme({ |
| 170 | + owner: username, |
| 171 | + repo: repo.name |
| 172 | + }); |
| 173 | +
|
| 174 | + readmeExists = true; |
| 175 | + } catch (err) { |
| 176 | + readmeExists = false; |
| 177 | + } |
| 178 | +
|
| 179 | + const details = repoDetails[repo.name] || {}; |
| 180 | +
|
| 181 | + // Add repository to database |
| 182 | + db.push({ |
| 183 | + Repo: repo.name, |
| 184 | + Date: repo.created_at, |
| 185 | + UpdatedAt: repo.pushed_at || repo.updated_at, |
| 186 | + Status: 'Pub', |
| 187 | + Desc: repo.description || 'No description provided.', |
| 188 | + Langs: languagesData, |
| 189 | + LangBytes: languageBytes, |
| 190 | + PreviewUrl: repo.homepage || '', |
| 191 | + ReadMeIs: readmeExists, |
| 192 | + Banner: details.ogImage || '', |
| 193 | + Commits: details.commits || 0, |
| 194 | + Stars: repo.stargazers_count || 0, |
| 195 | + Forks: repo.forks_count || 0 |
| 196 | + }); |
| 197 | +
|
| 198 | + // Crucial: Sleep 1.5s between REST API requests to respect GitHub Secondary Rate Limits |
| 199 | + await sleep(1500); |
| 200 | + } |
| 201 | +
|
| 202 | + // Save db.json |
| 203 | + fs.writeFileSync('db.json', JSON.stringify(db, null, 2)); |
| 204 | +
|
| 205 | + console.log( |
| 206 | + `Successfully created db.json with ${db.length} records.` |
| 207 | + ); |
| 208 | + } |
| 209 | +
|
| 210 | + await generateDatabase(); |
| 211 | +
|
| 212 | + - name: Deploy db.json to db branch |
| 213 | + run: | |
| 214 | + git config --global user.name "github-actions[bot]" |
| 215 | + git config --global user.email "github-actions[bot]@users.noreply.github.com" |
| 216 | +
|
| 217 | + # Save generated file temporarily |
| 218 | + mv db.json /tmp/db.json |
| 219 | +
|
| 220 | + # Fetch db branch if it exists |
| 221 | + git fetch origin db || true |
| 222 | +
|
| 223 | + # Checkout db branch or create it |
| 224 | + git checkout db || git checkout --orphan db |
| 225 | +
|
| 226 | + # Remove old files |
| 227 | + find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + |
| 228 | +
|
| 229 | + # Restore db.json |
| 230 | + mv /tmp/db.json db.json |
| 231 | +
|
| 232 | + # Commit changes |
| 233 | + git add db.json |
| 234 | + git commit -m "Automated update of repository database" || echo "No changes to commit" |
| 235 | +
|
| 236 | + # Force push db branch |
| 237 | + git push origin db --force |
0 commit comments