Skip to content

Commit 710a114

Browse files
authored
Merge branch 'main' into feat/zod-param-validation
2 parents e98f260 + b25110f commit 710a114

27 files changed

Lines changed: 1264 additions & 291 deletions

.env.local.example

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# ============================================================
2+
# CommitPulse — Environment Variables
3+
# ============================================================
4+
# Copy this file to .env.local and fill in your own values.
5+
# cp .env.local.example .env.local
6+
#
7+
# .env.local is in .gitignore and will NEVER be committed.
8+
# ============================================================
9+
10+
11+
# ------------------------------------------------------------
12+
# REQUIRED — GitHub Personal Access Token
13+
# ------------------------------------------------------------
14+
# Used to authenticate with the GitHub GraphQL API.
15+
# Without this, every request to /api/streak returns 401.
16+
#
17+
# How to generate:
18+
# GitHub → Settings → Developer settings →
19+
# Personal access tokens → Tokens (classic) →
20+
# Generate new token (classic)
21+
# Required scope: read:user
22+
# ------------------------------------------------------------
23+
GITHUB_TOKEN=ghp_your_personal_access_token_here
24+
25+
26+
# ------------------------------------------------------------
27+
# REQUIRED — Public site URL
28+
# ------------------------------------------------------------
29+
# Used for generating absolute URLs (e.g. Open Graph metadata).
30+
# For local development, use http://localhost:3000
31+
# ------------------------------------------------------------
32+
NEXT_PUBLIC_SITE_URL=http://localhost:3000
33+
34+
35+
# ------------------------------------------------------------
36+
# OPTIONAL — MongoDB connection string
37+
# ------------------------------------------------------------
38+
# Enables user tracking: when someone generates a monolith on
39+
# the landing page, their GitHub username is stored in MongoDB.
40+
#
41+
# If this is NOT set, the /api/track-user route will log a
42+
# warning and skip the DB write — the app works fine without it.
43+
#
44+
# How to get one (free):
45+
# 1. Create a free cluster at https://cloud.mongodb.com
46+
# 2. Create a database user and whitelist your IP
47+
# 3. Click "Connect" → "Drivers" → copy the connection string
48+
# 4. Replace <password> with your DB user's password
49+
# ------------------------------------------------------------
50+
# MONGODB_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/commitpulse?retryWrites=true&w=majority
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: Merge Conflict Notifier
2+
3+
on:
4+
schedule:
5+
# Runs every day at 00:00 UTC
6+
- cron: '0 0 * * *'
7+
workflow_dispatch: # Allow manual trigger for testing
8+
9+
permissions:
10+
issues: write
11+
pull-requests: write
12+
13+
jobs:
14+
check-conflicts:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Check open PRs for merge conflicts
18+
uses: actions/github-script@v7
19+
with:
20+
script: |
21+
const label = 'needs-rebase';
22+
23+
// Ensure the label exists in the repo
24+
try {
25+
await github.rest.issues.getLabel({
26+
owner: context.repo.owner,
27+
repo: context.repo.repo,
28+
name: label,
29+
});
30+
} catch {
31+
await github.rest.issues.createLabel({
32+
owner: context.repo.owner,
33+
repo: context.repo.repo,
34+
name: label,
35+
color: 'e11d48',
36+
description: 'This PR has merge conflicts and needs a rebase.',
37+
});
38+
}
39+
40+
// Fetch all open PRs
41+
const { data: openPRs } = await github.rest.pulls.list({
42+
owner: context.repo.owner,
43+
repo: context.repo.repo,
44+
state: 'open',
45+
per_page: 100,
46+
});
47+
48+
for (const pr of openPRs) {
49+
// GitHub computes `mergeable` lazily — fetch each PR individually to trigger computation
50+
const { data: fullPR } = await github.rest.pulls.get({
51+
owner: context.repo.owner,
52+
repo: context.repo.repo,
53+
pull_number: pr.number,
54+
});
55+
56+
const isConflicting = fullPR.mergeable === false;
57+
const currentLabels = fullPR.labels.map(l => l.name);
58+
const alreadyLabeled = currentLabels.includes(label);
59+
60+
if (isConflicting) {
61+
// Apply label if not already applied
62+
if (!alreadyLabeled) {
63+
await github.rest.issues.addLabels({
64+
owner: context.repo.owner,
65+
repo: context.repo.repo,
66+
issue_number: pr.number,
67+
labels: [label],
68+
});
69+
}
70+
71+
// Check if we've already posted a conflict comment to avoid spam
72+
const { data: comments } = await github.rest.issues.listComments({
73+
owner: context.repo.owner,
74+
repo: context.repo.repo,
75+
issue_number: pr.number,
76+
per_page: 100,
77+
});
78+
79+
const alreadyCommented = comments.some(c =>
80+
c.user?.login === 'github-actions[bot]' &&
81+
c.body?.includes('merge conflicts with the main branch')
82+
);
83+
84+
if (!alreadyCommented) {
85+
await github.rest.issues.createComment({
86+
owner: context.repo.owner,
87+
repo: context.repo.repo,
88+
issue_number: pr.number,
89+
body: `⚠️ Hey @${fullPR.user.login}, this PR has merge conflicts with the \`main\` branch.
90+
91+
Please pull the latest changes and resolve the conflicts so we can review it!
92+
93+
\`\`\`bash
94+
git fetch origin
95+
git rebase origin/main
96+
# resolve any conflicts, then:
97+
git push --force-with-lease
98+
\`\`\`
99+
100+
Once resolved, the \`needs-rebase\` label will be removed automatically on the next check. 🙌`,
101+
});
102+
}
103+
} else if (!isConflicting && alreadyLabeled) {
104+
// PR is no longer conflicting — remove the stale label
105+
await github.rest.issues.removeLabel({
106+
owner: context.repo.owner,
107+
repo: context.repo.repo,
108+
issue_number: pr.number,
109+
name: label,
110+
});
111+
}
112+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: PR Merged Greeter
2+
3+
on:
4+
pull_request_target:
5+
types: [closed]
6+
7+
permissions:
8+
pull-requests: write
9+
issues: write
10+
11+
jobs:
12+
thank-contributor:
13+
if: github.event.pull_request.merged == true
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Leave Welcome & Discord Comment
17+
uses: actions/github-script@v7
18+
with:
19+
script: |
20+
const creator = context.payload.pull_request.user.login;
21+
const message = `🎉 **Congratulations @${creator}! Your PR has been successfully merged.** 🚀\n\nThank you for contributing to **CommitPulse**. Your work helps us build a better tool for the community.\n\n⚠️ **Important for GSSoC Contributors:**\nYou are strictly advised to join our [Discord Server](https://discord.gg/Cb73bS79j) as it is **mandatory** for all GSSoC participants. All important announcements, point claims, and community discussions happen there.\n\nKeep building! 💻✨`;
22+
await github.rest.issues.createComment({
23+
owner: context.repo.owner,
24+
repo: context.repo.repo,
25+
issue_number: context.payload.pull_request.number,
26+
body: message
27+
});
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
name: PR Template Enforcer
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, edited, reopened]
6+
workflow_dispatch: # Manually trigger to scan ALL open PRs
7+
8+
permissions:
9+
issues: write
10+
pull-requests: write
11+
12+
jobs:
13+
enforce-template:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Check PR body against template structure
17+
uses: actions/github-script@v7
18+
with:
19+
script: |
20+
const label = 'needs-details';
21+
22+
// Build the list of PRs to check:
23+
// - workflow_dispatch: scan all open PRs
24+
// - pull_request_target: check only the triggering PR
25+
let prsToCheck;
26+
if (context.eventName === 'workflow_dispatch') {
27+
const { data } = await github.rest.pulls.list({
28+
owner: context.repo.owner,
29+
repo: context.repo.repo,
30+
state: 'open',
31+
per_page: 100,
32+
});
33+
prsToCheck = data;
34+
} else {
35+
prsToCheck = [context.payload.pull_request];
36+
}
37+
38+
const body_template = pr => pr.body ?? '';
39+
40+
// Required section headings from .github/pull_request_template.md
41+
const requiredSections = [
42+
'## Description',
43+
'## Pillar',
44+
'## Checklist',
45+
];
46+
47+
// Minimum character threshold — catches "fixed issue" type bodies
48+
const MIN_LENGTH = 80;
49+
50+
// Ensure the label exists once before the loop
51+
try {
52+
await github.rest.issues.getLabel({
53+
owner: context.repo.owner,
54+
repo: context.repo.repo,
55+
name: label,
56+
});
57+
} catch {
58+
await github.rest.issues.createLabel({
59+
owner: context.repo.owner,
60+
repo: context.repo.repo,
61+
name: label,
62+
color: 'f97316',
63+
description: 'This PR is missing required description details.',
64+
});
65+
}
66+
67+
for (const pr of prsToCheck) {
68+
const body = body_template(pr);
69+
70+
const missingSection = requiredSections.find(
71+
section => !body.toLowerCase().includes(section.toLowerCase())
72+
);
73+
const isTooShort = body.trim().length < MIN_LENGTH;
74+
const needsAction = missingSection || isTooShort;
75+
76+
const currentLabels = pr.labels.map(l => l.name);
77+
const alreadyLabeled = currentLabels.includes(label);
78+
79+
if (needsAction) {
80+
// Apply label
81+
if (!alreadyLabeled) {
82+
await github.rest.issues.addLabels({
83+
owner: context.repo.owner,
84+
repo: context.repo.repo,
85+
issue_number: pr.number,
86+
labels: [label],
87+
});
88+
}
89+
90+
// Check for an existing enforcement comment to avoid duplicates
91+
const { data: comments } = await github.rest.issues.listComments({
92+
owner: context.repo.owner,
93+
repo: context.repo.repo,
94+
issue_number: pr.number,
95+
per_page: 100,
96+
});
97+
98+
const alreadyCommented = comments.some(c =>
99+
c.user?.login === 'github-actions[bot]' &&
100+
c.body?.includes("didn't use our PR template")
101+
);
102+
103+
if (!alreadyCommented) {
104+
const reason = missingSection
105+
? `The section **\`${missingSection}\`** is missing from your PR description.`
106+
: `Your PR description is too short (${body.trim().length} characters — minimum is ${MIN_LENGTH}).`;
107+
108+
await github.rest.issues.createComment({
109+
owner: context.repo.owner,
110+
repo: context.repo.repo,
111+
issue_number: pr.number,
112+
body: `👋 Hey @${pr.user.login}, it looks like you didn't use our PR template!
113+
114+
${reason}
115+
116+
Please update your PR description to include all required sections so we can review this properly:
117+
- **\`## Description\`** — What does this PR do? Which issue does it fix?
118+
- **\`## Pillar\`** — Which contribution pillar does this fall under?
119+
- **\`## Checklist\`** — Have you ticked off the quality checklist?
120+
121+
You can find the full template in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/CONTRIBUTING.md#-opening-a-pull-request). Just edit your PR description and the \`needs-details\` label will be removed automatically. 🙌`,
122+
});
123+
}
124+
} else if (!needsAction && alreadyLabeled) {
125+
// PR has been updated and now meets the requirements — remove the label
126+
await github.rest.issues.removeLabel({
127+
owner: context.repo.owner,
128+
repo: context.repo.repo,
129+
issue_number: pr.number,
130+
name: label,
131+
});
132+
}
133+
}

