Ctrl K
ComunidadGuías…

CONTRIBUTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Contributing to Ferrofin

Thanks for your interest in Ferrofin — a from-scratch Rust implementation of the
Jellyfin server API. This guide covers how to build, test, and submit changes.

Ferrofin is **GPL-3.0-only** (it is a derivative of Jellyfin's GPL-3.0 server crates).
By contributing you agree your work is licensed under the same terms.

## Development setup

Rust workspace, edition 2024, toolchain pinned to **1.99.1** (stable — see
`rust-toolchain.toml`, which rustup honors automatically).

```bash
cargo build --workspace
cargo run -p ferrofin-server -- --data-dir ./data --bind 127.0.0.1 --port 8096
```

On a fresh database the server seeds an admin user with no password (Jellyfin's default),
which the setup wizard at `/web` sets; `FERROFIN_ADMIN_PASSWORD` seeds one instead.
ffmpeg/ffprobe are auto-discovered on `$PATH`; without them only transcoding is disabled.

## Quality gates (CI enforces all of these)

```bash
cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
cargo nextest run --workspace          # + cargo test --workspace --doc for doctests
cargo deny --locked check              # advisories, licenses, bans, sources (deny.toml)
cargo machete apps crates              # declared but unused dependencies
scripts/check-workspace-deps.py        # every dependency through [workspace.dependencies]
```

Every `pub` item needs a `///` doc and pedantic clippy must pass (warnings are
errors in CI). Under `src/`, a module with children is a self-named `foo.rs`
beside `foo/`, never `foo/mod.rs` (clippy's `mod_module_files`). New code needs
tests: Ferrofin ports Jellyfin behavior faithfully, so where upstream ships
xUnit `[Theory]/[InlineData]` cases, transliterate them into `rstest` `#[case]`
tests — the C# expected values are the oracle.

**The parity record is part of done.** A change to a handler or a core path either
adds or updates the parity row it touches, after comparing that code against the
upstream Jellyfin C# at `UPSTREAM_TAG`, or says in the PR why the behaviour it touches
is unchanged. The row is the operation's `handlers::VERIFIED` row, or the
`handlers::VERIFIED_SUBSYSTEMS` row of a shared subsystem the change sits in (query
translation, DTO fields, user data, access filtering, images, paging and sort), which
operation rows reference through `refs`. A re-checked row leaves
`VERIFIED_AGAINST_EARLIER_PIN` (or `VERIFIED_SUBSYSTEMS_AGAINST_EARLIER_PIN`). Live
parity runs and benchmark shape checks are supporting evidence; they never add a row.

**Dependencies.** Declare a dependency once, in the root `[workspace.dependencies]`, and
reference it as `dep.workspace = true`; `scripts/check-workspace-deps.py` refuses an inline
version, and cargo-deny a workspace entry nothing uses. When `cargo deny` reports an
advisory, even one published after your change, upgrade the crate if a fixed version exists
(`cargo update -p <crate> --precise <version>`, or a manifest bump for a new major). An
advisory with no fix, or one that does not reach Ferrofin's use of the crate, goes into
`deny.toml`'s `[advisories] ignore` with its reason, and leaves it once the crate is fixed.
A new license is a compatibility decision against GPL-3.0-only, not a fix.

**Coverage gate — ≥80% line coverage, per crate:**

```bash
crate=ferrofin-core   # the crate to gate
others=$(basename -a crates/*/ | grep -v "^$crate\$" | paste -sd'|')
cargo llvm-cov nextest -p "$crate" --fail-under-lines 80 --summary-only \
  --ignore-filename-regex "(crates/($others)|apps)/"
```

This is CI's form: both pipelines run it per crate from `scripts/coverage-gate.sh`,
split into three parallel shards. `-p` alone still instruments the
crate's workspace dependencies, so a crate whose tests link another crate (e.g.
`ferrofin-wasm`'s scan tests link `ferrofin-core`) would count that crate's uncovered
source in its total; the regex leaves only the crate's own files.

Gate each crate on its own; don't pass multiple `-p` flags to one run (that
checks the merged total and lets a weak crate hide behind a strong one).

The gate reads coverage data with the toolchain's own `llvm-profdata` and `llvm-cov`,
from the `llvm-tools-preview` component that `rust-toolchain.toml` (and `mise.toml`,
if you use mise) installs with the pinned Rust. If you set `LLVM_PROFDATA` or
`LLVM_COV`, point them at the toolchain's copies
(`$(rustc --print sysroot)/lib/rustlib/<host-triple>/bin/`): Rust's LLVM major
version moves with the toolchain, and a system `llvm-profdata` of another major
version fails with "raw profile version mismatch".

**Performance is measured per release, not per PR.** Body-diff correctness is not a
latency signal: a large slowdown can land "green." So every release is benchmarked
before it is tagged, and a regression blocks that release (see [`RELEASE.md`](RELEASE.md)).
A PR does not need a before/after. If it touches `ferrofin-core`, `ferrofin-db`,
`ferrofin-api`, or the query/repository/DTO paths, say so in the description so the
release run can be read against it.

The benchmark suite lives in [`gates/bench/`](gates/bench/README.md): rebuild `ferrofin:bench`, run
`gates/bench/run.sh`, and compare with `python3 gates/bench/report.py --serve`.
`gates/bench/run.sh --servers ferrofin --only loaded` on two builds is the cheap form when
chasing a regression. It is deliberately **not** a CI gate. Budget for the setup if you
run it: it needs `docker`, `k6`, `jq`, `taskset`, `curl` and `python3` (plus `ffmpeg`
with libx264/libx265, `ffprobe` and Python `Pillow` for the test data), the two Jellyfin
images must be pulled, the test data is generated locally and gitignored
(`gates/bench/testdata/build.sh`, ~20 min and ~17 GB), and a full run needs a quiet machine for
about forty minutes. `gates/bench/README.md` has the complete recipe.

> Green tests are necessary, not sufficient. When you touch a data, auth, or
> stateful path, **run the binary and exercise it over real HTTP** — several
> bugs here passed their tests and were only caught by hitting the server.

## Migrations and one-shot repairs

The Helm chart's default is a rolling update, so **a migration or a one-shot `FerrofinMeta`
repair runs while the previous version can still be writing the same database**, until the
new pod is ready ([what that means for operators](charts/ferrofin/README.md#upgrades-and-the-database)).
Nothing serializes the two: Ferrofin takes no lock across processes and sqlx's SQLite
migration lock is a no-op. Only SQLite's own file locks apply. Write for that:

- **A statement sees only the rows that exist when it runs.** The previous version can save
  a row in the old shape a moment later, and a one-shot never comes back for it. Keep the new
  code able to read the old shape, or normalise it on the next save, instead of assuming the
  data is converted. `MediaSegmentManager`'s `legacy_ids` keeps serving and clearing what `0037`
  moved, and scan saves keep doing what `sync_child_parent_names` did once.
- **The previous version's queries keep running against your schema.** Prefer additive changes.
  A dropped or renamed column, or a rebuilt table, fails what the old binary still asks for
  until its pod goes.
- **Keep write transactions short.** The other version waits 30 seconds for the write lock (the
  busy timeout), then its request fails.
- **A repair's transaction that reads before it writes takes the lock first**
  (`begin_with("BEGIN IMMEDIATE")`). A deferred one fails at once with `SQLITE_BUSY_SNAPSHOT`
  if the previous version commits in between, whatever the busy timeout, and fails the boot.
- **Never answer this with a refusal or a process-long lock.** With `maxUnavailable: 0` the old
  pod stays until the new one is ready, so a new pod that waits for the old one to leave never
  starts. Boot-time safety checks need a fallback that cannot wedge a rollout, plus a warning.
- **Say so in the pull request's [upgrade note](#upgrade-notes)** when a migration or repair is
  heavy, or when rows written during the overlap stay wrong, so Kubernetes users know to
  [stop the old pod first](docs/UPGRADING.md#kubernetes-stop-the-old-pod-first-for-heavy-migrations).

Nothing tests two versions running against one database at the same time, so a review is the
only check on this.

## Commits: Conventional Commits + sign-off

Commit messages follow [Conventional Commits](https://www.conventionalcommits.org)
— this drives the version bump (`scripts/version.sh`) and the release notes (`cliff.toml`).
There is no changelog file to edit: the notes are written from the commits and merged PRs
when a release is cut. Examples:

```
feat(sessions): remote-control bus-registered sockets
fix(router): merge repeated query params so array params don't 400
```

Types: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `ci`, `build`, `chore`.
A breaking change adds `!` (`feat!:`) or a `BREAKING CHANGE:` footer.

Sign off every commit (Developer Certificate of Origin):

```bash
git commit -s
```

## Submitting changes

1. Pick the branch by what the change is:
   - **A bug fix for the version already released** (the latest `vX.Y.Z` tag): branch off
     and target `main`. `main` carries the released line and can ship patch releases
     (`vX.Y.Z+1`); its fixes are merged into the open release branch.
   - **A feature, or a fix to a feature that is not released yet:** branch off and target
     the current `release/vX.Y.Z` branch. Only one exists at a time; it becomes the next
     feature release.
2. Keep the change scoped; run the quality gates locally before pushing.
3. Open a pull request against that branch. CI runs fmt/clippy/tests + the coverage gate.
4. If you add or rename an HTTP route, keep the contract-superset test green
   (`crates/ferrofin-api/tests/contract_superset.rs`) — the registered route table
   must remain a superset of the vendored Jellyfin OpenAPI spec.
5. Write the pull request's upgrade note (below).

### Upgrade notes

A pull request does not edit `docs/UPGRADING.md`. It says in its description, on the
**Upgrade note** line of the template, what an operator has to do, or plan for, when they
upgrade to a release that contains it, in a line or two:

- a manual step, such as stopping the old pod first for a heavy migration or repair;
- something that stops working until they act, such as a script that now gets `403`;
- a one-time cost to plan for, such as every client downloading its artwork again.

Otherwise it writes `none`. A change that only behaves differently, with nothing to do, is
`none`: the release notes cover it. When a release is cut, its section of `docs/UPGRADING.md`
is written from these notes.

## Reporting bugs and security issues

Functional bugs: open an issue. Security vulnerabilities: **do not** open a public
issue — follow [SECURITY.md](SECURITY.md).