Skip to content

Refuse a package that asks a server for more than its targetAbi claims - #367

Open
iderex wants to merge 1 commit into
masterfrom
ci/360-package-abi-floor
Open

Refuse a package that asks a server for more than its targetAbi claims#367
iderex wants to merge 1 commit into
masterfrom
ci/360-package-abi-floor

Conversation

@iderex

@iderex iderex commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #360.

0.2.0.0 shipped asking a 10.11.0 server for five assemblies at 10.11.11.0 while build.yaml claimed targetAbi: "10.11.0.0". A reference above what the host carries does not bind, so that server loads the archive, throws in GetTypes() and reports the plugin NotSupported. Nothing has to be called for it to happen, and every route here was green while it went out: abi-floor.yaml compiles the SOURCE against the floor and says nothing about the artefact, and the two routes that build the artefact never compared it with the claim.

The ruling on this issue was that the package moves to the floor and the claim stands, because moving the claim would drop every server below 10.11.11 in order to accommodate a packaging accident, silently. This is that, plus the refusal that holds it there. The two land together because the check would red the mainline on its own, which the issue body says.

Half one: the package moves to the floor

The net9.0 target compiles against 10.11.0 rather than 10.11.11. That is what claiming that floor already meant, and doing it in the project rather than in the two workflows means the lock file records the graph that ships and every route keeps locked mode.

The reference table before and after, read with the reader this change adds:

dotnet run --project tools/package-abi -- Jellyfin.Plugin.Requests/bin/Release/net9.0/Jellyfin.Plugin.Requests.dll

at f4f65a9, which is what origin/master was when I started:

MediaBrowser.Common	10.11.11.0
MediaBrowser.Model	10.11.11.0
MediaBrowser.Controller	10.11.11.0
Jellyfin.Data	10.11.11.0
Jellyfin.Database.Implementations	10.11.11.0

and on this head:

MediaBrowser.Common	10.11.0.0
MediaBrowser.Model	10.11.0.0
MediaBrowser.Controller	10.11.0.0
Jellyfin.Data	10.11.0.0
Jellyfin.Database.Implementations	10.11.0.0

The net10.0 target is untouched, and that is a measurement rather than an omission. 12.0.0 is not published, the release candidate that line builds against stamps its assemblies 12.0.0.0, and build-jf12.yaml claims 12.0.0.0. So that line already sits at its claim:

MediaBrowser.Common	12.0.0.0
MediaBrowser.Model	12.0.0.0
MediaBrowser.Controller	12.0.0.0
Jellyfin.Data	12.0.0.0
Jellyfin.Database.Implementations	12.0.0.0

Only the 10.11 line was the defect this issue names.

Half two: the refusal

scripts/check-package-abi.sh reads the reference table of every assembly in a built package, through tools/package-abi, and refuses a MediaBrowser.* or Jellyfin.* reference above the targetAbi of the packaging file the package was built from, naming the assembly, its version and the claim. It reaches no network, starts no server and needs no container.

It runs on both routes that produce a package - package.yaml per claimed line, and publish.yaml before the bill of materials and before anything is uploaded, because a release that exists is never touched again and a tag cannot be spent twice.

A set it could not read is refused rather than passed, which is the third done-when. An archive that does not unpack, a package with no assembly in it, an assembly the reader cannot open and an assembly with no host reference at all each print the same nothing as a package within its claim.

Watched refusing

scripts/prove-package-abi-refusals.sh builds the plugin at the floor build.yaml declares and at the next published release of that line, resolved from the feed rather than written down, and drives both through the check. Run on this head:

== the two packages this proof turns on
  build.yaml claims 10.11.0.0, so the floor is 10.11.0, and the next published release of that line is 10.11.2
  building the plugin against 10.11.0 into floor

Der Buildvorgang wurde erfolgreich ausgeführt.
    0 Warnung(en)
    0 Fehler

Verstrichene Zeit 00:00:04.28
  building the plugin against 10.11.2 into above

Der Buildvorgang wurde erfolgreich ausgeführt.
    0 Warnung(en)
    0 Fehler

Verstrichene Zeit 00:00:03.87

