Skip to content

Commit d4781a2

Browse files
authored
Merge branch 'main' into fix/type-safety-event-ts
Signed-off-by: Srejoye Saha <sahasrejoye2005@gmail.com>
2 parents 67d6d90 + affea61 commit d4781a2

35 files changed

Lines changed: 3108 additions & 491 deletions
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
module.exports = async ({ github, context }) => {
2+
const owner = context.repo.owner;
3+
const repo = context.repo.repo;
4+
5+
const EXCLUDED = new Set([
6+
'shantkhatri',
7+
'harxhit',
8+
'blankirigaya'
9+
]);
10+
11+
const contributors = new Map();
12+
13+
const ensure = (login, avatarUrl, profileUrl) => {
14+
if (!contributors.has(login)) {
15+
contributors.set(login, {
16+
login,
17+
avatarUrl,
18+
profileUrl,
19+
mergedPrs: 0,
20+
openPrs: 0,
21+
issues: 0
22+
});
23+
}
24+
25+
return contributors.get(login);
26+
};
27+
28+
const mergedPrs = await github.paginate(
29+
github.rest.pulls.list,
30+
{
31+
owner,
32+
repo,
33+
state: 'closed',
34+
per_page: 100
35+
}
36+
);
37+
38+
for (const pr of mergedPrs) {
39+
if (!pr.merged_at || !pr.user) continue;
40+
41+
const login = pr.user.login;
42+
43+
if (EXCLUDED.has(login.toLowerCase())) continue;
44+
45+
const user = ensure(
46+
login,
47+
pr.user.avatar_url,
48+
pr.user.html_url
49+
);
50+
51+
user.mergedPrs++;
52+
}
53+
54+
const openPrs = await github.paginate(
55+
github.rest.pulls.list,
56+
{
57+
owner,
58+
repo,
59+
state: 'open',
60+
per_page: 100
61+
}
62+
);
63+
64+
for (const pr of openPrs) {
65+
if (!pr.user) continue;
66+
67+
const login = pr.user.login;
68+
69+
if (EXCLUDED.has(login.toLowerCase())) continue;
70+
71+
const user = ensure(
72+
login,
73+
pr.user.avatar_url,
74+
pr.user.html_url
75+
);
76+
77+
user.openPrs++;
78+
}
79+
80+
const issues = await github.paginate(
81+
github.rest.issues.listForRepo,
82+
{
83+
owner,
84+
repo,
85+
state: 'all',
86+
per_page: 100
87+
}
88+
);
89+
90+
for (const issue of issues) {
91+
if (issue.pull_request || !issue.user) continue;
92+
93+
const login = issue.user.login;
94+
95+
if (EXCLUDED.has(login.toLowerCase())) continue;
96+
97+
const user = ensure(
98+
login,
99+
issue.user.avatar_url,
100+
issue.user.html_url
101+
);
102+
103+
user.issues++;
104+
}
105+
106+
const leaderboard = [...contributors.values()].sort(
107+
(a, b) =>
108+
b.mergedPrs - a.mergedPrs ||
109+
b.issues - a.issues ||
110+
b.openPrs - a.openPrs ||
111+
a.login.localeCompare(b.login)
112+
);
113+
114+
const fs = require('fs');
115+
const path = require('path');
116+
117+
const outputDir = path.join('apps', 'web', 'public');
118+
const outputFile = path.join(outputDir, 'leaderboard.json');
119+
120+
fs.mkdirSync(outputDir, { recursive: true });
121+
122+
fs.writeFileSync(
123+
outputFile,
124+
JSON.stringify(leaderboard, null, 2),
125+
'utf8'
126+
);
127+
128+
console.log(`Generated ${leaderboard.length} contributors`);
129+
console.log(`Leaderboard written to ${outputFile}`);
130+
};

.github/workflows/ci.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ jobs:
5858

5959
- name: Install backend dependencies
6060
run: npm --prefix apps/backend install
61+
- name: Install shared dependencies
62+
run: npm --prefix packages/shared install
6163

