Skip to content

Commit 85cf73d

Browse files
ericyangpanclaude
andcommitted
feat(github-stars): centralize GitHub stars data management with automated updates
Introduces a new architecture for managing GitHub stars data: - Creates centralized github-stars.json to store all star counts - Adds GitHub Actions workflow for daily automated updates via cron - Refactors fetch-github-stars.mjs to populate centralized storage - Generates TypeScript helper functions for accessing star data by category and ID This separation allows GitHub stars to be updated independently from product manifests, enabling automated daily updates without modifying individual manifest files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 56a6362 commit 85cf73d

File tree

5 files changed

+312
-15
lines changed

5 files changed

+312
-15
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Update GitHub Stars
2+
3+
on:
4+
schedule:
5+
# Run daily at midnight UTC
6+
- cron: '0 0 * * *'
7+
workflow_dispatch:
8+
# Allow manual triggering
9+
10+
jobs:
11+
update-stars:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
with:
18+
token: ${{ secrets.GITHUB_TOKEN }}
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: '20'
24+
cache: 'npm'
25+
26+
- name: Install dependencies
27+
run: npm ci
28+
29+
- name: Fetch GitHub stars
30+
run: npm run fetch:github-stars
31+
env:
32+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
34+
- name: Generate TypeScript files
35+
run: npm run generate:manifests
36+
37+
- name: Check for changes
38+
id: git-check
39+
run: |
40+
git diff --exit-code manifests/github-stars.json src/lib/generated/github-stars.ts || echo "changes=true" >> $GITHUB_OUTPUT
41+
42+
- name: Create Pull Request
43+
if: steps.git-check.outputs.changes == 'true'
44+
uses: peter-evans/create-pull-request@v5
45+
with:
46+
commit-message: 'chore: update GitHub stars data'
47+
title: 'chore: update GitHub stars data'
48+
body: |
49+
## Summary
50+
Automated daily update of GitHub stars data.
51+
52+
### Changes
53+
- Updated `manifests/github-stars.json` with latest star counts
54+
- Regenerated `src/lib/generated/github-stars.ts`
55+
56+
### Source
57+
This PR was automatically generated by the `update-github-stars` workflow.
58+
branch: auto-update-github-stars
59+
delete-branch: true
60+
labels: |
61+
automated
62+
dependencies

manifests/github-stars.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"extensions": {
3+
"amp": null,
4+
"augment-code": null,
5+
"claude-code": 42,
6+
"cline": 52.3,
7+
"continue": 29.8,
8+
"github-copilot": null,
9+
"jetbrains-junie": null,
10+
"kilo-code": 12.2,
11+
"roo-code": 20.7,
12+
"tabnine": 10.8
13+
},
14+
"clis": {
15+
"amazon-q-developer-cli": 1.8,
16+
"amp-cli": null,
17+
"augment-code-cli": 0.1,
18+
"claude-code": 42,
19+
"cline-cli": null,
20+
"codebuddy-cli": null,
21+
"codex": 50.2,
22+
"continue-cli": 29.8,
23+
"droid-cli": null,
24+
"gemini-cli": 82.1,
25+
"github-copilot-cli": 5,
26+
"kimi-cli": 2.6,
27+
"kilocode-cli": 12.2,
28+
"kode": 3.4,
29+
"neovate-code": 1,
30+
"opencode": 32.4,
31+
"qoder-cli": null
32+
},
33+
"ides": {
34+
"codebuddy": null,
35+
"codeflicker": null,
36+
"cursor": 31.6,
37+
"droid": null,
38+
"intellij-idea": 19.1,
39+
"kiro": 2.2,
40+
"qoder": null,
41+
"trae": null,
42+
"vscode": 178.4,
43+
"windsurf": null,
44+
"zed": 69.5
45+
}
46+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "https://aicodingstack.io/schemas/github-stars.schema.json",
4+
"title": "GitHub Stars Data",
5+
"description": "Schema for the centralized GitHub stars data file",
6+
"type": "object",
7+
"properties": {
8+
"extensions": {
9+
"type": "object",
10+
"description": "GitHub stars for extensions",
11+
"patternProperties": {
12+
"^[a-z0-9-]+$": {
13+
"oneOf": [
14+
{
15+
"type": "number",
16+
"minimum": 0,
17+
"description": "Number of GitHub stars in thousands (e.g., 42 = 42k stars)"
18+
},
19+
{
20+
"type": "null",
21+
"description": "No GitHub stars data available"
22+
}
23+
]
24+
}
25+
},
26+
"additionalProperties": false
27+
},
28+
"clis": {
29+
"type": "object",
30+
"description": "GitHub stars for CLI tools",
31+
"patternProperties": {
32+
"^[a-z0-9-]+$": {
33+
"oneOf": [
34+
{
35+
"type": "number",
36+
"minimum": 0,
37+
"description": "Number of GitHub stars in thousands (e.g., 42 = 42k stars)"
38+
},
39+
{
40+
"type": "null",
41+
"description": "No GitHub stars data available"
42+
}
43+
]
44+
}
45+
},
46+
"additionalProperties": false
47+
},
48+
"ides": {
49+
"type": "object",
50+
"description": "GitHub stars for IDEs",
51+
"patternProperties": {
52+
"^[a-z0-9-]+$": {
53+
"oneOf": [
54+
{
55+
"type": "number",
56+
"minimum": 0,
57+
"description": "Number of GitHub stars in thousands (e.g., 42 = 42k stars)"
58+
},
59+
{
60+
"type": "null",
61+
"description": "No GitHub stars data available"
62+
}
63+
]
64+
}
65+
},
66+
"additionalProperties": false
67+
}
68+
},
69+
"required": ["extensions", "clis", "ides"],
70+
"additionalProperties": false
71+
}

