-
-
Notifications
You must be signed in to change notification settings - Fork 0
Releasing
This is the policy the publish workflows and the changelog point at. It defines the three-part version scheme, the two release channels, the beta soak (the formal canary stage a build passes through before a stable tag is cut), and the manual E2E QA checklist (§6) that soak requires — the behaviours CI cannot exercise, verified by hand against the soaked beta before promotion.
For an authentication plugin a bad stable release is a mass-lockout event (see Rollback). The soak stage is the availability control that sits before rollback: it puts every stable candidate in front of real, opt-in beta installs for a defined window so a load-time or login-path regression is caught on beta testers, not on the whole fleet.
Versions are three-part X.Y.Z, class-encoded:
- X — breaking (Jellyfin ABI / platform), Y — feature, Z — bug-fix or security (the two share the digit and differ by release cadence, not by number).
The maintainer sets the next release target X.Y.Z in build.yaml
(build-jf12.yaml for the JF12 line) per the change class of what has landed —
a bug-fix or security patch bumps Z, a feature bumps Y, a breaking change
bumps X. That target is what the beta builds toward and what the matching
-stable tag releases; the publish job checks the tag's numeric prefix equals
build.yaml's version.
The channel and Jellyfin generation live only in the git tag / GitHub release
name as a suffix (-stable, -beta.<run>, -JF12-beta, -JF12-stable), never
in the installed numeric version — Jellyfin plugin versions must be numeric. A beta
is built as X.Y.Z.<run>, where the fourth octet is only the workflow run number
that keeps the tag unique and the update order monotonic; a stable is X.Y.Z.
Jellyfin has no in-manifest channel flag, so the channels are separate manifest branches:
| Channel | Manifest branch | ignorePrereleases |
Trigger |
|---|---|---|---|
| beta | manifest-beta |
false |
Daily scheduler (nightly-betas.yml) |
| stable | manifest-release |
true |
A maintainer-pushed -stable tag |
-
Beta is minted by
publish-beta.yml(JF10.11, frommain) andpublish-jf12-beta.yml(JF12, from4.2), which the dailynightly-betas.ymlscheduler dispatches at 04:00 UTC. Neither publish workflow has a push trigger — a beta is not minted on every merge tomain(that spammed one release per merge; #632). Each leg has a gate job that skips the build unless its branch advanced since that lineage's last beta, so an unchanged day mints nothing. The beta version is the next targetX.Y.Zwith the workflow run number as the fourth octet (X.Y.Z.<run>), released as the prereleaseX.Y.Z-beta. -
Stable is cut only by a maintainer pushing a tag by hand —
X.Y.Z-stable(publish.yml, JF10.11) orX.Y.Z-JF12-stable(publish-jf12-stable.yml, the 5.0 line, dormant until Jellyfin 12.0 GA). The tag push is protected-branch / version-tag gated byguard-git.ps1. There is no automatic promotion from beta to stable — the promotion gate below is a human gate, and the stable tag is the act of clearing it.
Releases are immutable (issue #146): assets are sealed on publish and tags are single-use. That is why the beta job uses a draft → attach → publish flow and why a burned tag can never be re-cut.
Each stable version climbs this ladder before it reaches the stable manifest. This is a per-release ladder (distinct from the product-wide maturity ladder on the Roadmap — In-Development → Alpha → Beta → Release Candidate → Full Release — which describes the plugin as a whole; both have a rung called "Beta", so this ladder is named the promotion ladder to keep them apart).
| Rung | Where it lives | Audience | What it means |
|---|---|---|---|
| Unreleased |
main HEAD (or 4.2 for JF12) |
CI only | Merged, green, but not yet built for install. |
| Beta (canary soak) |
manifest-beta, X.Y.Z-beta
|
Opt-in beta installers | Installable prerelease of the exact commit a stable would tag. |
| Stable |
manifest-release, X.Y.Z-stable
|
Everyone on the stable URL | Promoted after clearing the soak + the promotion gate. |
The beta build is the canary. It is a real installable build of the same
commit a stable tag would point at, served automatically to everyone on the beta
repository URL. Promoting it to stable is publishing the identical source under a
-stable tag; the soak is the observation window in between.
Default N = 7 calendar days. A stable candidate must have been published as
the newest beta on manifest-beta, unchanged, for at least N days with no
regression report filed against it before its -stable tag is pushed.
Definition and rules:
-
Newest-and-unchanged. The soak clock is on the specific commit the stable
tag will point at. To run the soak, ensure the current newest beta is built
from that commit (dispatch
publish-beta.ymlifmainhas moved past the last daily beta), then holdmain— merge only fixes that are themselves stable-blockers. - A new beta resets the clock. If any commit lands during the window (a soak fix or otherwise), a new beta supersedes the candidate and the N-day clock restarts on the new commit. You cannot promote a commit older than the newest soaked beta.
- "No regression report" means no open bug attributing a fault to that beta (or a newer beta) — a load failure, a broken login path, or any behaviour the previous stable did not have. Pre-existing, unrelated open issues do not block.
- Install count is not measured. This plugin ships no telemetry by design (compliance/privacy posture), so the gate is time-plus-evidence, not an install counter: the N-day window plus the manual E2E checklist (§6) stand in for a measured install threshold. N is a floor the maintainer may lengthen for a risky change; §7 is the only rule that shortens it.
All of the following must hold before pushing an X.Y.Z-stable (or
X.Y.Z-JF12-stable) tag. This is the checklist /release-prep runs; it composes
the soak window (§4) and the manual E2E QA checklist (§6).
- Soak satisfied. The candidate commit's beta has been the newest
manifest-betabuild for ≥ N days (default 7) with no regression report against it (§4) — or §7's security-shortening rule is invoked with a written reason. - Manual E2E checklist passed against the beta build on a real server: every item in the manual E2E QA checklist (§6 — real-IdP OIDC/SAML login, proxy attribution, packaging/install/upgrade, dashboard rendering). These are exactly the behaviours CI cannot exercise, so the soak is where they are verified.
- Rollback path confirmed. The pre-publish downgrade smoke check in
Rollback §7
passes: the new manifest retains prior versions and a downgrade to the
previous stable still loads and logs in without secret re-entry (or, if a
format boundary is crossed, Rollback §3 and
CHANGELOG.mdare updated). - Version matches the tag.
build.yaml(build-jf12.yamlfor JF12)versionequals the tag's numericX.Y.Zprefix (the publish job also enforces this) and the digit bumped matches the change class (X/Y/Z). - Changelog updated.
CHANGELOG.mdhas the release's entry.
Clearing every box is the precondition for the tag push; the tag push is the promotion. Nothing promotes a beta automatically.
The testability program (#192) makes every behaviour that can be automated an
automated test (unit, in-process endpoint, and full round-trip suites in
SSO-Auth.Tests). The items below are the residue that genuinely cannot be
automated in-repo — they depend on a real Jellyfin host, a real identity
provider, a browser DOM, or live packaging/install. Per #192's acceptance
("every behaviour automated-tested or an explicit release-QA item"), they are
enumerated here as explicit manual checks, each with the reason it stays manual.
Run these against a release candidate before publishing a stable. They are the manual E2E gate the beta soak requires: a candidate clears the promotion gate (§5) only once this checklist passes against its soaked beta build.
- OIDC login against a real provider (Keycloak / Authelia / authentik):
challenge → callback → login succeeds; roles map; PKCE and nonce are
honoured.
Why manual: the in-repo
OidcRoundTripTestsdrive a self-consistent fake IdP; real providers have discovery/JWKS/token quirks the fake cannot model. - SAML login against a real IdP: signed assertion accepted; secondary certificate rollover works; role gating applies. Why manual: same as above — a real IdP's signing, clock, and metadata behaviour is outside the plugin.
- Reverse-proxy forwarded-header / rate-limit attribution: behind a proxy
with
KnownProxiesconfigured, the client IP the rate limiter buckets on is the real client, not the proxy; throttling still fails open for unattributable clients. Why manual: depends on the proxy and network config, not plugin code (the gate's fail-open logic itself is unit-tested inSsoRateLimitGateTests). - Packaging / install / upgrade on a live server: the JPRM artifact
installs; an upgrade preserves config; secret-at-rest migration (#158)
re-wraps legacy plaintext secrets on first load; KEK loss fails closed
(#550) rather than logging users in without protection.
Why manual: install-time filesystem/permission and process-restart
behaviour (the serializer-level secret migration is covered by
ConfigXmlLifecycleTests).
- jellyfin-web dashboard rendering: the config page and the account-link page render inside the Jellyfin dashboard — tabs, the folder checkbox rebuild (#221), and the insecure-toggle warnings (#140) all display. Why manual: DOM rendering inside the host web app.
- Config-page JS behaviour: DOM-XSS hardening holds; the link / unlink round-trip works in the browser; a successful link shows success, not the login-failure text (see #614). Why manual: browser-side JavaScript/DOM behaviour.
- View-asset
If-None-Match→ 304 conditional revalidation (#253): a second request with the servedETaggets a 304. Why manual: the ETag the action emits is unit-tested (SSOViewsControllerTests), but the conditional-GET negotiation is ASP.NET middleware, not plugin code.
A fix for an exploitable vulnerability or an active mass-lockout regression may shorten N — down to zero — because shipping the fix fast outweighs a full soak. When N is shortened:
- Record the reason in the release notes and
CHANGELOG.md(a security fix shares theZdigit with a bug-fix, so the entry is what documents the class). - Still run the §5 manual E2E checklist and the Rollback §7 downgrade smoke — those guard against the fix itself causing a mass-lockout, which the soak would otherwise have caught. The soak time is what shortens, not the E2E gate.
- Prefer fix-forward (Rollback §6): a higher patch version pulls the fleet forward. A shortened soak is the release-side counterpart to a fix-forward.
The soak lowers the probability that a stable needs rollback; it does not remove the need for one. The two are complementary controls:
- Before promotion: the soak (this document) is the preventive gate.
-
After a bad promotion: Rollback is the containment —
fix-forward with a higher patch version, or de-list the bad version from
manifest-release, never delete the immutable release or reuse its tag.
The Rollback §7 downgrade smoke is shared by both: it is a promotion-gate item here and the rollback runbook's pre-publish check, so the rollback path is proven to work on the same candidate the soak just cleared.
Issue #146 (done) replaced the old mutable rolling nightly tag with the
immutable daily beta channel used here: the scheduler mints a dated,
single-use X.Y.Z-beta.<run> release into manifest-beta instead of moving one
tag. This soak policy is defined on that redesigned channel — the canary is the
daily beta, not a rolling nightly tag. The JF12 line (publish-jf12-beta.yml →
X.Y.Z-JF12-beta, shared manifest-beta) soaks the same way; its stable leg
stays dormant until Jellyfin 12.0 GA, at which point a X.Y.Z-JF12-stable tag
clears this same gate.
The repository maintains two parallel release lines, and they reach Release Candidate independently — the JF12 line does not ride the 4.x RC:
-
The 4.x line (JF10.11 / .NET 9) reaches RC now. It clears the standard
Beta→RC gates (Roadmap), including a full live end-to-end pass
against a real Jellyfin 10.11 server (#717);
docs/E2E-CHECKLIST.mdtargets 10.11. - The 5.0 line (JF12 / .NET 10) has its own, separate RC gate. It cannot clear the 4.x RC's 10.11 live gate, and it must not be assumed RC-ready just because a daily JF12 beta builds green. Its live-validation gate is deferred until a JF12 server exists to run it against.
JF12 live-validation trigger. When a Jellyfin 12.0 RC or GA server is
available, run the E2E checklist (a JF12 variant of docs/E2E-CHECKLIST.md)
against it, with specific attention to:
- the framework-provided crypto assemblies that must NOT ship in the JF12
zip on .NET 10 (the plugin bundles the JWT-validation closure only where the
host does not provide it — on .NET 10 Jellyfin ships those, so shipping them
would duplicate/clash; see the
build-jf12.yamlartifact comment); and - legacy-auth-enforcement login end to end (#136), the highest mass-lockout-risk path on a first JF12 upgrade wave.
Until that live pass exists, the JF12 beta is unvalidated against a live
JF12 server and its channel notes say so — the daily JF12 beta is a build
artifact for early adopters, not a Release Candidate. Only after the JF12 live
pass and its own promotion gate does a X.Y.Z-JF12-stable tag cut the 5.0
stable line.
Repository · Issues · Releases · Security policy — report vulnerabilities privately, never in a public issue. Pages describe what is implemented today; if the wiki disagrees with the code, the code wins.
Getting started
- Installation
- Provider Setup
- Hardening & Options Reference
- Migrating from 9p4
- Troubleshooting
- Rollback
How it works
Security
- Security Model
- Security Conformance (ASVS / RFC 9700)
- Threat Model
- SSO-Only Login — design record
- Single Logout — design record
Standards & process (internal / maintainer)