64+
- name: Build shared package
65+
run: npm --prefix packages/shared run build
66+
6267
- name: DB migration check
6368
if: needs.detect-changes.outputs.dbFiles != ''
6469
continue-on-error: true
@@ -113,7 +118,8 @@ jobs:
113118
run: npm --prefix apps/web run build
114119

115120
- name: Fail job if any check failed
116-
if: steps.web_build.outcome == 'failure'
121+
if: >
122+
steps.web_build.outcome == 'failure'
117123
run: exit 1
118124

119125
mobile-ci:

.github/workflows/leaderboard.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Update Leaderboard
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: '0 * * * *'
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
issues: read
12+
13+
jobs:
14+
leaderboard:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Setup Node
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: 22
25+
26+
- name: Generate leaderboard
27+
uses: actions/github-script@v7
28+
with:
29+
github-token: ${{ secrets.GITHUB_TOKEN }}
30+
script: |
31+
const script = require('./.github/scripts/generateLeaderboard.js');
32+
await script({ github, context });
33+
34+
- name: Create Pull Request
35+
uses: peter-evans/create-pull-request@v6
36+
with:
37+
token: ${{ secrets.GITHUB_TOKEN }}
38+
commit-message: "chore: update leaderboard"
39+
title: "chore: update leaderboard"
40+
body: "Automated update of contributor leaderboard data."
41+
branch: "automation/leaderboard-update"
42+
base: "main"
43+
delete-branch: true

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ Thanks to all the amazing people who contribute to **DevCard** 🚀
307307
</a>
308308
</p>
309309