== a package built against the floor its packaging file claims
  What /tmp/tmp.v06Mb6gQX0/package-floor asks a server for, against the targetAbi 10.11.0.0 it claims:
    Jellyfin.Plugin.Requests.dll references Jellyfin.Data at 10.11.0.0
    Jellyfin.Plugin.Requests.dll references Jellyfin.Database.Implementations at 10.11.0.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Common at 10.11.0.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Controller at 10.11.0.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Model at 10.11.0.0
  check-package-abi: every host reference is at or below 10.11.0.0, so a server of the claimed floor carries all of them.

== the same source built one published version above that floor
  What /tmp/tmp.v06Mb6gQX0/package-above asks a server for, against the targetAbi 10.11.0.0 it claims:
    Jellyfin.Plugin.Requests.dll references Jellyfin.Data at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references Jellyfin.Database.Implementations at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Common at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Controller at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Model at 10.11.2.0
  check-package-abi: Jellyfin.Plugin.Requests.dll references MediaBrowser.Common at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries MediaBrowser.Common below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: Jellyfin.Plugin.Requests.dll references MediaBrowser.Model at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries MediaBrowser.Model below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: Jellyfin.Plugin.Requests.dll references MediaBrowser.Controller at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries MediaBrowser.Controller below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: Jellyfin.Plugin.Requests.dll references Jellyfin.Data at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries Jellyfin.Data below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: Jellyfin.Plugin.Requests.dll references Jellyfin.Database.Implementations at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries Jellyfin.Database.Implementations below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: this package asks a server for more than its targetAbi claims. Build the package against the floor the packaging file declares, or move the claim - and moving the claim narrows every server this plugin reaches.

== a package built below a claim that sits above it
  What /tmp/tmp.v06Mb6gQX0/package-floor asks a server for, against the targetAbi 12.0.0.0 it claims:
    Jellyfin.Plugin.Requests.dll references Jellyfin.Data at 10.11.0.0
    Jellyfin.Plugin.Requests.dll references Jellyfin.Database.Implementations at 10.11.0.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Common at 10.11.0.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Controller at 10.11.0.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Model at 10.11.0.0
  check-package-abi: every host reference is at or below 12.0.0.0, so a server of the claimed floor carries all of them.

== a build below a claim that a string comparison would call higher
  What /tmp/tmp.v06Mb6gQX0/package-above asks a server for, against the targetAbi 10.11.10.0 it claims:
    Jellyfin.Plugin.Requests.dll references Jellyfin.Data at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references Jellyfin.Database.Implementations at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Common at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Controller at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Model at 10.11.2.0
  check-package-abi: every host reference is at or below 10.11.10.0, so a server of the claimed floor carries all of them.

== the cases where the answer could not be read, which are refusals rather than passes

== an asset that is not an archive
  check-package-abi: /tmp/tmp.v06Mb6gQX0/not-an-archive.zip is not an archive that unpacks, so nothing in it can be read. A server handed this asset cannot install it either.

== a package with no assembly in it
  check-package-abi: /tmp/tmp.v06Mb6gQX0/empty carries no assembly, so it has no reference table to read. A package with nothing in it installs and does nothing, and this check cannot tell that apart from one that is within its claim.

== an assembly the reader cannot open
    package-abi: C:/Users/nils7/AppData/Local/Temp/tmp.v06Mb6gQX0/not-an-assembly/Jellyfin.Plugin.Requests.dll is not an assembly this reader can open: Unknown file format.
  check-package-abi: the reference table of /tmp/tmp.v06Mb6gQX0/not-an-assembly/Jellyfin.Plugin.Requests.dll could not be read, so what this package asks a server for is unknown. An unknown answer is refused rather than passed.

Der Buildvorgang wurde erfolgreich ausgeführt.
    0 Warnung(en)
    0 Fehler

Verstrichene Zeit 00:00:01.04

== an assembly that asks the server for nothing at all
  check-package-abi: no assembly in /tmp/tmp.v06Mb6gQX0/no-host-reference references a Jellyfin or MediaBrowser assembly at all. A plugin that asks the server for nothing is a build that went wrong somewhere earlier, and its reference set cannot be compared with a claim of 10.11.0.0.

