diff --git a/.gitignore b/.gitignore index 66d6a95..a09f934 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ src/assets/data dist tests/config.txt configBackup.json +.env diff --git a/AGENTS.md b/AGENTS.md index bd9967f..3450db8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,9 @@ The rank column and some aggregate stats are rendered with invisible/white text. ```bash git clone && cd Opensource-Contribution-Leaderboard cp src/server/config-example.json src/server/config.json -# Edit config.json with your GitHub token + org + contributors +cp src/server/.env.example src/server/.env +# Edit .env with your GitHub token, org, admin password, etc. +# Edit config.json with your contributors list npm run add npm run build cd dist/server @@ -72,26 +74,36 @@ npm run serve # backend API server ``` Frontend: http://localhost:8080 -Backend port: whatever `serverPort` is set to in config.json (default 62050) +Backend port: whatever `SERVER_PORT` is set to in .env (default 62050) ## Config Reference +Static/environment settings live in `.env` (copy from `src/server/.env.example`): + +```bash +AUTH_TOKEN=ghp_YOUR_GITHUB_TOKEN # required — GitHub personal access token +ORGANIZATION=YourOrg # required — GitHub org name +ORGANIZATION_HOMEPAGE=https://yourorg.com/ +ORGANIZATION_GITHUB_URL=https://github.com/YourOrg +ADMIN_PASSWORD=change-this # required — admin panel password +SERVER_PORT=62050 # backend API port +``` + +Dynamic/runtime values stay in `config.json` (modifiable via admin panel): + ```json { - "organization": "YourOrg", - "organizationHomepage": "https://yourorg.com/", - "organizationGithubUrl": "https://github.com/YourOrg", - "authToken": "ghp_YOUR_GITHUB_TOKEN", - "adminPassword": "change-this", "delay": "10", - "serverPort": "62050", - "contributors": ["user1", "user2"] + "startDate": "2025-06-01", + "contributors": ["user1", "user2"], + "includedRepositories": ["Repo1", "Repo2"] } ``` -- `authToken` — required. GitHub personal access token with repo read access. - `delay` — seconds between API calls per contributor (respect rate limits). -- `contributors` — array of GitHub usernames to track. Add users here even before their first contribution. +- `startDate` — filter contributions from this date onwards. +- `contributors` — array of GitHub usernames to track. +- `includedRepositories` — repos to include in contribution tracking. ## Rules for Agents diff --git a/GEMINI.md b/GEMINI.md index bcfdf13..a22303b 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -42,24 +42,34 @@ npm start # webpack-dev-server on :8080 npm run serve # backend on :62050 (in a second terminal) ``` -## Config File (src/server/config.json) +## Config + +Static/environment settings live in `.env` (copy from `src/server/.env.example`): + +```bash +AUTH_TOKEN=ghp_... # GitHub personal access token +ORGANIZATION=YourOrg # GitHub org name +ORGANIZATION_HOMEPAGE=https://yourorg.com/ +ORGANIZATION_GITHUB_URL=https://github.com/YourOrg +ADMIN_PASSWORD=change-this # admin panel password +SERVER_PORT=62050 # backend API port +``` + +Dynamic/runtime values stay in `config.json` (modifiable via admin panel): ```json { - "organization": "YourOrg", - "organizationHomepage": "https://yourorg.com/", - "organizationGithubUrl": "https://github.com/YourOrg", - "authToken": "ghp_...", - "adminPassword": "change-this", "delay": "10", - "serverPort": "62050", - "contributors": ["username1", "username2"] + "startDate": "2025-06-01", + "contributors": ["username1", "username2"], + "includedRepositories": ["Repo1", "Repo2"] } ``` -- `authToken` — GitHub personal access token (needed for API rate limits) - `delay` — seconds between each contributor's API poll +- `startDate` — filter contributions from this date onwards - `contributors` — GitHub usernames to track +- `includedRepositories` — repos to include in tracking ## Design Decisions You Must Respect diff --git a/README.md b/README.md index 01101a5..1977f45 100644 --- a/README.md +++ b/README.md @@ -23,20 +23,28 @@ cd Opensource-Contribution-Leaderboard # 2. Create your config cp src/server/config-example.json src/server/config.json +cp src/server/.env.example src/server/.env ``` -Edit `src/server/config.json`: +Edit `src/server/.env` with your static settings: + +```bash +AUTH_TOKEN=ghp_YOUR_GITHUB_TOKEN +ORGANIZATION=YourOrg +ORGANIZATION_HOMEPAGE=https://yourorg.com/ +ORGANIZATION_GITHUB_URL=https://github.com/YourOrg +ADMIN_PASSWORD=pick-something-better +SERVER_PORT=62050 +``` + +Edit `src/server/config.json` with dynamic/runtime values: ```json { - "organization": "YourOrg", - "organizationHomepage": "https://yourorg.com/", - "organizationGithubUrl": "https://github.com/YourOrg", - "authToken": "ghp_YOUR_GITHUB_TOKEN", - "adminPassword": "pick-something-better", "delay": "10", - "serverPort": "62050", - "contributors": ["contributor1", "contributor2", "contributor3"] + "startDate": "2025-06-01", + "contributors": ["contributor1", "contributor2"], + "includedRepositories": ["Repo1", "Repo2"] } ``` @@ -56,14 +64,25 @@ Open **http://localhost:8080** — you're done. ## Config Reference +Static settings live in `.env`: + +| Env Variable | What it is | +|---|---| +| `AUTH_TOKEN` | GitHub personal access token (repo read access) | +| `ORGANIZATION` | Your GitHub org name | +| `ORGANIZATION_HOMEPAGE` | Org homepage URL | +| `ORGANIZATION_GITHUB_URL` | Org GitHub URL | +| `ADMIN_PASSWORD` | Password for the admin panel | +| `SERVER_PORT` | Internal backend port (default 62050) | + +Dynamic settings live in `config.json` (modifiable via admin panel): + | Key | What it is | |---|---| -| `organization` | Your GitHub org name | -| `authToken` | GitHub personal access token (repo read access) | -| `adminPassword` | Password for the admin panel | | `delay` | Seconds between API calls per contributor (respect rate limits) | -| `serverPort` | Internal backend port (default 62050) | +| `startDate` | Filter contributions from this date onwards | | `contributors` | Array of GitHub usernames to track | +| `includedRepositories` | Repos to include in contribution tracking | ## Local Development diff --git a/src/server/.env.example b/src/server/.env.example new file mode 100644 index 0000000..1076108 --- /dev/null +++ b/src/server/.env.example @@ -0,0 +1,31 @@ +# Leaderboard .env configuration +# Copy this file to .env and fill in your values. +# Static/environment-specific settings live here. +# Dynamic values (contributors, delay, startDate, includedRepositories) +# remain in config.json since they are modified at runtime via the admin panel. + +# GitHub personal access token (repo read access) — REQUIRED +AUTH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx + +# GitHub organization name — REQUIRED +ORGANIZATION=RocketChat + +# Organization homepage URL +ORGANIZATION_HOMEPAGE=https://rocket.chat/ + +# Organization GitHub URL +ORGANIZATION_GITHUB_URL=https://github.com/RocketChat + +# Admin panel password — REQUIRED +ADMIN_PASSWORD=change-this + +# Server port for the backend API +SERVER_PORT=62050 + +# Path overrides (relative to src/server/) +# CONFIG_PATH=./config.json +# ADMINDATA_PATH=./admindata.json +# DATA_PATH=../assets/data/data.json +# LOG_PATH=../assets/data/log.json +# DATA_BASE_PATH=../assets/data +# CONFIG_BACKUP_PATH=../../configBackup.json diff --git a/src/server/app.js b/src/server/app.js index d62b63b..666970f 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -1,3 +1,4 @@ +require('dotenv').config() const http = require('http') const jsonfile = require('jsonfile') const url = require('url') @@ -10,12 +11,16 @@ const app = express() const proxy = require('http-proxy-middleware') const path = require('path') -const configPath = './config.json' -const admindataPath = './admindata.json' -const dataPath = '../assets/data/data.json' -const logPath = '../assets/data/log.json' -const port = jsonfile.readFileSync(configPath).serverPort -const configBackupPath = '../../configBackup.json' +const configPath = process.env.CONFIG_PATH || './config.json' +const admindataPath = process.env.ADMINDATA_PATH || './admindata.json' +const dataPath = process.env.DATA_PATH || '../assets/data/data.json' +const logPath = process.env.LOG_PATH || '../assets/data/log.json' +const port = process.env.SERVER_PORT || 62050 +const configBackupPath = process.env.CONFIG_BACKUP_PATH || '../../configBackup.json' +const organization = process.env.ORGANIZATION +const organizationHomepage = process.env.ORGANIZATION_HOMEPAGE +const organizationGithubUrl = process.env.ORGANIZATION_GITHUB_URL +const adminPassword = process.env.ADMIN_PASSWORD const proxyOption = { target: 'http://localhost:' + port + '/', pathRewrite: { '^/api': '' }, @@ -66,7 +71,6 @@ process.on('exit', () => { const server = http .createServer((req, res) => { const route = url.parse(req.url).pathname - const { adminPassword } = jsonfile.readFileSync(configPath) switch (route) { case '/data': @@ -84,12 +88,11 @@ const server = http }) break case '/config': - var Config = jsonfile.readFileSync(configPath) res.end( JSON.stringify({ - organization: Config.organization, - organizationHomepage: Config.organizationHomepage, - organizationGithubUrl: Config.organizationGithubUrl, + organization: organization, + organizationHomepage: organizationHomepage, + organizationGithubUrl: organizationGithubUrl, }) ) break @@ -154,7 +157,7 @@ const server = http res.end('Permission denied\n') return } - var { organization, includedRepositories } = jsonfile.readFileSync( + var { includedRepositories } = jsonfile.readFileSync( configPath ) API.getRepositories(organization).then((repositories) => { @@ -297,9 +300,10 @@ const server = http // Add this contributor in the data.json const data = jsonfile.readFileSync(dataPath) API.getContributorInfo( - Config.organization, + organization, username, - Config.includedRepositories + Config.includedRepositories, + Config.startDate ).then((result) => { if ( result.avatarUrl !== '' && diff --git a/src/server/config-example.json b/src/server/config-example.json index 37848ac..0937fa2 100644 --- a/src/server/config-example.json +++ b/src/server/config-example.json @@ -1,11 +1,5 @@ { - "organization": "RocketChat", - "organizationHomepage": "https://rocket.chat/", - "organizationGithubUrl": "https://github.com/RocketChat", - "authToken": "", - "adminPassword": "123456", "delay": "10", - "serverPort": "62050", "contributors": [ "HusseinElFeky", "rodriq", diff --git a/src/server/package.json b/src/server/package.json index bb1d320..da53764 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -3,6 +3,7 @@ "axios": "^0.18.0", "bluebird": "^3.5.4", "chalk": "^2.4.2", + "dotenv": "^16.6.1", "express": "^4.16.4", "http-proxy-middleware": "^0.19.1", "jsonfile": "^5.0.0", diff --git a/src/server/refresh.js b/src/server/refresh.js index b4d470b..59f6c43 100644 --- a/src/server/refresh.js +++ b/src/server/refresh.js @@ -1,12 +1,13 @@ +require('dotenv').config() const Promise = require('bluebird') const API = require('./util/API') const jsonfile = require('jsonfile') const fs = require('fs') -const dataBasePath = '../assets/data' -const dataPath = '../assets/data/data.json' -const logPath = '../assets/data/log.json' -const configPath = './config.json' +const dataBasePath = process.env.DATA_BASE_PATH || '../assets/data' +const dataPath = process.env.DATA_PATH || '../assets/data/data.json' +const logPath = process.env.LOG_PATH || '../assets/data/log.json' +const configPath = process.env.CONFIG_PATH || './config.json' let interval = 150 let dataBuffer = {} @@ -27,9 +28,9 @@ if (fs.existsSync(logPath)) { async function getAllContributorsInfo() { let Config = jsonfile.readFileSync(configPath) - let organization = Config.organization let contributors = Config.contributors let includedRepositories = Config.includedRepositories + let startDate = Config.startDate interval = contributors.length < 150 ? 150 : (contributors.length + 10) // update interval @@ -40,7 +41,7 @@ async function getAllContributorsInfo() { await Promise.delay(delay * 1000) - API.getContributorInfo(organization, contributor, includedRepositories).then( res => { + API.getContributorInfo(process.env.ORGANIZATION, contributor, includedRepositories, startDate).then( res => { Config = jsonfile.readFileSync(configPath) // update Config delay = Config.delay // update delay diff --git a/src/server/util/API.js b/src/server/util/API.js index 5e82e9a..71b3c8d 100644 --- a/src/server/util/API.js +++ b/src/server/util/API.js @@ -1,5 +1,4 @@ const axios = require('axios') -const Config = require('../config.json') const chalk = require('chalk') const BASEURL = 'https://github.com' @@ -11,7 +10,7 @@ async function get(url, _authToken) { headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'GSoC-Contribution-Leaderboard', - Authorization: 'token ' + Config.authToken, + Authorization: 'token ' + process.env.AUTH_TOKEN, }, }) return new Promise((resolve) => { @@ -32,7 +31,7 @@ async function get(url, _authToken) { case 'Bad credentials': console.log( chalk.red( - '[ERROR] Your GitHub Token is not correct! Please check it in the config.json.' + '[ERROR] Your GitHub Token is not correct! Please check the AUTH_TOKEN env variable.' ) ) process.exit() @@ -128,16 +127,17 @@ async function getIssuesNumber(IssuesURL) { async function getContributorInfo( organization, contributor, - includedRepositories + includedRepositories, + startDate ) { const home = BASEURL + '/' + contributor const avatarUrl = await getContributorAvatar(contributor) - let OpenPRsURL = `/search/issues?q=is:pr+author:${contributor}+is:Open+created:>=${Config.startDate}` - let openPRsLink = `${BASEURL}/search?q=type:pr+author:${contributor}+is:open+created:>=${Config.startDate}` - let MergedPRsURL = `/search/issues?q=is:pr+author:${contributor}+is:Merged+created:>=${Config.startDate}` - let mergedPRsLink = `${BASEURL}/search?q=type:pr+author:${contributor}+is:merged+created:>=${Config.startDate}` - let IssuesURL = `/search/issues?q=is:issue+author:${contributor}+created:>=${Config.startDate}` - let issuesLink = `${BASEURL}/search?q=type:issue+author:${contributor}+created:>=${Config.startDate}` + let OpenPRsURL = `/search/issues?q=is:pr+author:${contributor}+is:Open+created:>=${startDate}` + let openPRsLink = `${BASEURL}/search?q=type:pr+author:${contributor}+is:open+created:>=${startDate}` + let MergedPRsURL = `/search/issues?q=is:pr+author:${contributor}+is:Merged+created:>=${startDate}` + let mergedPRsLink = `${BASEURL}/search?q=type:pr+author:${contributor}+is:merged+created:>=${startDate}` + let IssuesURL = `/search/issues?q=is:issue+author:${contributor}+created:>=${startDate}` + let issuesLink = `${BASEURL}/search?q=type:issue+author:${contributor}+created:>=${startDate}` includedRepositories.forEach((repository) => { openPRsLink += `+repo:${organization}/${repository}` mergedPRsLink += `+repo:${organization}/${repository}`