310+
> 🏆 **Contributor Leaderboard** — contributors are also ranked by merged PRs, issues, and open PRs in the web app at the [`/leaderboard`](https://devcard.app/leaderboard) route (`apps/web`).
311+
310312
<br>
311313

312314
## Project Support

apps/backend/src/__tests__/event.test.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,14 @@ async function buildApp(): Promise<FastifyInstance> {
7979
app.decorateRequest('jwtVerify', function () {
8080
return mockJwtVerify();
8181
});
82-
82+
app.decorate('authenticate', async function (request, reply) {
83+
try {
84+
const payload = await request.jwtVerify();
85+
if (payload) { request.user = payload as typeof request.user; }
86+
} catch {
87+
return reply.status(401).send({ error: 'Unauthorized' });
88+
}
89+
});
8390
// Register with the same prefix used in production (app.ts) so that
8491
// tests exercise routes at their real paths — /api/events, /api/events/:slug, etc.
8592
await app.register(eventRoutes, { prefix: '/api/events' });
@@ -253,14 +260,15 @@ describe('Events API', () => {
253260
it('200 — returns event info with attendee count', async () => {
254261
prismaMock.event.findUnique.mockResolvedValue({
255262
...MOCK_EVENT,
263+
organizer: { username: 'johndoe', displayName: 'John Doe' },
256264
_count: { attendees: 42 },
257265
});
258266

259267
const res = await app.inject({
260268
method: 'GET',
261269
url: '/api/events/devcard-conf-2025',
262270
});
263-
271+
console.log(JSON.stringify(res.json(), null, 2));
264272
expect(res.statusCode).toBe(200);
265273
const body = res.json();
266274
expect(body.slug).toBe('devcard-conf-2025');
@@ -277,7 +285,7 @@ describe('Events API', () => {
277285
method: 'GET',
278286
url: '/api/events/ghost-event',
279287
});
280-
288+
281289
expect(res.statusCode).toBe(404);
282290
expect(res.json()).toMatchObject({ error: 'Event not found' });
283291
});
@@ -287,6 +295,7 @@ describe('Events API', () => {
287295
mockJwtVerify.mockRejectedValue(new Error('Should not be called'));
288296
prismaMock.event.findUnique.mockResolvedValue({
289297
...MOCK_EVENT,
298+
organizer: { username: 'johndoe', displayName: 'John Doe' },
290299
_count: { attendees: 0 },
291300
});
292301

@@ -505,6 +514,7 @@ describe('Events API', () => {
505514
prismaMock.event.findUnique.mockResolvedValue({
506515
...MOCK_EVENT,
507516
attendees: attendeeRows,
517+
_count: { attendees: 2 },
508518
});
509519

510520
const res = await app.inject({
@@ -533,6 +543,7 @@ describe('Events API', () => {
533543
prismaMock.event.findUnique.mockResolvedValue({
534544
...MOCK_EVENT,
535545
attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)],
546+
_count: { attendees: 1 },
536547
});
537548

538549
const res = await app.inject({
@@ -555,6 +566,7 @@ describe('Events API', () => {
555566
prismaMock.event.findUnique.mockResolvedValue({
556567
...MOCK_EVENT,
557568
attendees: [],
569+
_count: { attendees: 0 },
558570
});
559571

560572
const res = await app.inject({
@@ -571,6 +583,7 @@ describe('Events API', () => {
571583
prismaMock.event.findUnique.mockResolvedValue({
572584
...MOCK_EVENT,
573585
attendees: [],
586+
_count: { attendees: 0 },
574587
});
575588

576589
const res = await app.inject({
@@ -587,6 +600,7 @@ describe('Events API', () => {
587600
prismaMock.event.findUnique.mockResolvedValue({
588601
...MOCK_EVENT,
589602
attendees: [],
603+
_count: { attendees: 0 },
590604
});
591605

592606
const res = await app.inject({
@@ -604,6 +618,7 @@ describe('Events API', () => {
604618
prismaMock.event.findUnique.mockResolvedValue({
605619
...MOCK_EVENT,
606620
attendees: [makeAttendeeRow(MOCK_USER_PROFILE)],
621+
_count: { attendees: 1 },
607622
});
608623

609624
const res = await app.inject({
@@ -693,4 +708,4 @@ describe('Events API', () => {
693708
expect(slug).not.toMatch(/--/);
694709
});
695710
});
696-
});
711+
});

apps/backend/src/__tests__/team.test.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { type PrismaClient, TeamRole } from '@prisma/client';
2+
import Fastify, { type FastifyInstance } from 'fastify';
13
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2-
import Fastify, { FastifyInstance } from 'fastify';
3-
import { PrismaClient, TeamRole } from '@prisma/client';
4+
45
import { teamRoutes } from '../routes/team';
56

67
// ─── Shared mock data ─────────────────────────────────────────────────────────
@@ -92,7 +93,7 @@ const prismaMock = {
9293

9394
// ─── App factory ──────────────────────────────────────────────────────────────
9495

95-
let mockJwtVerify = vi.fn();
96+
const mockJwtVerify = vi.fn();
9697

9798
async function buildApp(): Promise<FastifyInstance> {
9899
const app = Fastify({ logger: false });
@@ -102,7 +103,14 @@ async function buildApp(): Promise<FastifyInstance> {
102103
app.decorateRequest('jwtVerify', function () {
103104
return mockJwtVerify();
104105
});
105-
106+
app.decorate('authenticate', async function (request, reply) {
107+
try {
108+
const payload = await request.jwtVerify();
109+
if (payload) {request.user = payload as typeof request.user;}
110+
} catch {
111+
return reply.status(401).send({ error: 'Unauthorized' });
112+
}
113+
});
106114
await app.register(teamRoutes);
107115
await app.ready();
108116
return app;
@@ -118,7 +126,7 @@ async function createTeam(
118126
app: FastifyInstance,
119127
body: Record<string, unknown>,
120128
authenticated = true,
121-
) {
129+
): Promise<Awaited<ReturnType<typeof app.inject>>> {
122130
return app.inject({
123131
method: 'POST',
124132
url: '/',

apps/backend/src/plugins/prisma.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import fp from 'fastify-plugin';
21
import { PrismaClient } from '@prisma/client';
3-
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2+
import fp from 'fastify-plugin';
3+
4+
import type { FastifyInstance } from 'fastify';
45

56
declare module 'fastify' {
67
interface FastifyInstance {
78
prisma: PrismaClient;
8-
authenticate(
9-
request: FastifyRequest,
10-
reply: FastifyReply
11-
): Promise<void>;
129
}
1310
}
1411

0 commit comments

Comments
 (0)