== a claim that is not four numeric parts
  check-package-abi: targetAbi '10.11' is not four numeric parts, so there is no version for a reference to be compared against.

== a reference the package answers itself is not a demand on the server
  What /tmp/tmp.v06Mb6gQX0/carries-a-host-name asks a server for, against the targetAbi 10.11.0.0 it claims:
    Jellyfin.Plugin.Requests.dll references Jellyfin.Data at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references Jellyfin.Database.Implementations at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Controller at 10.11.2.0
    Jellyfin.Plugin.Requests.dll references MediaBrowser.Model at 10.11.2.0
  check-package-abi: Jellyfin.Plugin.Requests.dll references MediaBrowser.Model at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries MediaBrowser.Model below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: Jellyfin.Plugin.Requests.dll references MediaBrowser.Controller at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries MediaBrowser.Controller below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: Jellyfin.Plugin.Requests.dll references Jellyfin.Data at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries Jellyfin.Data below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: Jellyfin.Plugin.Requests.dll references Jellyfin.Database.Implementations at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries Jellyfin.Database.Implementations below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
  check-package-abi: this package asks a server for more than its targetAbi claims. Build the package against the floor the packaging file declares, or move the claim - and moving the claim narrows every server this plugin reaches.

prove-package-abi-refusals: every case answered as claimed.

And deleting the refusal turns the first of those green

The done-when asks for that directly. I deleted the block that compares the versions and left everything else, then put it back. The package is the one built one published version above its floor, at 10.11.2:

$ ./scripts/check-package-abi.sh <the 10.11.2 build> 10.11.0.0
check-package-abi: Jellyfin.Plugin.Requests.dll references Jellyfin.Database.Implementations at 10.11.2.0, and this package claims targetAbi 10.11.0.0. A server of the claimed floor carries Jellyfin.Database.Implementations below 10.11.2.0, the reference does not bind, and the server reports the plugin NotSupported after a download that looked like it worked.
check-package-abi: this package asks a server for more than its targetAbi claims. Build the package against the floor the packaging file declares, or move the claim - and moving the claim narrows every server this plugin reaches.
exit=1

with the comparison deleted, the same package and the same claim:

Jellyfin.Plugin.Requests.dll references MediaBrowser.Controller at 10.11.2.0
Jellyfin.Plugin.Requests.dll references MediaBrowser.Model at 10.11.2.0
check-package-abi: every host reference is at or below 10.11.0.0, so a server of the claimed floor carries all of them.
exit=0

It does not only stop refusing, it prints a sentence that is false about the package in front of it, which is the failure the whole check exists against arriving one level up.

$ git diff --stat scripts/check-package-abi.sh
(empty, and the same run afterwards exits 1 again)

The suite, and the three readers this board runs over its own documents

dotnet test Jellyfin.Plugin.Requests.sln --configuration Release -warnaserror
Bestanden!   : Fehler:     0, erfolgreich:   777, übersprungen:     0, gesamt:   777, Dauer: 22 s - Jellyfin.Plugin.Requests.Tests.dll (net9.0)
Bestanden!   : Fehler:     0, erfolgreich:   777, übersprungen:     0, gesamt:   777, Dauer: 22 s - Jellyfin.Plugin.Requests.Tests.dll (net10.0)

777 on both targets, with the plugin compiled against the floor rather than against 10.11.11.

$ ./scripts/check-pasted-evidence.sh
read 118 pasted match lines in 24 document(s)
check-pasted-evidence: every pasted match line reproduces against the file it names.

$ ./scripts/check-negative-disclosures.sh
ran 4 negative disclosure(s) in 24 document(s)
check-negative-disclosures: every negative disclosure exits as the document says it does.

That reader caught something rather than agreeing with me. Widening the comment above JellyfinVersion moved two line numbers docs/storage.md pastes out of the project file, and it named both. They are corrected to what the file now produces rather than deleted.

$ npx --yes prettier@3.6.2 --check CHANGELOG.md README.md docs/compatibility.md docs/bridge.md docs/storage.md
Checking formatting...
All matched files use Prettier code style!

