RANCHER-2997. Security-Patch Versioning Approach — Impact Analysis & Adoption Scope
Summary
Root cause of every issue: a critical/security patch (CSP, e.g.
1.0.1-1) and aSNAPSHOTbuild are both SemVer pre-releases, are indistinguishable to every component, andSNAPSHOToutranks the CSP. The proposal treats a pre-release as a released patch; all SemVer tooling assumes a pre-release is unreleased.The gaps are systemic and surface differently per service:
Conflict A — default exclusion: a CSP is a pre-release, so a stable-only path skips it (plugin default;
preRelease=falsein Okapi/FAR).Conflict B — snapshot outranks CSP: every "latest including pre-release" path returns the CI snapshot over the security patch — plugin
max(), Okapi/FARlatest=N&preRelease=only, and FAR dependency resolution (which forces pre-releases on, so^1.0.2-1resolves to a snapshot).Conflict E — hard upgrade blocker:
mgr-tenant-entitlementsrejects any target not strictly greater than the entitled version, so upgrading an entitledapp-1.0.2-SNAPSHOT.5toapp-1.0.2-1fails as a downgrade, with no override.
The Proposed Scheme
Format <major>.<minor>.<patch>-[critical | SNAPSHOT.build], applied to modules and applications.
Event on | Result | Pre-release identifier(s) |
|---|---|---|
First critical/security fix |
|
|
Second critical fix |
|
|
CI build toward next release |
|
|
Bug fix (release) |
| — |
A CSP is deliberately a pre-release of the next patch, so precedence makes 1.0.1 eventually supersede 1.0.1-1. Ordering on one patch line:
1.0.1-1 < 1.0.1-2 < … < 1.0.1-SNAPSHOT.1 < 1.0.1
(numeric) (alphanumeric) (release)The decisive SemVer rule throughout: numeric pre-release identifiers always sort below alphanumeric ones, so SNAPSHOT beats any critical number on the same x.y.z.
Findings by Component
folio-application-generator (Maven plugin) — semver4j 5.8.0
Area | Behavior | Verdict |
|---|---|---|
Parse / normalize |
| OK — no collision with UI-snapshot normalization |
Compare / order |
| OK — spec-correct |
Constraint resolution |
| Conflict A |
Pre-release filter |
| Conflict B |
Build-number stamping |
| Conflict C (plugin-only) |
Exact pin / ambiguity |
| Conflict D (minor) |
Okapi (folio-registry) — own hand-rolled SemVer
org.folio.okapi.common.SemVer.compareComp implements all four SemVer 2.0.0 pre-release rules (numeric-as-integer; alphanumeric-lexical; numeric always lower than alphanumeric; larger set wins on equal prefix), so ordering is correct: 1.0.1-1 < 1.0.1-2 < 1.0.1-10 < 1.0.1-SNAPSHOT.5 < 1.0.1.
/_/proxy/modulessupportsfilter,latest,order/orderBy=id,preRelease,npmSnapshot.Conflict A/B present:
preRelease=falsehides CSPs;latest=1&preRelease=onlyreturns the snapshot over the CSP.The separate
npmSnapshotaxis does not help —hasNpmSnapshot()means a 3-part version with a 5+ digit patch (UI timestamp snapshots), not-SNAPSHOT.x. So1.0.1-SNAPSHOT.xand1.0.1-1are both justhasPreRelease().
mgr-applications (FAR) — semver4j 5.8.0
The scheme numbers applications too, so app dependencies are ranges like ^1.0.2-1 / ^1.0.2-SNAPSHOT.
GET /applicationssorts SemVer-aware in Java (not SQL) — listing order is correct.Pre-release filter is the same ternary (Conflict A/B):
preRelease=falsehides CSPs;latest=1&preRelease=onlyreturns the snapshot.Dependency resolution forces pre-releases ON:
ApplicationDescriptorsValidationServiceandDependenciesValidatorcallRangesListFactory.create(version, true). So Conflict A does not apply here — but Conflict B is worse:^1.0.2-1matches both1.0.2-1and1.0.2-SNAPSHOT.x, andmax(bySemver())picks the snapshot (since1.0.2-1 < 1.0.2-SNAPSHOT.x). App dependencies resolve to the wrong (snapshot) version.Register/update enforces only exact-ID uniqueness — no downgrade guard here (that lives in entitlements). No DB migration needed (
VARCHAR).
mgr-tenant-entitlements — semver4j (via folio-backend-common)
The entitlement upgrade flow refuses any target not strictly newer than what the tenant runs:
return requestVersion.compareTo(entitledVersion) > 0
? Optional.empty()
: Optional.of(new Parameter().key(applicationId)
.value("Application version is same or lower than entitled"));UpgradeRequestValidator(duplicated inValidatorUtilsfor the declarative/entitlements/statepath).No override —
EntitlementRequestexposes onlyignoreErrors/purge/async.Conflict E: since
1.0.2-1 < 1.0.2-SNAPSHOT.x, upgrading an entitledapp-1.0.2-SNAPSHOT.5to the security patchapp-1.0.2-1is rejected as a downgrade — the CSP cannot be installed at all. (A tenant on a release —1.0.1→1.0.2-1— upgrades fine, since1.0.2-1 > 1.0.1.)
Use Cases
1. Eureka CI builds an application version through the app generator
Flow today. Eureka CI (workflows/scripts in kitfox-github/.github and platform-lsp/.github) invokes the app generator (generateFromJson / updateFromTemplate) as a key step of building or updating an application descriptor. The generator reads a module-version constraint from the template and resolves it to the latest matching version, governed by a preRelease mode carried in the template / branch config (update-config.yml): true (release + pre-release), false (release only), only (pre-release only). These map onto branches — release branches use false, the snapshot branch uses only.
What the new scheme breaks.
On a release branch (
preRelease="false", the standard release path) the generator will not select a CSP — a CSP is a pre-release, so it is treated as non-release and skipped; the security patch never enters a release application. (Probability: high.)With
preRelease="only"a CSP can be returned as "the latest" pre-release, which may be unintended. (Probability: low, but possible.)Independently, Eureka CI's own guard rejects the format:
collect-app-versionvalidates against^[0-9]+\.[0-9]+\.[0-9]+(-SNAPSHOT(\.[0-9]+)?)?$, which does not match a CSP (1.0.1-1/1.0.1-z.1) and fails withINVALID_VERSION_FORMAT.
Why doesn't " just set preRelease=true “ save it? Forcing true everywhere means the constraint can no longer carve a clean band: a range meant to admit only releases (or, on a snapshot app, only snapshots) now also pulls in CSPs, because they are pre-releases. You lose the ability to express "this band, without CSPs / without snapshots."
Consequence. Snapshot applications keep building correctly with high probability (today: 100%); release applications would need their CSP releases produced by hand. To avoid opening that error surface, the change must land in every component that uses SemVer over app or module versions — the app generator, Eureka CI (version-format regex, branch preRelease mapping, check-apps.sh), mgr-applications, mgr-tenant-entitlements, and Okapi.
2. Okapi module listing (/_/proxy/modules, folio-registry)
Flow today. The app generator — and the broader community, and likely Index Data pipelines — query Okapi's module registry with a preRelease filter, e.g.:
https://folio-registry.dev.folio.org/_/proxy/modules?filter=folio_serials-management&latest=100&order=desc&preRelease=true&orderBy=id
What the new scheme breaks. A CSP is a pre-release, so the preRelease filter misclassifies it — hidden by false, out-ranked by a coexisting SNAPSHOT under true/only. The parameter stops working correctly for every consumer of the endpoint. There is no workaround — it is broken by the new semantics.
Fix. Change Okapi itself (hand-rolled SemVer / ModuleVersionFilter), which also partially addresses UC1. Okapi is a deployed service: after the change, folio-registry must be re-released and redeployed — additional effort.
Pre-existing UI-snapshot complication. FOLIO UI modules carry a non-SemVer numeric form, e.g. 1.1.0.10000005564, which strict SemVer cannot parse. Both Okapi and the app generator already pre-process these into a numeric pre-release (e.g. 1.1.0-10000005564) before handling. This is a years-deep legacy — the existing artifacts cannot be renumbered. The CSP fix must keep these UI snapshots distinct from CSPs so the new numbering does not collide.
3. FAR application listing (/applications, mgr-applications)
Flow today. The same pattern one level up, for applications, served by mgr-applications (FAR), e.g.:
https://far.ci.folio.org/applications?query=name=app-requests-mediated&preRelease=false&orderBy=version&order=desc&limit=10
check-apps.sh (platform-lsp) already drives this endpoint (…&preRelease=only&latest=1) and trusts FAR to return the correct latest.
What the new scheme breaks. Identical to UC2 — the preRelease parameter misclassifies CSP application versions and stops working correctly; no workaround. Fix: change mgr-applications and redeploy far.ci. The one relief vs. UC2: there is no UI-module legacy at the application level.
4. Promote a tested CSP onto a tenant (mgr-tenant-entitlements)
Flow today. To validate a CSP before release, an operator first builds a snapshot of that CSP and installs it on a test environment — e.g., app-platform-complete-1.0.1-1.SNAPSHOT.x — then upgrades the tenant to the real CSP release app-platform-complete-1.0.1-1.
What the new scheme breaks. The snapshot-of-the-CSP carries more pre-release fields than the CSP, so 1.0.1-1.SNAPSHOT.x outranks 1.0.1-1. The entitlement upgrade therefore sees the target as lower-or-equal and rejects it:
return requestVersion.compareTo(entitledVersion) > 0
? Optional.empty()
: Optional.of(new Parameter().key(applicationId)
.value("Application version is same or lower than entitled"));
This guard lives in mgr-tenant-entitlements and has no bypass (verified in source). The only way to move the test tenant from the CSP snapshot to the CSP release is to change this module's code.
What Adopting the Scheme Means
The version format is valid and orders correctly everywhere (see above). The problem is operational: with the code as it stands today, adopting the scheme does not deliver its goal — "ship a security patch on any release line." End to end:
A published CSP (
1.0.1-1) is invisible to default resolution — the app generator resolves^1.0.0to the release and silently skips the patch.Wherever pre-releases are turned on to surface the CSP, a
SNAPSHOToutranks it and is picked instead (pluginmax(), Okapi/FARlatest, FAR dependency resolution).The entitlement upgrade hard-rejects
SNAPSHOT.x → CSPas a downgrade.
So the scheme is format-correct but operationally blocked by the current code.
Can It Be Mitigated Without Code Changes?
Component | No-code workaround | Verdict |
|---|---|---|
Plugin (app generator) | Pin the exact CSP ( | Possible but unscalable and error-prone — defeats the automation |
Okapi / FAR (mgr-applications) | Query with explicit flags and hand-pick the CSP over the snapshot; every downstream consumer must do the same | Possible but manual at every call site |
mgr-tenant-entitlements | None. The upgrade validator is a mandatory pre-flight stage; no | Hard wall — a tenant cannot be patched without a code change |
Bottom line: "do nothing and work around it by hand" is not a real option. The plugin/registry workarounds are manual toil; the entitlement path has no override at all (verified in source — see Appendix). Manual mitigation is painful where it exists and impossible where it matters most.
Change the code across every affected component
The only viable adoption is a coordinated change across every component that handles SemVer over app or module versions, so they classify a pre-release as CSP vs. SNAPSHOT and behave consistently wherever a version is selected or validated:
folio-application-generator — classifier in
SemverUtils; selection policy inModuleVersionService(default = releases + CSP, exclude SNAPSHOT); extendPreReleaseFilter; stamping guard inApplicationDescriptorUpdateService.
# | Change | Location | Effort |
|---|---|---|---|
1 | Pre-release classifier ( |
| S |
2 | Selection policy: default = releases + CSP, exclude snapshots (per Decision 1) |
| M |
3 | Filter model: third axis or classifier-driven |
| S–M |
4 | Guard stamping so only |
| S |
Eureka CI (
kitfox-github/.github,platform-lsp/.github) — widen the version-format regex incollect-app-version; revisit the branch→preReleasemapping inupdate-config.yml; adjust thecheck-apps.shFAR query and equality check; CSP-aware bump logic inrelease-preparation-flow.Okapi — distinguish
-SNAPSHOT.xfrom CSP inModuleVersionFilter/ModuleUtil.filter; optionallatestpreference for the shipped patch over a snapshot; redeploy folio-registry.
# | Change | Location | Effort |
|---|---|---|---|
1 | Distinguish |
| M |
2 | Optional |
| M |
— | Infra: redeploy folio-registry; coordinate | — | infra |
mgr-applications (FAR) — CSP-vs-snapshot selection (
ApplicationDescriptorsValidationService,DependenciesValidator); optional filter dimension onGET /applications; redeploy far.ci (no DB migration).
# | Change | Location | Effort |
|---|---|---|---|
1 | Classifier + CSP-vs-snapshot selection so |
| M |
2 | Optional snapshot-vs-CSP filter dimension on |
| M |
— | Infra: redeploy far.ci | — | infra |
mgr-tenant-entitlements — make the upgrade guard CSP-aware (a CSP / its release must not be blocked by an installed snapshot of the same target) or add an explicit override; fix
UpgradeRequestValidator+ the duplicatedValidatorUtils.
# | Change | Location | Effort |
|---|---|---|---|
1 | Make the upgrade guard CSP-aware (a CSP must not be blocked by an installed snapshot of the same/lower target), or add an explicit override |
|