scripts/fetch-github-stars.mjs

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,26 @@ const __dirname = dirname(__filename);
1212
// Set via environment variable: GITHUB_TOKEN=your_token_here node fetch-github-stars.mjs
1313
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
1414

15+
// Path to the centralized GitHub stars data file
16+
const GITHUB_STARS_FILE = path.join(__dirname, '..', 'manifests', 'github-stars.json');
17+
1518
// Directories configuration - now pointing to individual file directories
1619
const dirsConfig = [
1720
{
1821
directory: 'manifests/extensions',
22+
category: 'extensions',
1923
githubUrlField: 'communityUrls.github',
2024
type: 'nested'
2125
},
2226
{
2327
directory: 'manifests/ides',
28+
category: 'ides',
2429
githubUrlField: 'communityUrls.github',
2530
type: 'nested'
2631
},
2732
{
2833
directory: 'manifests/clis',
34+
category: 'clis',
2935
githubUrlField: 'communityUrls.github',
3036
type: 'nested'
3137
}
@@ -110,79 +116,82 @@ function sleep(ms) {
110116
}
111117

112118
// Process a single JSON file
113-
async function processFile(filePath, fileName, type) {
119+
async function processFile(filePath, fileName, type, starsData) {
114120
const content = fs.readFileSync(filePath, 'utf8');
115121
const item = JSON.parse(content);
116122

117123
const githubUrl = getGithubUrl(item, type);
118124

119125
if (!githubUrl) {
120126
console.log(` ⏭️ Skipping ${item.name || item.id} (no GitHub URL)`);
121-
return { updated: false, skipped: true, error: false };
127+
return { updated: false, skipped: true, error: false, id: item.id, stars: null };
122128
}
123129

124130
const parsed = parseGithubUrl(githubUrl);
125131
if (!parsed) {
126132
console.log(` ❌ Failed to parse GitHub URL for ${item.name || item.id}: ${githubUrl}`);
127-
return { updated: false, skipped: false, error: true };
133+
return { updated: false, skipped: false, error: true, id: item.id, stars: null };
128134
}
129135

130136
try {
131137
console.log(` 🔍 Fetching stars for ${item.name || item.id} (${parsed.owner}/${parsed.repo})...`);
132138
const stars = await fetchStars(parsed.owner, parsed.repo);
133-
item.githubStars = stars;
134-
135-
// Write back to file
136-
fs.writeFileSync(filePath, JSON.stringify(item, null, 2) + '\n', 'utf8');
137139
console.log(` ✅ Updated ${item.name || item.id}: ${stars}k stars`);
138140

139141
// Sleep for 1 second to avoid rate limiting
140142
await sleep(1000);
141-
return { updated: true, skipped: false, error: false };
143+
return { updated: true, skipped: false, error: false, id: item.id, stars };
142144
} catch (error) {
143145
console.log(` ❌ Error fetching ${item.name || item.id}: ${error.message}`);
144-
return { updated: false, skipped: false, error: true };
146+
return { updated: false, skipped: false, error: true, id: item.id, stars: null };
145147
}
146148
}
147149

148150
// Process all files in a directory
149-
async function processDirectory(dirConfig) {
151+
async function processDirectory(dirConfig, starsData) {
150152
const dirPath = path.join(__dirname, '..', dirConfig.directory);
151153
console.log(`\n📁 Processing ${dirConfig.directory}...`);
152154

153155
if (!fs.existsSync(dirPath)) {
154156
console.log(` ⚠️ Directory not found: ${dirPath}`);
155-
return;
157+
return { categoryData: {}, stats: { updated: 0, skipped: 0, errors: 0 } };
156158
}
157159

158160
// Get all JSON files in the directory
159161
const files = fs.readdirSync(dirPath).filter(file => file.endsWith('.json'));
160162

161163
if (files.length === 0) {
162164
console.log(` ⚠️ No JSON files found in ${dirConfig.directory}`);
163-
return;
165+
return { categoryData: {}, stats: { updated: 0, skipped: 0, errors: 0 } };
164166
}
165167

166168
let updated = 0;
167169
let skipped = 0;
168170
let errors = 0;
171+
const categoryData = {};
169172

170173
for (const file of files) {
171174
const filePath = path.join(dirPath, file);
172-
const result = await processFile(filePath, file, dirConfig.type);
175+
const result = await processFile(filePath, file, dirConfig.type, starsData);
173176

174177
if (result.updated) updated++;
175178
if (result.skipped) skipped++;
176179
if (result.error) errors++;
180+
181+
// Store the stars data for this item
182+
if (result.id) {
183+
categoryData[result.id] = result.stars;
184+
}
177185
}
178186

179187
console.log(`\n✨ ${dirConfig.directory} completed: ${updated} updated, ${skipped} skipped, ${errors} errors`);
188+
return { categoryData, stats: { updated, skipped, errors } };
180189
}
181190

182191
// Main function
183192
async function main() {
184193
console.log('🚀 Starting GitHub stars fetcher...\n');
185-
console.log('📝 Note: Now processing individual JSON files in directories\n');
194+
console.log('📝 Note: Updating centralized github-stars.json file\n');
186195

187196
if (!GITHUB_TOKEN) {
188197
console.log('⚠️ Warning: No GITHUB_TOKEN set. You may hit rate limits (60 requests/hour).');
@@ -191,16 +200,59 @@ async function main() {
191200
console.log('✅ Using GitHub token for authentication\n');
192201
}
193202

203+
// Load existing stars data or create new structure
204+
let starsData = { extensions: {}, clis: {}, ides: {} };
205+
if (fs.existsSync(GITHUB_STARS_FILE)) {
206+
try {
207+
const content = fs.readFileSync(GITHUB_STARS_FILE, 'utf8');
208+
starsData = JSON.parse(content);
209+
console.log('📂 Loaded existing github-stars.json\n');
210+
} catch (error) {
211+
console.log('⚠️ Failed to parse existing github-stars.json, creating new one\n');
212+
}
213+
}
214+
215+
let totalUpdated = 0;
216+
let totalSkipped = 0;
217+
let totalErrors = 0;
218+
219+
// Process each directory and collect stars data
194220
for (const dirConfig of dirsConfig) {
195221
try {
196-
await processDirectory(dirConfig);
222+
const { categoryData, stats } = await processDirectory(dirConfig, starsData);
223+
224+
// Sort the category data by key (alphabetically)
225+
const sortedCategoryData = Object.keys(categoryData)
226+
.sort()
227+
.reduce((acc, key) => {
228+
acc[key] = categoryData[key];
229+
return acc;
230+
}, {});
231+
232+
// Update the stars data for this category
233+
starsData[dirConfig.category] = sortedCategoryData;
234+
235+
totalUpdated += stats.updated;
236+
totalSkipped += stats.skipped;
237+
totalErrors += stats.errors;
197238
} catch (error) {
198239
console.error(`❌ Failed to process ${dirConfig.directory}:`, error.message);
240+
totalErrors++;
199241
}
200242
}
201243

244+
// Write the updated stars data to file
245+
try {
246+
fs.writeFileSync(GITHUB_STARS_FILE, JSON.stringify(starsData, null, 2) + '\n', 'utf8');
247+
console.log('\n📝 Successfully updated manifests/github-stars.json');
248+
} catch (error) {
249+
console.error('\n❌ Failed to write github-stars.json:', error.message);
250+
process.exit(1);
251+
}
252+
202253
console.log('\n' + '='.repeat(50));
203254
console.log('🎉 All directories processed!');
255+
console.log(`📊 Total: ${totalUpdated} updated, ${totalSkipped} skipped, ${totalErrors} errors`);
204256
console.log('='.repeat(50));
205257
}
206258

0 commit comments

Comments
 (0)