.github/workflows/welcome.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Welcome First-Time Contributors
2+
3+
on:
4+
issues:
5+
types: [opened]
6+
pull_request_target:
7+
types: [opened]
8+
9+
permissions:
10+
issues: write
11+
pull-requests: write
12+
13+
jobs:
14+
welcome:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Welcome first-time issue opener
18+
if: >
19+
github.event_name == 'issues' &&
20+
github.event.issue.author_association == 'NONE'
21+
uses: actions/github-script@v7
22+
with:
23+
script: |
24+
await github.rest.issues.createComment({
25+
owner: context.repo.owner,
26+
repo: context.repo.repo,
27+
issue_number: context.issue.number,
28+
body: `👋 Hey @${context.payload.issue.user.login}, welcome to **CommitPulse**! 🎉
29+
30+
Thanks for opening your first issue — we're glad you're here.
31+
32+
Before diving in, please take a moment to:
33+
- 📖 Read the [**CONTRIBUTING.md**](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/CONTRIBUTING.md) guide
34+
- 💬 Join our [**Discord community**](https://discord.gg/Cb73bS79j) for faster support and collaboration
35+
- 🏷️ Comment \`/claim\` on any open, unassigned issue you'd like to work on
36+
37+
A maintainer will review your issue shortly. Happy contributing! 🚀`
38+
});
39+
40+
- name: Welcome first-time PR opener
41+
if: >
42+
github.event_name == 'pull_request_target' &&
43+
github.event.pull_request.author_association == 'NONE'
44+
uses: actions/github-script@v7
45+
with:
46+
script: |
47+
await github.rest.issues.createComment({
48+
owner: context.repo.owner,
49+
repo: context.repo.repo,
50+
issue_number: context.payload.pull_request.number,
51+
body: `👋 Hey @${context.payload.pull_request.user.login}, welcome to **CommitPulse**! 🎉
52+
53+
Thanks for opening your first pull request — this is a big deal and we appreciate the effort!
54+
55+
While you wait for a review, please double-check:
56+
- ✅ You've read the [**CONTRIBUTING.md**](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/CONTRIBUTING.md) checklist
57+
- ✅ \`npm run lint\`, \`npm run format\`, and \`npm run test\` all pass locally
58+
- ✅ Your PR has a visual preview if it touches any SVG output
59+
- 💬 You've joined our [**Discord**](https://discord.gg/Cb73bS79j) for faster PR feedback
60+
61+
A maintainer will review your PR shortly. Hang tight! 🚀`
62+
});

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ yarn-debug.log*
3030
yarn-error.log*
3131
.pnpm-debug.log*
3232

33-
# env files (can opt-in for committing if needed)
33+
# env files — ignore all local secrets but commit the example template
3434
.env*
35+
!.env.local.example
3536

3637
# vercel
3738
.vercel

0 commit comments

Comments
 (0)