Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
57e855c
feat: add Community Activity Score (Issue #138)
shravanithouta108 Jun 13, 2026
b117d67
fix: sync with upstream main, keep our changes
shravanithouta108 Jun 16, 2026
c90740c
fix: resolve ORGS undefined error and duplicate filteredOrgs declaration
shravanithouta108 Jun 20, 2026
5661fcd
chore: reset community_activity.json to empty object
shravanithouta108 Jul 10, 2026
b71c066
fix: include bare GitHub owners and preserve null metrics
shravanithouta108 Jul 10, 2026
3b2fea3
fix: resolve safeHTML/rawHTML ReferenceError, duplicate communityActi…
shravanithouta108 Jul 11, 2026
d6f2053
fix: updated github-action bot issue assignment workflow (#1792)
imayuss Jun 17, 2026
592becc
Fix: mariadb mentor contact issue 346 clean (#898)
aasthakhatri11 Jun 17, 2026
ea9d77a
chore: update mentor leaderboard (#1839)
github-actions[bot] Jun 17, 2026
b9ac12e
docs: backfill contributors (#1896)
github-actions[bot] Jun 17, 2026
a5bf07b
fix: add OpenWISP mentors, channels and mailing lists (#1061)
shreyjoshi73 Jun 17, 2026
a98546b
chore(deps-dev): bump js-yaml from 4.1.1 to 4.2.0 (#1898)
dependabot[bot] Jun 17, 2026
857d105
chore: update mentor leaderboard (#1897)
github-actions[bot] Jun 17, 2026
8303254
chore: update mentor leaderboard (#1899)
github-actions[bot] Jun 17, 2026
86260a1
feat: add live word and character counter to proposal builder (#1851)
Soquixx Jun 19, 2026
a723c90
fix: add stdlib mentor contact info to mentors.json (#1927)
amrita-a-menon Jun 21, 2026
3ff83c1
Update gssoc-mentors.json (#1924)
KumarNirupam1 Jun 21, 2026
4064ccc
fix: align privacy.html navbar and footer with homepage (#1879)
Manav5234 Jun 21, 2026
3dc8d60
update gssoc-mentor username (#1942)
KumarNirupam1 Jun 23, 2026
2987bf4
ci: fix dual-label PR classification for GSSoC+NSoC conflict (#1359) …
NayansiDupare Jun 25, 2026
37324f4
feat: added landing page for better navigation (#1854)
arpit2006 Jun 26, 2026
d1a993c
fix: update Stichting SU2 mentor contacts (#1956)
shrutisd739 Jun 27, 2026
0ad46eb
fix: add program classification template and graceful fallback valida…
Nilamma19 Jun 27, 2026
de55ca3
fix(data): verify Genome Assembly and Annotation mentor contact info …
Deepakvarna02 Jun 27, 2026
385afba
docs: backfill contributors (#1973)
github-actions[bot] Jun 28, 2026
e3a2c71
fix: add flex-shrink-0 to org card logo container to prevent squishin…
amrita-a-menon Jun 30, 2026
96103e0
fix: update routing links in landing.html for Vercel rewrite rules (#…
arghya29 Jun 30, 2026
0033b18
chore: update mentor leaderboard (#1900)
github-actions[bot] Jun 30, 2026
7b7692e
docs: fix alt text for flowchart image in README (#1424) (#1873)
Shan7Usmani Jul 2, 2026
f125f66
fix: switch vercel.json from rewrites to routes to fix '/' routing pr…
arghya29 Jul 4, 2026
dc116d5
chore: update mentor leaderboard (#1980)
github-actions[bot] Jul 4, 2026
cb6063f
fix: verify and update mentor contact info for GNU Radio (#309) (#1991)
Manav5234 Jul 4, 2026
ff1e033
fix: suppress lint unused-var warnings and add null guards
shravanithouta108 Jul 12, 2026
bbd3ae4
fix: move filteredOrgs to global comment to fix lint
shravanithouta108 Jul 12, 2026
dba796a
fix: resolve merge conflicts and remove package-lock.json
shravanithouta108 Jul 12, 2026
3c45e75
fix: add ORGS global declaration to landing.js for lint
shravanithouta108 Jul 12, 2026
b59eb52
fix: declare filteredOrgs in app.js and add ORGS global to landing.js…
shravanithouta108 Jul 12, 2026
364ffb5
fix: restore package-lock.json from upstream for lint workflow
shravanithouta108 Jul 12, 2026
059bcdb
fix: move ORGS global directive before JSDoc block in landing.js
shravanithouta108 Jul 12, 2026
c169b41
fix: add initial sync renderOrgs call to fix DOM smoke test
shravanithouta108 Jul 12, 2026
fabaa23
fix: initialize filteredOrgs from ORGS before renderOrgs to fix DOM s…
shravanithouta108 Jul 12, 2026
ce054a4
fix: remove renderMentorFinder stub, add README docs, use create-pull…
shravanithouta108 Jul 13, 2026
c6e96c0
fix: add renderMentorFinder global, pin peter-evans SHA, fix XSS esca…
shravanithouta108 Jul 13, 2026
bc13aa7
fix: guard renderMentorFinder calls with typeof check for test enviro…
shravanithouta108 Jul 13, 2026
a567f98
fix: align README weights/tiers with code, return null for orgs with …
shravanithouta108 Jul 18, 2026
dbc8582
fix: remove unreachable null check after computeScore
shravanithouta108 Jul 18, 2026
01a6ce7
chore: add initial community activity scores from GitHub API
shravanithouta108 Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/compute-activity-scores.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Compute Community Activity Scores
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight
workflow_dispatch: # Manual trigger
jobs:
compute-scores:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.2.0
with:
node-version: '18'
- name: Run activity score computation
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node agent/scripts/compute-activity-scores.js
- name: Commit updated scores
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b2b3e36ac28eb95 # v7.0.8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: update community activity scores"
title: "chore: update community activity scores"
body: "Automated daily update of community activity scores."
branch: "auto/update-community-activity-scores"
delete-branch: true
add-paths: data/community_activity.json
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,27 @@ The Edge Function proxies GitHub API calls so your token never hits the client.
| `GET /api/github?repo=owner/repo&gfi=1` | Good First Issue count only (faster, cached separately) |
| `GET /api/github?repo=owner/repo&gfi=1&issues=1` | Full list of up to 30 open Good First Issues |

---
## πŸ† Community Activity Score

Each organization is scored daily (0–100) based on GitHub activity signals fetched by `.github/workflows/compute-activity-scores.yml`:

| Signal | Weight | Description |
|--------|--------|-------------|
| Issue resolution time | 30% | Average days to close issues |
| Commit frequency | 20% | Commits in last 90 days |
| PR merge rate | 15% | % of PRs merged in 90 days |
| Repository freshness | 20% | Days since last push |
| Stars score | 15% | Scaled stargazers count |

**Tiers:**
- 🟒 **Very Active** β€” score 75–100
- πŸ”΅ **Active** β€” score 50–74
- 🟑 **Moderate** β€” score 25–49
- πŸ”΄ **Low Activity** β€” score 0–24

Scores are stored in `data/community_activity.json` and updated daily. Organizations with no GitHub data are excluded from scoring.

All responses are cached in-memory for **1 hour** on the Edge runtime.
# πŸš€ Official Open Source Program Project

Expand Down
159 changes: 159 additions & 0 deletions agent/scripts/compute-activity-scores.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// @ts-check
/* eslint-env node */
// Computes community activity scores for all GSoC orgs
// Run via GitHub Actions daily

const fs = require('node:fs');
const path = require('node:path');
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const OUTPUT_FILE = path.join(__dirname, '../../data/community_activity.json');

if (!GITHUB_TOKEN) {
console.error('❌ GITHUB_TOKEN is required');
process.exit(1);
}

const headers = {
'Authorization': `token ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'gsoc-org-finder'
};

// Load orgs from org.js
const orgDataRaw = fs.readFileSync(
path.join(__dirname, '../../src/js/org.js'), 'utf8'
);

// Extract github field from each org
const githubRepos = [];
const orgMatches = orgDataRaw.matchAll(/name:\s*"([^"]+)"[^}]+github:\s*"([^"]+)"/g);
for (const m of orgMatches) {
const name = m[1];
const repo = m[2];
if (repo && repo.includes('/')) {

Check warning on line 33 in agent/scripts/compute-activity-scores.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ9Pwk1OfVNoVDTzL1p5&open=AZ9Pwk1OfVNoVDTzL1p5&pullRequest=2008
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
githubRepos.push({ name, repo });
} else {
console.warn(`Skipping bare owner (no repo path): ${name} β†’ "${repo}"`);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function fetchJSON(url) {
const res = await fetch(url, { headers });
if (!res.ok) return null;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return res.json();
}

function computeScore({ issueResponseDays, commitFrequency, prMergeRate, ideasFreshnessDays, starsGrowth }) {
const issueScore = Math.max(0, 100 - (issueResponseDays * 5));
const commitScore = Math.min(100, commitFrequency * 10);
const prScore = Math.min(100, prMergeRate * 100);
const ideasScore = Math.max(0, 100 - (ideasFreshnessDays * 0.5));
const growthScore = Math.min(100, starsGrowth * 2);

return Math.round(
issueScore * 0.3 +
commitScore * 0.2 +
prScore * 0.15 +
ideasScore * 0.2 +
growthScore * 0.15
);
}

function getTier(score) {
if (score >= 80) return { tier: 'very-active', label: '🟒 Very Active' };
if (score >= 60) return { tier: 'active', label: 'πŸ”΅ Active' };
if (score >= 40) return { tier: 'moderate', label: '🟑 Moderate' };
return { tier: 'low', label: 'πŸ”΄ Low Activity' };
}

async function analyzeOrg({ name, repo }) {
try {
const since = new Date(Date.now() - 90 * 86400000).toISOString();

const commits = await fetchJSON(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The GitHub API calls use per_page=100 but never follow pagination (Link headers). For repos with more than 100 commits, issues, or PRs in the 90-day window, the data is silently truncated. This systematically under-estimates scores for high-traffic repos like apache/spark, blender/blender, jenkinsci/jenkins, and NixOS/nixpkgs, which can each have hundreds of commits per quarter. The computed scores will be less accurate for the most active organizations β€” the exact ones the activity score is meant to rank.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At agent/scripts/compute-activity-scores.js, line 71:

<comment>The GitHub API calls use `per_page=100` but never follow pagination (`Link` headers). For repos with more than 100 commits, issues, or PRs in the 90-day window, the data is silently truncated. This systematically under-estimates scores for high-traffic repos like `apache/spark`, `blender/blender`, `jenkinsci/jenkins`, and `NixOS/nixpkgs`, which can each have hundreds of commits per quarter. The computed scores will be less accurate for the most active organizations β€” the exact ones the activity score is meant to rank.</comment>

<file context>
@@ -0,0 +1,155 @@
+  try {
+    const since = new Date(Date.now() - 90 * 86400000).toISOString();
+
+    const commits = await fetchJSON(
+      `https://api.github.com/repos/${repo}/commits?since=${since}&per_page=100`
+    );
</file context>

`https://api.github.com/repos/${repo}/commits?since=${since}&per_page=100`
);
const commitFrequency = Array.isArray(commits) ? commits.length / 90 : 0;
Comment on lines +73 to +76

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ—„οΈ Data Integrity & Integration | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby helpers.
git ls-files agent/scripts/compute-activity-scores.js
wc -l agent/scripts/compute-activity-scores.js
sed -n '1,220p' agent/scripts/compute-activity-scores.js

# Search for any pagination/counting helpers used elsewhere.
rg -n "Link:|rel=\"next\"|per_page=100|fetchJSON|countItemsSince|commits\\?since" agent/scripts

Repository: S3DFX-CYBER/GSoC-Org-Finder-

Length of output: 6102


Commit-frequency signal undercounts active repos

This only reads the first 100 commits from the 90-day window, so repos with more activity get the same commitFrequency as repos with exactly 100. Follow the Link: rel="next" pages (or otherwise count all results) before turning this into a score.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/scripts/compute-activity-scores.js` around lines 71 - 74, Update the
commit retrieval logic in the function containing fetchJSON and commitFrequency
to paginate through GitHub’s Link rel="next" responses and accumulate commits
from every page in the 90-day window. Compute commitFrequency from the total
collected commits rather than the first page’s length, while preserving the
existing fallback for invalid responses.


const issues = await fetchJSON(
`https://api.github.com/repos/${repo}/issues?state=closed&since=${since}&per_page=100`
);
let issueResponseDays = null;
if (Array.isArray(issues) && issues.length > 0) {
const responseTimes = issues
.filter(i => i.created_at && i.closed_at && !i.pull_request)
.map(i => (+new Date(i.closed_at) - +new Date(i.created_at)) / 86400000);
if (responseTimes.length > 0) {
issueResponseDays = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length;
}
}

// Fetch PRs
const prs = await fetchJSON(
`https://api.github.com/repos/${repo}/pulls?state=closed&per_page=100`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: PR Merge Rate becomes a partial, creation-ordered sample for busy repositories, not the stated 90-day rate. Fetch all relevant pages or query/sort so every PR closed in the window contributes before calculating the ratio.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At agent/scripts/compute-activity-scores.js, line 91:

<comment>PR Merge Rate becomes a partial, creation-ordered sample for busy repositories, not the stated 90-day rate. Fetch all relevant pages or query/sort so every PR closed in the window contributes before calculating the ratio.</comment>

<file context>
@@ -0,0 +1,155 @@
+
+    // Fetch PRs
+    const prs = await fetchJSON(
+      `https://api.github.com/repos/${repo}/pulls?state=closed&per_page=100`
+    );
+    let prMergeRate = null;
</file context>

);
let prMergeRate = null;
if (Array.isArray(prs) && prs.length > 0) {
const recentPrs = prs.filter(p => p.closed_at && new Date(p.closed_at) >= new Date(since));
if (recentPrs.length > 0) {
const merged = recentPrs.filter(p => p.merged_at).length;
prMergeRate = merged / recentPrs.length;
}
}

const repoData = await fetchJSON(`https://api.github.com/repos/${repo}`);
const starsGrowth = repoData ? Math.min(50, repoData.stargazers_count / 1000) : 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The score rewards accumulated repository popularity, not star growth: an old, inactive project retains this score contribution even with zero new stars. Persist a prior star count/timestamp and calculate a period delta, or rename/remove this signal if total stars is intended.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At agent/scripts/compute-activity-scores.js, line 103:

<comment>The score rewards accumulated repository popularity, not star growth: an old, inactive project retains this score contribution even with zero new stars. Persist a prior star count/timestamp and calculate a period delta, or rename/remove this signal if total stars is intended.</comment>

<file context>
@@ -0,0 +1,155 @@
+    }
+
+    const repoData = await fetchJSON(`https://api.github.com/repos/${repo}`);
+    const starsGrowth = repoData ? Math.min(50, repoData.stargazers_count / 1000) : 0;
+
+    const ideasFreshnessDays = repoData?.pushed_at
</file context>


const ideasFreshnessDays = repoData?.pushed_at
? (Date.now() - new Date(repoData.pushed_at)) / 86400000
: 60;

if (issueResponseDays === null || prMergeRate === null) return null;

const score = computeScore({
issueResponseDays: issueResponseDays,
commitFrequency,
prMergeRate: prMergeRate,
ideasFreshnessDays,
starsGrowth
});
const { tier, label } = getTier(score);

return {
score,
tier,
label,
signals: {
issueResponseDays: issueResponseDays === null ? null : Math.round(issueResponseDays),
commitFrequency: Math.round(commitFrequency * 10) / 10,
prMergeRate: prMergeRate === null ? null : Math.round(prMergeRate * 100),
ideasFreshnessDays: Math.round(ideasFreshnessDays),
starsGrowth: Math.round(starsGrowth * 10) / 10
},
lastUpdated: new Date().toISOString()
};
} catch (e) {
console.error(`Error analyzing ${name}:`, e.message);
return null;
}
}

async function main() {
console.log(`Analyzing ${githubRepos.length} orgs...`);
const results = {};

for (const org of githubRepos) {
console.log(`Processing: ${org.name}`);
const data = await analyzeOrg(org);
if (data) results[org.name] = data;
await new Promise(r => setTimeout(r, 500));
}

fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2));
console.log(`βœ… Done! Wrote ${Object.keys(results).length} orgs to community_activity.json`);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading
Loading