Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"start": "npm run build && NODE_ENV=development webpack-dev-server --host 0.0.0.0 --open --hot --config build/webpack.config.js",
"build": "npm --prefix admin run build && NODE_ENV=production webpack --config build/webpack.config.js",
"serve": "cd src/server && NODE_ENV=development node app.js",
"lint": "eslint ."
"lint": "eslint .",
"test": "node --test \"test/**/*.test.js\""
},
"husky": {
"hooks": {
Expand Down
131 changes: 82 additions & 49 deletions src/server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,55 @@ const app = express()
const proxy = require('http-proxy-middleware')
const path = require('path')

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
function resolvePathFromEnv(primaryEnvName, secondaryEnvName, defaultRelativeToCwd) {
if (process.env[primaryEnvName]) {
return path.resolve(process.env[primaryEnvName])
}
if (process.env[secondaryEnvName]) {
return path.resolve(process.env[secondaryEnvName])
}
return path.resolve(process.cwd(), defaultRelativeToCwd)
}

const configPath = resolvePathFromEnv('LEADERBOARD_CONFIG_PATH', 'CONFIG_PATH', 'config.json')
const admindataPath = resolvePathFromEnv('LEADERBOARD_ADMINDATA_PATH', 'ADMINDATA_PATH', 'admindata.json')
const dataPath = resolvePathFromEnv('LEADERBOARD_DATA_PATH', 'DATA_PATH', '../assets/data/data.json')
const logPath = resolvePathFromEnv('LEADERBOARD_LOG_PATH', 'LOG_PATH', '../assets/data/log.json')
const configBackupPath = resolvePathFromEnv(
'LEADERBOARD_CONFIG_BACKUP_PATH',
'CONFIG_BACKUP_PATH',
'../../configBackup.json'
)

function getConfig() {
return jsonfile.readFileSync(configPath)
}

function getAdminPassword() {
return process.env.ADMIN_PASSWORD || getConfig().adminPassword
}

function getOrganizationConfig() {
const config = getConfig()
return {
organization: process.env.ORGANIZATION || config.organization,
organizationHomepage: process.env.ORGANIZATION_HOMEPAGE || config.organizationHomepage,
organizationGithubUrl: process.env.ORGANIZATION_GITHUB_URL || config.organizationGithubUrl,
}
}

const httpPort =
process.env.LEADERBOARD_PORT ||
process.env.SERVER_PORT ||
String(getConfig().serverPort || 62050)

const proxyOption = {
target: 'http://localhost:' + port + '/',
target: 'http://localhost:' + httpPort + '/',
pathRewrite: { '^/api': '' },
changeOrigin: true,
}
const websocketProxyOption = {
target: 'http://localhost:' + port + '/',
target: 'http://localhost:' + httpPort + '/',
changeOrigin: true,
}

Expand Down Expand Up @@ -59,18 +91,19 @@ if (!fs.existsSync(admindataPath)) {
jsonfile.writeFileSync(admindataPath, [])
}

// spawn - `node refresh.js`
const refresh = spawn('node', ['refresh.js'], {
shell: true,
stdio: 'inherit',
})
process.on('exit', () => {
refresh.kill() // kill it when exit
})
if (process.env.LEADERBOARD_SKIP_REFRESH !== '1') {
const refresh = spawn('node', ['refresh.js'], {
shell: true,
stdio: 'inherit',
})
process.on('exit', () => {
refresh.kill()
})
}

const server = http
.createServer((req, res) => {
const server = http.createServer((req, res) => {
const route = url.parse(req.url).pathname
const adminPassword = getAdminPassword()

switch (route) {
case '/data':
Expand All @@ -88,11 +121,12 @@ const server = http
})
break
case '/config':
var Config = getOrganizationConfig()
res.end(
JSON.stringify({
organization: organization,
organizationHomepage: organizationHomepage,
organizationGithubUrl: organizationGithubUrl,
organization: Config.organization,
organizationHomepage: Config.organizationHomepage,
organizationGithubUrl: Config.organizationGithubUrl,
})
)
break
Expand All @@ -102,17 +136,15 @@ const server = http
return
}

var { delay, contributors, startDate } = jsonfile.readFileSync(
configPath
)
var { delay, contributors, startDate } = getConfig()
var contributorsList = []

Util.post(req, async (params) => {
const { token } = params
if (token === adminPassword) {
await Promise.all(
contributors.map(async (contributor) => {
const admindata = jsonfile.readFileSync('./admindata.json')
const admindata = jsonfile.readFileSync(admindataPath)
const existContributor = findContributor(
contributor,
admindata
Expand Down Expand Up @@ -157,10 +189,8 @@ const server = http
res.end('Permission denied\n')
return
}
var { includedRepositories } = jsonfile.readFileSync(
configPath
)
API.getRepositories(organization).then((repositories) => {
var { includedRepositories } = getConfig()
API.getRepositories(getOrganizationConfig().organization).then((repositories) => {
if (repositories !== '') {
res.end(
JSON.stringify({
Expand All @@ -186,7 +216,7 @@ const server = http
res.end(JSON.stringify({ message: 'Authentication failed' }))
} else {
// set includedRepositories in config.json
const Config = jsonfile.readFileSync(configPath)
const Config = getConfig()
Config.includedRepositories = includedRepositories
jsonfile.writeFileSync(configPath, Config, { spaces: 2 })
jsonfile.writeFileSync(configBackupPath, Config, { spaces: 2 })
Expand All @@ -207,7 +237,7 @@ const server = http
res.end(JSON.stringify({ message: 'Authentication failed' }))
} else {
// set startDate in config.json
const Config = jsonfile.readFileSync(configPath)
const Config = getConfig()
Config.startDate = startDate
jsonfile.writeFileSync(configPath, Config, { spaces: 2 })
jsonfile.writeFileSync(configBackupPath, Config, { spaces: 2 })
Expand All @@ -229,7 +259,7 @@ const server = http
res.end(JSON.stringify({ message: 'Authentication failed' }))
} else {
// set delay in config.json
const Config = jsonfile.readFileSync(configPath)
const Config = getConfig()
Config.delay = interval
jsonfile.writeFileSync(configPath, Config, { spaces: 2 })
jsonfile.writeFileSync(configBackupPath, Config, { spaces: 2 })
Expand All @@ -250,7 +280,7 @@ const server = http
if (token !== adminPassword) {
res.end(JSON.stringify({ message: 'Authentication failed' }))
} else {
const Config = jsonfile.readFileSync(configPath)
const Config = getConfig()
// Remove this contributor in config.json
Config.contributors.forEach((contributor, index, object) => {
if (contributor == username) {
Expand Down Expand Up @@ -281,7 +311,7 @@ const server = http
if (token !== adminPassword) {
res.end(JSON.stringify({ message: 'Authentication failed' }))
} else {
const Config = jsonfile.readFileSync(configPath)
const Config = getConfig()

if (Config.contributors.includes(username)) {
res.end(JSON.stringify({ message: `${username} aready exists` }))
Expand All @@ -300,18 +330,18 @@ const server = http
// Add this contributor in the data.json
const data = jsonfile.readFileSync(dataPath)
API.getContributorInfo(
organization,
getOrganizationConfig().organization,
username,
Config.includedRepositories,
Config.startDate
).then((result) => {
).then((contributorInfo) => {
if (
result.avatarUrl !== '' &&
result.issuesNumber !== -1 &&
result.mergedPRsNumber !== -1 &&
result.openPRsNumber != -1
contributorInfo.avatarUrl !== '' &&
contributorInfo.issuesNumber !== -1 &&
contributorInfo.mergedPRsNumber !== -1 &&
contributorInfo.openPRsNumber !== -1
) {
data[`${username}`] = result
data[`${username}`] = contributorInfo
// Update contributors infomation
jsonfile.writeFile(dataPath, data, { spaces: 2 }, (err) => {
if (err) console.error(err)
Expand Down Expand Up @@ -358,9 +388,9 @@ const server = http
// Responds with rank of username
if (query.username) {
const rank =
contributors
.map((c) => c.toLowerCase())
.indexOf(query.username.toLowerCase()) + 1
contributors
.map((c) => c.toLowerCase())
.indexOf(query.username.toLowerCase()) + 1
res.end(
JSON.stringify(
rank
Expand Down Expand Up @@ -412,7 +442,6 @@ const server = http
break
}
})
.listen(port)

const io = require('socket.io')(server)
io.on('connection', (socket) => {
Expand All @@ -427,6 +456,10 @@ io.on('connection', (socket) => {
})
})

server.listen(httpPort)

module.exports = { server }

function findContributor(contributorName, admindata) {
let result = null

Expand Down
34 changes: 27 additions & 7 deletions src/server/util/API.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
const axios = require('axios')
Copy link
Copy Markdown
Member

@Sing-Li Sing-Li Mar 27, 2026

Choose a reason for hiding this comment

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

@srijnabhargav "making it work with dotEnv" means absolutely NO CHANGES to the base code.

So please remove all changes to any file under src/server

And fix the tests so they work.

Thanks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry sir and thank you for the clarification. I have updated the PR

What was wrong earlier:

  • I had introduced test-related changes under src/server, which was not aligned with the already-merged dotenv support in master.
  • That was unnecessary because the upstream server already supports env-based path/config overrides such as CONFIG_PATH, DATA_PATH, LOG_PATH, ADMINDATA_PATH, CONFIG_BACKUP_PATH, and SERVER_PORT.

What I changed now:

  • Reverted the src/server changes so the PR does not rely on core code modifications.
  • Updated the regression test to use the existing upstream env/dotenv model from the test side only.
  • Kept the test isolated by wiring fixture paths through env vars and preventing the background refresh process from affecting the snapshot check.
  • Updated test/README.md with the exact install and test steps.

Validation: ran following commands

npm i
npm --prefix src/server install
npm test

Result: the regression test passes on the current upstream server code.

const chalk = require('chalk')
const path = require('path')

function resolveConfigPath() {
if (process.env.LEADERBOARD_CONFIG_PATH) {
return path.resolve(process.env.LEADERBOARD_CONFIG_PATH)
}

if (process.env.CONFIG_PATH) {
return path.resolve(process.env.CONFIG_PATH)
}

return path.resolve(__dirname, '../config.json')
}

const Config = require(resolveConfigPath())

const BASEURL = 'https://github.com'
const APIHOST = 'https://api.github.com'
Expand All @@ -10,7 +25,7 @@ async function get(url, _authToken) {
headers: {
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'GSoC-Contribution-Leaderboard',
Authorization: 'token ' + process.env.AUTH_TOKEN,
Authorization: 'token ' + (process.env.AUTH_TOKEN || Config.authToken),
},
})
return new Promise((resolve) => {
Expand Down Expand Up @@ -130,14 +145,19 @@ async function getContributorInfo(
includedRepositories,
startDate
) {
if (!Array.isArray(includedRepositories)) {
includedRepositories = []
}

const effectiveStartDate = startDate || Config.startDate
const home = BASEURL + '/' + contributor
const avatarUrl = await getContributorAvatar(contributor)
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}`
let OpenPRsURL = `/search/issues?q=is:pr+author:${contributor}+is:Open+created:>=${effectiveStartDate}`
let openPRsLink = `${BASEURL}/search?q=type:pr+author:${contributor}+is:open+created:>=${effectiveStartDate}`
let MergedPRsURL = `/search/issues?q=is:pr+author:${contributor}+is:Merged+created:>=${effectiveStartDate}`
let mergedPRsLink = `${BASEURL}/search?q=type:pr+author:${contributor}+is:merged+created:>=${effectiveStartDate}`
let IssuesURL = `/search/issues?q=is:issue+author:${contributor}+created:>=${effectiveStartDate}`
let issuesLink = `${BASEURL}/search?q=type:issue+author:${contributor}+created:>=${effectiveStartDate}`
includedRepositories.forEach((repository) => {
openPRsLink += `+repo:${organization}/${repository}`
mergedPRsLink += `+repo:${organization}/${repository}`
Expand Down
24 changes: 24 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Regression tests

This directory holds automated regression tests for stable leaderboard behavior.

`leaderboard-e2e.test.js` starts the current server code against a fixed Rocket.Chat snapshot and verifies that `/stats`, `/rank`, and selected `/contributor` and `/rank?username=` responses still match the checked-in expected output.

Node version used:

- Node.js `v25.4.0`

Fixtures:

- `../contrib/rocketchat/gsoc/2025/gsoc2025final.json` is the canonical snapshot used as the fixed leaderboard input.
- `fixtures/gsoc2025final.expected.json` is the checked-in golden output generated from the current stable ranking logic and used for regression comparisons.

Run from the repo root:

```bash
npm i
npm --prefix src/server install
npm test
```

Note: `npm i` at the repo root installs only root dependencies. The regression test boots `src/server/app.js`, so `src/server` dependencies must also be installed before running `npm test`.
12 changes: 12 additions & 0 deletions test/fixtures/gsoc2025final.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"organization": "RocketChat",
"organizationHomepage": "https://rocket.chat/",
"organizationGithubUrl": "https://github.com/RocketChat",
"authToken": "",
"adminPassword": "123456",
"delay": "10",
"serverPort": "62050",
"contributors": [],
"startDate": "2024-12-01",
"includedRepositories": []
}
Loading