Skip to content

Releasing

iderex edited this page Jul 20, 2026 · 1 revision

Releasing — versioning, the beta soak, and the pre-promotion QA gate

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.


1. Versioning

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.

2. Channels and how a release is triggered

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, from main) and publish-jf12-beta.yml (JF12, from 4.2), which the daily nightly-betas.yml scheduler dispatches at 04:00 UTC. Neither publish workflow has a push trigger — a beta is not minted on every merge to main (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 target X.Y.Z with the workflow run number as the fourth octet (X.Y.Z.<run>), released as the prerelease X.Y.Z-beta.
  • Stable is cut only by a maintainer pushing a tag by handX.Y.Z-stable (publish.yml, JF10.11) or X.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 by guard-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.

3. The release promotion ladder

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 RoadmapIn-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.

4. The soak window (N)

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.yml if main has moved past the last daily beta), then hold main — 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.

5. The promotion gate

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-beta build 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.md are updated).
  • Version matches the tag. build.yaml (build-jf12.yaml for JF12) version equals the tag's numeric X.Y.Z prefix (the publish job also enforces this) and the digit bumped matches the change class (X / Y / Z).
  • Changelog updated. CHANGELOG.md has 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.

6. The manual E2E QA checklist

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.

Federation against a real IdP

  • 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 OidcRoundTripTests drive 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.

Deployment topology

  • Reverse-proxy forwarded-header / rate-limit attribution: behind a proxy with KnownProxies configured, 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 in SsoRateLimitGateTests).
  • 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).

Browser / dashboard

  • 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 served ETag gets 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.

7. Security-patch shortening

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 the Z digit 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.

8. Rollback tie-in

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.

9. Coordination with the immutable-releases / nightly redesign (#146)

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.ymlX.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.

10. RC scoping of the two ABI lines (#743)

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.md targets 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.yaml artifact 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.

Clone this wiki locally