a8d5f205db
Fix "Prever" typo, add Windows WSL test runner command, and align Markdown table columns for readability. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
229 lines
8.8 KiB
Markdown
229 lines
8.8 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Development
|
||
|
||
```bash
|
||
# Install dependencies (generates composer.lock if missing — commit it)
|
||
composer install
|
||
|
||
# Run dev server
|
||
APP_ENV=dev php -S localhost:8080 -t public
|
||
|
||
# Test the graph endpoint
|
||
curl "http://localhost:8080/graph.svg?theme=dark" -o graph.svg
|
||
curl "http://localhost:8080/health"
|
||
```
|
||
|
||
### Testing conventions (apply to every test file)
|
||
|
||
- `declare(strict_types=1)` at the top of every test file
|
||
- Test classes are `final` and extend `TestCase`
|
||
- Methods: `#[Test]` attribute + `it_` prefix + `snake_case` — e.g. `it_returns_empty_when_no_contributions`
|
||
- Structure: Arrange → Act → Assert, separated by blank lines; one assertion per test when possible
|
||
- Use `$this->assert*()` not `self::assert*()`
|
||
|
||
```php
|
||
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Tests\Service;
|
||
|
||
use App\Service\SvgRenderer;
|
||
use PHPUnit\Framework\Attributes\Test;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
final class SvgRendererTest extends TestCase
|
||
{
|
||
#[Test]
|
||
public function it_renders_an_svg_with_no_contributions(): void
|
||
{
|
||
$renderer = new SvgRenderer();
|
||
|
||
$svg = $renderer->render([], 'light');
|
||
|
||
$this->assertStringContainsString('<svg', $svg);
|
||
}
|
||
}
|
||
```
|
||
|
||
### TDD: Red → Green → Refactor
|
||
|
||
Always drive new features and bug fixes with this cycle. Claude writes implementation first by default — override that with explicit prompts.
|
||
|
||
**Phase 1 — Red (write a failing test first)**
|
||
|
||
```
|
||
Write a failing test for [feature description].
|
||
Do NOT write the implementation yet.
|
||
The test should fail because the class/method does not exist.
|
||
```
|
||
|
||
**Phase 2 — Green (minimal implementation)**
|
||
|
||
```
|
||
The tests are written and failing. Now implement the minimum code to make them pass. Nothing more.
|
||
```
|
||
|
||
**Phase 3 — Refactor**
|
||
|
||
```
|
||
Tests are green. Refactor the implementation for [readability / removing duplication / naming].
|
||
Run vendor/bin/phpunit after each change to confirm tests stay green.
|
||
```
|
||
|
||
**Common anti-patterns**
|
||
|
||
| Wrong prompt | Why it breaks TDD | Correct prompt |
|
||
| ------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------ |
|
||
| "Write tests for this feature" | Claude implements first, then fits tests to it | "Write **failing** tests for [feature]. Stop before any implementation." |
|
||
| "Add tests and implementation" | Loses the design feedback of failing tests | Two separate prompts: Red, then Green |
|
||
| "Make the tests pass" | Encourages skipping to a green state | "Implement the minimum to make the failing tests pass." |
|
||
| Combining Red + Green in one request | No failing baseline | Always separate the two phases |
|
||
|
||
### Running tests
|
||
|
||
Prefer to use the tests in `docker compose exec graph`
|
||
|
||
```bash
|
||
# Run full suite
|
||
vendor/bin/phpunit
|
||
|
||
# Run with human-readable output
|
||
vendor/bin/phpunit --testdox
|
||
|
||
# Run a single test file
|
||
vendor/bin/phpunit tests/Unit/Service/SvgRendererTest.php
|
||
|
||
# Run tests matching a filter
|
||
vendor/bin/phpunit --filter it_renders
|
||
```
|
||
|
||
On Windows, run via WSL (If Docker Desctop is not):
|
||
|
||
```powershell
|
||
wsl -e bash -c "cd /mnt/g/_DEV/repos/git-contribution-graph && docker compose exec graph vendor/bin/phpunit --testdox 2>&1"
|
||
```
|
||
|
||
### Auto-run hook
|
||
|
||
Add to `.claude/settings.json` to run PHPUnit automatically after every file edit:
|
||
|
||
```json
|
||
{
|
||
"hooks": {
|
||
"PostToolUse": [
|
||
{
|
||
"matcher": "Edit|Write",
|
||
"command": "vendor/bin/phpunit 2>&1 | tail -20"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
## Gitea Workflows
|
||
|
||
Workflow files live in [.gitea/workflows/](.gitea/workflows/). This project uses Gitea Actions (GitHub Actions-compatible syntax).
|
||
|
||
**When editing workflow files:**
|
||
|
||
- Use `gitea.*` context variables, not `github.*` — e.g. `${{ gitea.server_url }}`, `${{ gitea.actor }}`, `${{ gitea.repository }}`
|
||
- `GITHUB_ENV` and `GITHUB_OUTPUT` are still the correct env file names (Gitea Actions re-uses these)
|
||
- Secrets are set under Repository → Settings → Secrets → Actions
|
||
- The registry hostname must be derived from `gitea.server_url` (strip the protocol prefix)
|
||
- Triggers use standard `on:` syntax; `tags: '*.*.*'` matches semver pushes without a `v` prefix
|
||
|
||
**Current workflows:**
|
||
|
||
| File | Trigger | Purpose |
|
||
| -------------------- | ---------------- | --------------------------------------------------------- |
|
||
| `docker-publish.yml` | Push tag `*.*.*` | Build & push multi-arch image to Gitea container registry |
|
||
|
||
## Docker
|
||
|
||
### Development
|
||
|
||
`docker-compose.override.yml` is picked up automatically and targets the `dev` stage (Xdebug + all deps, source mounted).
|
||
|
||
```bash
|
||
# Start dev container (override applied automatically)
|
||
docker compose up -d --build
|
||
|
||
# Shell into the container to run commands
|
||
docker compose exec graph sh
|
||
|
||
# Run tests inside the container
|
||
docker compose exec graph vendor/bin/phpunit
|
||
|
||
# Disable Xdebug for faster test runs
|
||
XDEBUG_MODE=off docker compose up -d
|
||
|
||
# Force cache clear
|
||
docker compose exec graph rm -rf var/cache/*
|
||
|
||
# View logs
|
||
docker compose logs -f graph
|
||
```
|
||
|
||
**Xdebug:** listens on port `9003`. Configure your IDE to accept connections from Docker. On Linux `host.docker.internal` is resolved via `extra_hosts: host-gateway` in the override file.
|
||
|
||
### Production
|
||
|
||
Use only the base compose file to skip the dev override:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.yml up -d --build
|
||
```
|
||
|
||
The `final` stage runs as a non-root `app` user and contains no Composer binary. The build pipeline is:
|
||
|
||
```
|
||
base → deps (composer install --no-dev)
|
||
→ build (copy source + dump-autoload)
|
||
→ final (copy vendor + source, chown app, USER app)
|
||
```
|
||
|
||
There is no `composer.lock` in the repo. If you add or change dependencies, run `composer install` locally and commit the resulting lock file — without it, Docker builds resolve versions fresh each time and may produce inconsistent results.
|
||
|
||
## Architecture
|
||
|
||
Single-controller Symfony app with no database. The request flow:
|
||
|
||
```
|
||
GET /graph.svg?theme=dark|light
|
||
└─ GraphController
|
||
├─ host check (ALLOWED_HOSTS env, optional)
|
||
├─ cache lookup (filesystem, 1h TTL, key = "graph_{theme}")
|
||
│ └─ on miss:
|
||
│ ├─ GitHubProvider → GitHub GraphQL API (contributionCalendar query)
|
||
│ ├─ GitLabProvider → GitLab REST /users/:id/events (paginated, 100/page)
|
||
│ └─ GiteaProvider → Gitea REST /api/v1/users/:user/heatmap
|
||
│ each returns array<string, int> (Y-m-d => count)
|
||
│ failures are caught and logged; remaining providers still render
|
||
│ └─ merge by date (sum counts across providers)
|
||
│ └─ SvgRenderer::render()
|
||
└─ Response: image/svg+xml, Cache-Control: public max-age=3600
|
||
```
|
||
|
||
**Provider activation:** a provider only runs when its env vars are non-empty. GitHub and GitLab require `_USER` + `_TOKEN`; Gitea additionally requires `_URL`. GitLab resolves a numeric user ID from the username via a `/api/v4/users?username=` lookup before fetching events.
|
||
|
||
**SvgRenderer:** builds a 53-column × 7-row grid aligned so the last column always ends on the Saturday of the current week. Five intensity levels (0 contributions → level 0, 1–3 → 1, 4–6 → 2, 7–9 → 3, 10+ → 4) mapped to GitHub's exact colour tokens. No external assets — the SVG is fully self-contained.
|
||
|
||
**Cache:** filesystem adapter (`var/cache/`), mounted as a Docker volume to survive container restarts. Theme is part of the cache key so dark and light are cached independently.
|
||
|
||
## Environment variables
|
||
|
||
| Variable | Required | Notes |
|
||
| ------------------------------------------ | ---------- | -------------------------------------------- |
|
||
| `APP_SECRET` | Yes | 32+ char random string |
|
||
| `GITHUB_USER` / `GITHUB_TOKEN` | For GitHub | Token scope: `read:user` |
|
||
| `GITLAB_USER` / `GITLAB_TOKEN` | For GitLab | Token scopes: `read_user`, `read_api` |
|
||
| `GITLAB_URL` | No | Defaults to `https://gitlab.com` |
|
||
| `GITEA_USER` / `GITEA_TOKEN` / `GITEA_URL` | For Gitea | Token scope: `read:user` |
|
||
| `ALLOWED_HOSTS` | No | Comma-separated hostnames; empty = allow all |
|
||
|
||
Copy `.env` to `.env.local` for local development — `.env.local` is gitignored.
|