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.
- 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.
- 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 testpasses at 100% - nomarkTestSkipped()ormarkTestIncomplete(). - Edge cases covered:
null, empty strings, unicode, boundaries.
- If you touched an Entity: ran
make migrations-diffand committed the generated file. - Reviewed the SQL in
migrations/Version*.php- especiallyDROPandRENAMEoperations (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.
-
#[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).
- Tested in a real browser: happy path + 1–2 edge cases.
- Turbo-safe if there's inline JS:
- IIFE wrapper
(function(){ ... })() varinstead ofconst/let.then()instead ofasync/await- Declarative
data-bs-togglefor modals (nevernew bootstrap.Modal()) - Helpers
bindDoc(event, key, handler)for global listeners window._prismarr*Timer+turbo:before-rendercleanup for polling
- IIFE wrapper
- User-facing error messages go through the translator (
trans()), in plain language, no technical jargon. - Any dynamic value spliced into an
innerHTMLtemplate runs throughwindow.escHtml()(escapes& < > " '). - Dark and light theme both checked.
- Responsive verified on at least one mobile viewport.
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 callwithInstance($i)->method()per loop iteration. - Items present on multiple instances are deduped by
tmdbId(movies) ortvdbIdthentmdbId(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
UIDs and any persistent identifier surfaced to the user must be rooted ontmdbId/tvdbId, not on the per-instance internal Radarr/Sonarrid, so the same item stays stable across instance swaps.
- Commit message is clear and self-contained (one or two lines describing the change and its motivation).
-
README.mdupdated if the feature changes user-facing behaviour. -
CHANGELOG.mdupdated under[Unreleased]with a one-liner entry. -
git pushimmediately after the commit (no local-only commits).
- 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.
Feature works → tests green → migration reviewed → security checked → UI tested → docs updated → commit message in English or French → push.
make checkRuns 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.
This document is not frozen. As the project grows (new tooling, new types of tests, new security concerns), amend it:
- Propose the change in a PR, motivated by a real failure or a new risk.
- Update
CONTRIBUTING.md+Makefile+.github/workflows/ci.ymltogether. - Announce the change in
CHANGELOG.mdunder a### Contributorheading so existing contributors notice it. - Do not quietly lower the bar - always raise it. If a check becomes redundant, replace it with something stronger, not nothing.
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.
-
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 thesettingDB table. -
No modified migrations after push. Once a
migrations/Version*.phpis onmain, it is immutable. To fix a bad schema, write a NEW migration. Editing a released migration corrupts every user's database on upgrade. -
No destructive or breaking changes for production users. Covers: renaming a public env var, renaming a
settingDB key (users lose their config), changing the default port, breaking the.env.local/prismarr.dbformat without a migration path, removing a feature without a deprecation cycle, hard-deleting entities instead of soft-deleting. Ifdocker compose pullcan break an existing install, the change needs a documented migration plan or must be rethought. -
make checkmust 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. -
No silent weakening of existing security checks. CSP, CSRF, rate-limiter login, SSRF guard, ProfilerGuard, CURLOPT_REDIR_PROTOCOLS,
ROLE_ADMINattributes - 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.