# Contributing to Prismarr > Definition of Done - every feature, fix or refactor must pass this checklist > before being considered complete and pushed to `main`. Prismarr is a self-hosted app published publicly on Docker Hub. Quality and security directly affect real users who run this on their homelab. Respect the process - "we'll fix it later" becomes a user-reported bug in v1.x. --- ## ๐Ÿ”ง 1. Code - [ ] The feature works end-to-end, tested manually in a running container. - [ ] No leftover `console.log`, `dd()`, `var_dump`, `dump()`, `TODO`, `FIXME`. - [ ] Comments are written in **English**. User-visible strings go through the i18n layer (`messages+intl-icu.{en,fr}.yaml`), with English as the source of truth. - [ ] **Zero credentials** in code, commits, logs, docs (sacred rule). - [ ] No unused `use` / imports. ## ๐Ÿงช 2. Tests - [ ] At least **one unit test** for any new business logic. - [ ] For a bug fix: **a regression test that first reproduces the bug**, then the fix turns it green. - [ ] `make test` passes at 100% - no `markTestSkipped()` or `markTestIncomplete()`. - [ ] Edge cases covered: `null`, empty strings, unicode, boundaries. ## ๐Ÿ’พ 3. Schema / Migrations - [ ] If you touched an Entity: ran `make migrations-diff` and committed the generated file. - [ ] Reviewed the SQL in `migrations/Version*.php` - especially `DROP` and `RENAME` operations (they lose data). - [ ] Tested with a clean DB: `docker compose down -v && make restart`. - [ ] **Never modify a migration once pushed to `main`.** If a released migration is wrong, create a new migration that corrects it. ## ๐Ÿ”’ 4. Security - [ ] `#[IsGranted('ROLE_ADMIN')]` on any destructive action (delete, reset, bulk operations, config changes). - [ ] `$e->getMessage()` is never leaked in a JSON response - log it internally and return a generic message. - [ ] All user input is validated (email format, URL scheme, length, type). - [ ] No concatenated SQL - use Doctrine parameter binding. - [ ] CSRF token on every sensitive form. - [ ] If you fetch a user-supplied URL: SSRF guard (protocol whitelist + `CURLOPT_REDIR_PROTOCOLS`). ## ๐ŸŽจ 5. UI / UX - [ ] Tested in a real browser: happy path + 1โ€“2 edge cases. - [ ] Turbo-safe if there's inline JS: - IIFE wrapper `(function(){ ... })()` - `var` instead of `const`/`let` - `.then()` instead of `async/await` - Declarative `data-bs-toggle` for modals (never `new bootstrap.Modal()`) - Helpers `bindDoc(event, key, handler)` for global listeners - `window._prismarr*Timer` + `turbo:before-render` cleanup for polling - [ ] User-facing error messages go through the translator (`trans()`), in plain language, no technical jargon. - [ ] Any dynamic value spliced into an `innerHTML` template runs through `window.escHtml()` (escapes `& < > " '`). - [ ] Dark **and** light theme both checked. - [ ] Responsive verified on at least one mobile viewport. ### 5.1 Multi-instance Radarr / Sonarr Since v1.1.0, `RadarrClient` and `SonarrClient` are autowired *unbound* โ€” they only point at a real instance once `bindInstance($i)` (mutating, used by `MultiInstanceBinderSubscriber` for slug-aware routes) or `withInstance($i)` (immutable clone, used in dashboard / calendar / search where the same request iterates over every instance) has been called. - [ ] Inside a slug-aware route (`/medias/{slug}/โ€ฆ`), the autowired client is already bound โ€” call its methods directly. The subscriber 404s unknown slugs before the controller runs. - [ ] Outside a slug-aware route, **never** call the autowired client directly: it points at whichever instance was last bound and is effectively undefined. Iterate `ServiceInstanceProvider::getEnabled()` and call `withInstance($i)->method()` per loop iteration. - [ ] Items present on multiple instances are deduped by `tmdbId` (movies) or `tvdbId` then `tmdbId` (series) โ€” same key Phase D uses everywhere (dashboard, calendar, search, qBit resolver). - [ ] If the data needs to know which instance it came from (badge, "open in Radarr" link, owner status) carry the slug alongside the record โ€” the slug is the primary key the frontend uses to round-trip. - [ ] iCal `UID`s and any persistent identifier surfaced to the user must be rooted on `tmdbId` / `tvdbId`, not on the per-instance internal Radarr/Sonarr `id`, so the same item stays stable across instance swaps. ## ๐Ÿ“š 6. Docs & Git - [ ] Commit message is clear and self-contained (one or two lines describing the change and its motivation). - [ ] `README.md` updated if the feature changes user-facing behaviour. - [ ] `CHANGELOG.md` updated under `[Unreleased]` with a one-liner entry. - [ ] `git push` immediately after the commit (no local-only commits). --- ## When you hit any of these situations, stop - You catch yourself saying *"I'll add the test later"* โ†’ **no**, add it now. - You want to tweak an already-pushed migration โ†’ **no**, write a new one. - A user-supplied input is going into a URL request โ†’ **no**, SSRF-guard it. - A JSON error response is about to contain an exception message โ†’ **no**, strip it and log internally. --- ## The workflow in one sentence > Feature works โ†’ tests green โ†’ migration reviewed โ†’ security checked โ†’ > UI tested โ†’ docs updated โ†’ commit message in English or French โ†’ push. --- ## The single command before every commit ```bash make check ``` Runs PHP lint + Twig lint + full PHPUnit suite in ~2 seconds. If it fails, do not commit. Fix first. A GitHub Actions CI workflow runs `make check` on every PR and blocks merging if it fails (`.github/workflows/ci.yml`) - the same contract, enforced. --- ## Evolving this workflow This document is not frozen. As the project grows (new tooling, new types of tests, new security concerns), amend it: 1. Propose the change in a PR, motivated by a real failure or a new risk. 2. Update `CONTRIBUTING.md` + `Makefile` + `.github/workflows/ci.yml` together. 3. Announce the change in `CHANGELOG.md` under a `### Contributor` heading so existing contributors notice it. 4. Do not quietly lower the bar - always raise it. If a check becomes redundant, replace it with something stronger, not nothing. --- ## ๐Ÿ”’ Absolute golden rules (non-negotiable, no exceptions) These five rules override every other concern, including urgency. Breaking them is what kills a public project. If a contribution requires violating any of them, reject it and propose an alternative. 1. **No credentials** in code, commits, logs, docs, or messages. Prismarr's repo is public; a leaked secret is exploited by a bot within minutes. Credentials live in `.env.local` (gitignored) or the `setting` DB table. 2. **No modified migrations after push.** Once a `migrations/Version*.php` is on `main`, it is immutable. To fix a bad schema, write a NEW migration. Editing a released migration corrupts every user's database on upgrade. 3. **No destructive or breaking changes for production users.** Covers: renaming a public env var, renaming a `setting` DB key (users lose their config), changing the default port, breaking the `.env.local` / `prismarr.db` format without a migration path, removing a feature without a deprecation cycle, hard-deleting entities instead of soft-deleting. **If `docker compose pull` can break an existing install, the change needs a documented migration plan or must be rethought.** 4. **`make check` must be green before every commit.** Runs lint + full PHPUnit suite. Completes the Definition of Done checklist above - no test for new logic = no commit. No regression test for a bug fix = no commit. 5. **No silent weakening of existing security checks.** CSP, CSRF, rate-limiter login, SSRF guard, ProfilerGuard, CURLOPT_REDIR_PROTOCOLS, `ROLE_ADMIN` attributes - each one has a documented reason (see commit history for Sessions 7aโ€“7d + 8a). If a check blocks a legitimate feature, refine it (e.g. extend the CSP with justification) - do not disable it. Violating any of these, even if `make check` passes, makes the commit invalid.