The documents that said this was undecided

README.md, docs/compatibility.md and docs/operating.md each carried a sentence saying which of the two moves is open. The first two now say which way it went and that the answer does not reach a release that already exists; docs/operating.md describes the published release rather than the question, so it is unchanged. None of them claims the published 0.2.0.0 is repaired, because it is not: it carries the references it shipped with until a later release replaces it, which is #152's.

docs/bridge.md read a member list out of "the reference assemblies each target framework compiles against" and named 10.11.11 for net9.0. That sentence would have become untrue, so the page now names the move and carries a reading of the same three members in jellyfin.common 10.11.0, stated as the weaker reading it is - a count of a name in the assembly rather than a signature.

The means

Bash and a small .NET reader, which is the means this repository already uses for a check that judges an artefact: the packaging routes are shell steps, and reading an assembly's metadata tables needs a managed reader, so tools/package-abi is the smallest surface that answers it. It is modelled on tools/full-disk-probe down to the Directory.Build.props that stops inheritance, it references no package at all - System.Reflection.Metadata is in the shared framework - and it prints rather than judges, so the rule lives in one shell script a fixture harness can drive.

What this does not claim

No server was started and nothing was installed. Every reading above is of a file in this repository or of an assembly built from it on this machine. That a package built against the floor now loads on a 10.11.0 server is not measured here; release-install.yaml measures the published release, and the published release is unchanged.

The two new contexts are not required. the package check refuses what it says it refuses is a new check-run name and nothing requires it, for the same reason the six on #30 were not required; a red job here does not by itself hold a merge today.

The proof builds twice and reaches the package feed. It is a job of its own in package.yaml rather than a step in the matrix, so it costs one pair of builds per run rather than one pair per line.

I watched no other guard on this board bite. The suite is named as passing, which is a different statement from a guard watched failing for the reason it names, and only the check this change adds was watched doing that.

Nobody else has read this. No second reader was available tonight, and the transcripts above stand in place of one.

The Scope: line

It named .github/workflows/package.yaml, .github/workflows/publish.yaml and scripts/. The answer on this issue - that the package moves to the floor rather than the claim moving to the package - puts the project file and its lock files under it, and the documents that said the question was open follow from the same answer. I have widened the line on the issue to name what the work touches, the way the line on #30 was widened when an answer put a workflow change under it.

`0.2.0.0` shipped compiled against `10.11.11` while `build.yaml` claimed
`targetAbi: "10.11.0.0"`. An assembly's reference table is stamped with the
versions it was compiled against, a reference above what the host carries does
not bind, and a `10.11.0` server loads the archive, throws in `GetTypes()` and
reports the plugin `NotSupported`. Nothing has to be called for that to happen,
so every route was green while it went out. `abi-floor.yaml` compiles the source
against the floor and says nothing about the artefact; `package.yaml` and
`publish.yaml` built the artefact and never compared it with the claim.

Two halves, and neither works without the other.

The package moves to the floor. The `net9.0` target compiles against `10.11.0`
rather than `10.11.11`, which is what claiming that floor already meant, so the
lock file and every route move with it and none of them loses locked mode. The
`net10.0` target is unchanged: `12.0.0` is not published, the release candidate
it builds against stamps `12.0.0.0`, and that is exactly what
`build-jf12.yaml` claims.

The refusal holds it there. `scripts/check-package-abi.sh` reads the reference
table of every assembly in a built package, through `tools/package-abi`, and
refuses a `MediaBrowser.*` or `Jellyfin.*` reference above the claim, naming the
assembly and both versions. It runs on both routes that produce a package and on
every claimed line. A package it could not read - no assembly, an assembly it
cannot open, no host reference at all - is refused rather than passed, because
those three print the same nothing as a package that is within its claim.

`scripts/prove-package-abi-refusals.sh` builds the plugin at the floor and at
the next published version above it and drives both through the check, beside
the cases where the answer could not be read and the two accepting directions a
careless comparison gets wrong. The transcripts, including the run with the
refusal deleted, are in the pull request body.

Part of #360.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex mentioned this pull request Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refuse a package that asks a server for more than its targetAbi claims

1 participant