Skip to content

Commit 84af6d4

Browse files
Merge branch 'main' into improve-contributors-loading-state
2 parents 496e6b4 + a4dcf09 commit 84af6d4

135 files changed

Lines changed: 12969 additions & 3483 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/issue-management/claim-handler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ async function handleClaim({ github, context }) {
7474
owner,
7575
repo,
7676
issue_number: issueNumber,
77-
body: `❌ You already have **${existingIssues.length}/${MAX_ASSIGNED_ISSUES}** active assigned issues (the maximum allowed).\nPlease complete or unassign one of your current issues before claiming another.\n\n${issueList}`,
77+
body: `❌ You already have **${existingIssues.length}/${MAX_ASSIGNED_ISSUES}** active assigned issues (the maximum allowed).\nPlease complete or \`/unclaim\` one of your current issues before claiming another.\n\n${issueList}`,
7878
});
7979
return;
8080
}

.github/scripts/issue-management/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { handleAssign } = require('./assign-handler');
44
const { handleUnassign } = require('./unassign-handler');
55
const { handleAddLabel } = require('./addlabel-handler');
66
const { handleClaim } = require('./claim-handler');
7+
const { handleUnclaim } = require('./unclaim-handler');
78

89
module.exports = async ({ github, context, core }) => {
910
const commentBody = context.payload.comment?.body;
@@ -48,6 +49,9 @@ module.exports = async ({ github, context, core }) => {
4849
case 'claim':
4950
await handleClaim({ github, context });
5051
break;
52+
case 'unclaim':
53+
await handleUnclaim({ github, context });
54+
break;
5155
case 'ping':
5256
await github.rest.issues.createComment({
5357
owner,

.github/scripts/issue-management/parse-command.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ function parseCommand(commentBody) {
1515
const claimMatch = line.match(/^\/claim\s*$/i);
1616
if (claimMatch) return { command: 'claim' };
1717

18+
const unclaimMatch = line.match(/^\/unclaim\s*$/i);
19+
if (unclaimMatch) return { command: 'unclaim' };
20+
1821
const pingMatch = line.match(/^\/ping\s*$/i);
1922
if (pingMatch) return { command: 'ping' };
2023

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
async function handleUnclaim({ github, context }) {
2+
const { owner, repo } = context.repo;
3+
const issueNumber = context.payload.issue.number;
4+
const commenter = context.payload.comment.user.login;
5+
const issueState = context.payload.issue.state;
6+
7+
if (issueState === 'closed') {
8+
await github.rest.issues.createComment({
9+
owner,
10+
repo,
11+
issue_number: issueNumber,
12+
body: `❌ Commands cannot be used on closed issues.`,
13+
});
14+
return;
15+
}
16+
17+
const currentAssignees = context.payload.issue.assignees.map((a) => a.login.toLowerCase());
18+
19+
if (!currentAssignees.includes(commenter.toLowerCase())) {
20+
await github.rest.issues.createComment({
21+
owner,
22+
repo,
23+
issue_number: issueNumber,
24+
body: `ℹ️ @${commenter}, you are not currently assigned to this issue, so there's nothing to unclaim.`,
25+
});
26+
return;
27+
}
28+
29+
await github.rest.issues.removeAssignees({
30+
owner,
31+
repo,
32+
issue_number: issueNumber,
33+
assignees: [commenter],
34+
});
35+
36+
await github.rest.issues.createComment({
37+
owner,
38+
repo,
39+
issue_number: issueNumber,
40+
body: `✅ Successfully unclaimed this issue for @${commenter}.\n\n> 🔓 The issue is now open for others to claim.`,
41+
});
42+
}
43+
44+
module.exports = { handleUnclaim };

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main, dev]
66
pull_request:
77
branches: [main, dev]
8+
merge_group: # Run CI on the merged commit before it lands on main
89

910
jobs:
1011
quality:

.github/workflows/conflict-notifier.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches:
88
- main
99
pull_request_target:
10-
types: [opened, synchronize, reopened, edited]
10+
types: [synchronize, reopened, edited]
1111
workflow_dispatch:
1212

1313
permissions:

.github/workflows/issue-management.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ jobs:
2222
contains(github.event.comment.body, '/assign') ||
2323
contains(github.event.comment.body, '/unassign') ||
2424
contains(github.event.comment.body, '/addlabel') ||
25-
contains(github.event.comment.body, '/claim')
25+
contains(github.event.comment.body, '/claim') ||
26+
contains(github.event.comment.body, '/unclaim')
2627
)
2728
steps:
2829
- name: Checkout Repository

CONTRIBUTING.md

Lines changed: 139 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
- [The Standard We Hold](#-the-standard-we-hold)
1111
- [Local Setup](#-local-setup)
12-
- [Testing Your Changes](#-testing-your-changes)
12+
- [Testing & Previewing Your Changes](#-testing--previewing-your-changes)
1313
- [What to Contribute](#-what-to-contribute)
1414
- [Automated Issue Management & Claiming](#-automated-issue-management--claiming)
1515
- [Branch & Commit Conventions](#-branch--commit-conventions)
@@ -89,13 +89,13 @@ http://localhost:3000/api/streak?user=YOUR_GITHUB_USERNAME
8989
9090
---
9191

92-
## 🧪 Testing Your Changes
92+
## 🧪 Testing & Previewing Your Changes
9393

94-
This section covers everything you need to verify your changes work correctly **before opening a PR**. There are two layers of testing: visual browser preview and the automated test suite.
94+
Before opening a PR, every contributor is expected to verify their changes locally across three areas: **visual SVG output**, **unit tests**, and **branch coverage**. This section explains how to do all three.
9595

9696
### 1. Visual Browser Preview
9797

98-
With your dev server running (`npm run dev`), open your browser and visit these URLs to preview the SVG output directly:
98+
With your dev server running (`npm run dev`), open your browser and visit these URLs to preview the SVG output directly**open them as raw URLs, not embedded in an `<img>` tag or Markdown preview**:
9999

100100
**Standard badge — valid username:**
101101

@@ -127,46 +127,90 @@ http://localhost:3000/api/streak?user=vivek%20Sangani
127127
http://localhost:3000/api/streak?user=xyzabc999notreallll
128128
```
129129

130+
**Useful DevTools tricks:**
131+
132+
- **Inspect SVG structure:** Open DevTools (`Cmd+Option+I` / `F12`) → Elements panel → expand the `<svg>` node to inspect every path, rect, and text element
133+
- **Force a fresh fetch:** Add `&refresh=true` to bypass the cache: `http://localhost:3000/api/streak?user=YOUR_GITHUB_USERNAME&refresh=true`
134+
- **Test different params live:** `?theme=obsidian`, `?size=large`, `?grace=2`, `?autoTheme=true`
135+
130136
> **⚠️ Browser XML error warning:** If your browser shows an `EntityRef: expecting ';'` error instead of rendering the SVG, it means an unescaped `&` character exists somewhere in the SVG output. All `&` characters inside SVG `<style>` blocks (e.g. in Google Fonts `@import` URLs) must be written as `&amp;`. Check `lib/svg/generator.ts` for any raw `&` in template literals.
131137
132138
> **💡 Tip:** For a cleaner SVG preview, open the URL in **Firefox** — it renders SVG directly in the browser with no wrapper page. Chrome wraps it in an XML viewer which can show false parse warnings.
133139
134-
### 2. Running the Vitest Test Suite
140+
### 2. Offline SVG Inspection with SVG Viewer
141+
142+
When you cannot run a dev server — or you want to inspect the raw SVG geometry in isolation — use **[SVG Viewer](https://www.svgviewer.dev/)**.
143+
144+
**Step-by-step:**
145+
146+
1. With the dev server running, open the badge URL in your browser
147+
2. Right-click anywhere on the page → **View Page Source** (`Cmd+U` / `Ctrl+U`)
148+
3. Select all (`Cmd+A`) → Copy (`Cmd+C`)
149+
4. Go to [https://www.svgviewer.dev/](https://www.svgviewer.dev/)
150+
5. Paste the SVG source into the left panel — the right panel renders it instantly
151+
152+
**What to check in SVG Viewer:**
153+
154+
- Tower geometry is correctly positioned on the isometric grid
155+
- No overlapping paths or misaligned elements
156+
- Text labels are readable and not clipped by the SVG boundary
157+
- Glow filter (`<feGaussianBlur>`) renders at the correct intensity
158+
- The `<title>` accessibility tag is present on every tower group (required by CONTRIBUTING.md a11y rules)
159+
160+
### 3. Running the Vitest Test Suite
135161

136162
CommitPulse uses **Vitest** for unit and integration tests. Run the full test suite with:
137163

138164
```bash
165+
# Run all tests once (use before committing)
139166
npm run test
140-
```
141167

142-
To run tests in watch mode while you develop (reruns on every file save):
143-
144-
```bash
168+
# Re-run automatically on every file save (use during active development)
145169
npm run test -- --watch
146-
```
147170

148-
To run only a specific test file:
149-
150-
```bash
171+
# Run only a specific test file in isolation (fastest way to debug a failure)
151172
npm run test -- lib/calculate.test.ts
173+
174+
# Run only tests whose name matches a pattern
175+
npm run test -- --reporter=verbose -t "generateVersusSVG"
152176
```
153177

154178
**What a passing run looks like:**
155179

156180
```
157-
✓ lib/calculate.test.ts (12 tests)
158-
✓ lib/svg/generator.test.ts (8 tests)
159-
✓ app/api/streak/route.test.ts (6 tests)
181+
✓ lib/calculate.test.ts (43 tests)
182+
✓ lib/svg/generator.test.ts (19 tests)
183+
✓ app/api/streak/route.test.ts (112 tests)
160184
161-
Test Files 3 passed (3)
162-
Tests 26 passed (26)
185+
Test Files 70 passed (70)
186+
Tests 819 passed (819)
163187
```
164188

165189
> **🚨 All tests must pass before you open a PR.** The CI pipeline runs `npm run test` automatically on every pull request and will block merging if any test fails.
166190
167-
### 3. Interpreting SVG Output in the Browser
191+
### 4. Interpreting Vitest Output
192+
193+
Vitest output can look intimidating at first. Here is how to read it:
194+
195+
```
196+
FAIL lib/svg/generator.test.ts
197+
198+
AssertionError: expected '<svg width="600"…' to include 'width="100%"'
199+
200+
- Expected: width="100%"
201+
+ Received: width="600"
202+
```
203+
204+
| Part of output | What it means |
205+
| ------------------------------------ | ----------------------------------------------------------------- |
206+
| `FAIL lib/svg/generator.test.ts` | The test file that contains the failing test |
207+
| `AssertionError` | The `expect()` assertion did not match |
208+
| `- Expected` | What the test wanted to see in your output |
209+
| `+ Received` | What your code actually produced |
210+
| `stderr \| …` | Expected console output from error-handling tests — **not a bug** |
211+
| `✓ lib/calculate.test.ts (43 tests)` | This file passed — all good |
168212

169-
When you open a badge URL in your browser, here is what each response means:
213+
**Interpreting SVG URL responses in the browser:**
170214

171215
| What you see | What it means |
172216
| -------------------------------------------- | ---------------------------------------------------------- |
@@ -177,15 +221,61 @@ When you open a badge URL in your browser, here is what each response means:
177221
| Browser XML parse error / blank white page | ❌ Bug — unescaped `&` or malformed SVG in generator |
178222
| `401 Unauthorized` in the terminal | ❌ Your `GITHUB_PAT` in `.env.local` is missing or invalid |
179223

180-
### 4. Lint and Format
224+
### 5. Checking Branch Coverage Before Pushing
225+
226+
The CI pipeline enforces a **minimum 70% branch coverage** across all `lib/` files. If your PR drops coverage below this threshold, it will be **blocked from merging automatically** — no exceptions.
227+
228+
Always run the coverage report locally before pushing:
229+
230+
```bash
231+
npm run test:coverage
232+
```
233+
234+
The output shows a table like this:
235+
236+
```
237+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
238+
-------------------|---------|----------|---------|---------|------------------
239+
lib/svg/generator | 72.22 | 49.73 | 72.22 | 72.72 | 823-885,1116,1136
240+
lib/calculate | 99.00 | 93.54 | 100.00 | 98.92 | 167
241+
```
242+
243+
**What each column means:**
244+
245+
| Column | CI Gate | What it measures |
246+
| ------------------- | --------- | -------------------------------------------------------------------------- |
247+
| `% Branch` | **≥ 70%** | Every `if/else`, ternary, `??`, and `&&` path covered by at least one test |
248+
| `% Funcs` | ≥ 70% | Every exported function called at least once in tests |
249+
| `% Stmts` | ≥ 70% | Every statement executed at least once |
250+
| `Uncovered Line #s` || Exact lines with no test coverage — start here when fixing gaps |
251+
252+
**The most important file:** `lib/svg/generator.ts` has the most complex branch logic (auto-theme vs static, size scaling, particle generation, ghost city mode, versus mode). If you add any new `if/else` or ternary logic here, you **must** add tests that exercise both branches — otherwise coverage will drop and block your PR.
253+
254+
**If coverage drops below 70%:**
255+
256+
1. Look at the `Uncovered Line #s` column for the failing file
257+
2. Open the file and find those exact lines
258+
3. Identify which branch condition is untested (the `if` path? the `else`? a ternary fallback?)
259+
4. Add a test that reaches that code path
260+
5. Re-run `npm run test:coverage` and confirm the percentage recovered
261+
262+
> **Tip:** The coverage report is also generated as an HTML file. Open it in your browser for a visual, line-by-line view of exactly which branches are hit (green) and which are missed (red):
263+
264+
```bash
265+
open coverage/index.html # macOS
266+
xdg-open coverage/index.html # Linux
267+
start coverage/index.html # Windows
268+
```
269+
270+
### 6. Lint and Format
181271

182272
Run these before every commit:
183273

184274
```bash
185-
# Auto-format all files
275+
# Auto-format all files to match the project's Prettier config
186276
npm run format
187277

188-
# Check for linting errors
278+
# Check for any remaining linting errors
189279
npm run lint
190280
```
191281

@@ -338,6 +428,7 @@ Our automation runs entirely through issue comments. Here is how you interact wi
338428
| Command | Who Can Use It? | What It Does |
339429
| ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------- |
340430
| `/claim` | **Issue Author (or Anyone if authored by jhasourav07)** | Self-assigns the issue to you. |
431+
| `/unclaim` | **Assigned Contributor** | Removes the assignment from yourself (opens it back up). |
341432
| `/addlabel <label1> <label2>` | **Anyone** | Adds labels to the issue (e.g. `/addlabel frontend bug`). |
342433
| `/unassign @username` | **Maintainers Only** | Removes the assignee from an issue. |
343434
| `/assign @username` | **Maintainers Only** | Manually assigns someone to an issue. |
@@ -363,7 +454,7 @@ To keep the project moving, assignments are not permanent.
363454
If the bot rejects your command, check these common scenarios:
364455

365456
- **"Commands cannot be used on closed issues"**: You cannot claim, assign, or unassign on closed issues. Find an open one!
366-
- **"You already have X/3 active assigned issues"**: You have reached the maximum of 3 concurrent assignments. Finish one of your current tasks before claiming a new issue. If you're stuck, ask a maintainer to `/unassign` you from one.
457+
- **"You already have X/3 active assigned issues"**: You have reached the maximum of 3 concurrent assignments. Finish one of your current tasks before claiming a new issue. If you're stuck, use the `/unclaim` command to unassign yourself from an issue, or ask a maintainer to `/unassign` you.
367458
- **"This issue is already assigned to @username"**: Be faster next time! Look for issues without assignees.
368459
- **"Only the author of this issue can claim it"**: You tried to `/claim` an issue you did not create. You can only claim issues that you authored (unless the issue was authored by `jhasourav07`, which anyone can claim).
369460
- **"The following label(s) do not exist"**: You can only add existing repo labels. The bot will reply with a list of valid labels you can use.
@@ -385,6 +476,7 @@ Use the following format: `type/short-description`
385476
| Timezone work | `fix/utc-midnight-edge-case` |
386477
| Documentation | `docs/readme-update` |
387478
| Refactor | `refactor/generator-cleanup` |
479+
| Testing | `test/generator-coverage` |
388480

389481
### Commit Messages
390482

@@ -398,6 +490,7 @@ feat(themes): add aurora preset with teal-pink palette
398490
fix(calculate): handle grace period when today has zero contributions
399491
docs(readme): add aurora theme to parameter table
400492
refactor(generator): extract tower path builder into helper function
493+
test(generator): add Vitest coverage for generateVersusSVG
401494
```
402495

403496
**Types:** `feat`, `fix`, `docs`, `refactor`, `chore`, `test`
@@ -432,6 +525,8 @@ Fixes # (issue number)
432525
- [ ] I have read the `CONTRIBUTING.md` file.
433526
- [ ] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`).
434527
- [ ] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise).
528+
- [ ] I have run `npm run test` and all tests pass locally.
529+
- [ ] I have run `npm run test:coverage` and branch coverage is at or above 70%.
435530
- [ ] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`).
436531
- [ ] I have updated `README.md` if I added a new theme or URL parameter.
437532
- [ ] I have started the repo.
@@ -463,10 +558,29 @@ npm run lint
463558

464559
# 3. Ensure all tests pass
465560
npm run test
561+
562+
# 4. Verify branch coverage stays at or above 70%
563+
npm run test:coverage
466564
```
467565

468566
Fix every error before pushing. If you add new logic or features, you are expected to write tests for them.
469567

568+
### Coverage Threshold
569+
570+
The CI pipeline enforces a **minimum 70% branch coverage** across all measured files. This is not optional — PRs that drop coverage below this threshold are automatically blocked from merging.
571+
572+
The threshold exists because branch coverage (covering every `if/else`, ternary, and `??` path) is the most reliable indicator that edge cases are tested. Statement coverage alone is not enough.
573+
574+
**Files most likely to affect the gate:**
575+
576+
| File | Why it matters |
577+
| ------------------------- | -------------------------------------------------------------------------- |
578+
| `lib/svg/generator.ts` | Most complex branching — auto-theme, size scaling, ghost city, versus mode |
579+
| `lib/calculate.ts` | Streak logic — grace period, timezone edge cases, empty calendars |
580+
| `app/api/streak/route.ts` | Request handling — validation, rate limit, not-found, error paths |
581+
582+
If you add a new `if/else`, ternary, or optional-chain (`?.`) anywhere in `lib/`, you must add a test covering both the truthy and falsy paths before pushing.
583+
470584
### Testing Guidelines
471585

472586
We use **Vitest** alongside **React Testing Library** for our test suite.
@@ -478,7 +592,7 @@ We use **Vitest** alongside **React Testing Library** for our test suite.
478592
- **Humanic Comments:** Comments in test files should explain _why_ a test exists or what specific edge-case it covers, rather than just repeating what the code does line-by-line.
479593

480594
> **🚨 GitHub Actions CI Gate**
481-
> Our CI pipeline runs `npm run lint`, `npm run format --check`, and `npm run test` automatically on **every pull request**. If your code fails any check, **the PR will be blocked from merging** until the issues are resolved. There is no way to bypass this gate — so run the commands locally first and save yourself the round-trip.
595+
> Our CI pipeline runs `npm run lint`, `npm run format --check`, `npm run test`, and `npm run test:coverage` automatically on **every pull request**. If your code fails any check, **the PR will be blocked from merging** until the issues are resolved. There is no way to bypass this gate — so run the commands locally first and save yourself the round-trip.
482596
483597
**Key style rules:**
484598

0 commit comments

Comments
 (0)