Skip to content

Refuse a run that would lose a plugin it could not order - #125

Merged
iderex merged 1 commit into
mainfrom
work/af10-undated-releases
Aug 22, 2026
Merged

Refuse a run that would lose a plugin it could not order#125
iderex merged 1 commit into
mainfrom
work/af10-undated-releases

Conversation

@iderex

@iderex iderex commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes #114.

internal/posture ordered a plugin's releases by publication time and treated
index 0 as the release the run exists to publish. Where no release in the set
carried a time, none of newerFirst's clauses fired, the order fell through to
the tag, and the highest tag arrived at index 0. That is the string
decisions/manifest-schema.md refuses as a version answering the question the
publication time exists to answer, and a defect in that release stopped the whole
run over a published release nobody here can repair.

#114 carries the decision this implements, taken on the issue on 2026-08-21: in
such a set no release is classified as the newest, and the second rule lands with
it rather than after it.

What changed

One term in the classification. A release that cannot be placed in time is not
the newest however far up the order it landed, so in a set where nothing carries
a time every defect is a named skip and nothing stops.

newest := i == 0 && !ordered[i].Published.IsZero()

That alone moves the failure rather than removing it. The plugin resolves,
contributes nothing, and the catalogue is one entry shorter with a zero exit,
which decisions/failure-posture.md spends its longest section on being the
worse of the two. So catalogue.JudgeDropped lands in the same change and
refuses a run in which a plugin that resolved produces no entry and nothing
stops. Route.Publish asks it after posture.Judge and before Judge, so a
stop keeps its own more specific reason.

decisions/failure-posture.md places both: the unorderable set under what is a
loud skip, and the dropped plugin under what is fatal.

Both guards were watched refusing

Putting the classification term back to what it was:

-		newest := i == 0 && !ordered[i].Published.IsZero()
+		newest := i == 0

go test ./internal/posture -run 'TestNoReleaseInTheSetCarriesAPublicationTime|TestOneDatedReleaseInTheSetIsStillTheNewest' -count=1
--- FAIL: TestNoReleaseInTheSetCarriesAPublicationTime (0.00s)
    posture_test.go:224: a release nothing could place in time stopped the run: [a-plugin 2.0.0-stable: no-usable-sidecar: no asset's contents name a-plugin_2.0.0.0.zip]
FAIL
FAIL	flowfin.dev/hub/internal/posture	0.367s
exit=1

Deleting the second rule's call from the route:

-	if err := JudgeDropped(plans); err != nil {
-		return err
-	}

go test ./internal/catalogue -run TestARunThatWouldDropAResolvedPluginIsRefusedRatherThanPlaced -count=1
--- FAIL: TestARunThatWouldDropAResolvedPluginIsRefusedRatherThanPlaced (0.01s)
    catalogue_test.go:490: a plugin was dropped from the catalogue and the run exited zero:
          a-plugin             resolved                 2 finished, 0 test
          another-plugin       resolved                 1 finished, 0 test

        2 of 2 declared plugin(s) resolved with something to publish.

        what each resolved plugin's releases came to:
          a-plugin             0 version(s), 2 skipped, 0 stopping the run
            skipped 2.0.0-stable: no-usable-sidecar: no asset's contents name a-plugin_2.0.0.0.zip
            skipped 1.0.0-stable: no-usable-sidecar: no asset's contents name a-plugin_1.0.0.0.zip
          another-plugin       1 version(s), 0 skipped, 0 stopping the run

        manifest.json: wrote new bytes, carrying 1 plugin(s) and 1 version(s).
FAIL
FAIL	flowfin.dev/hub/internal/catalogue	0.386s

That red output is the failure the issue measured, reproduced from the tree: one
plugin gone from the placed file, exit zero, and the skips named in an output no
server reads. Both plants were made in the checkout, run, and reverted, and the
branch carries neither.

Each rule also has a near miss beside it.
TestOneDatedReleaseInTheSetIsStillTheNewest holds that one dated release in the
set makes the question answerable again and a defect in it fatal as before, and
TestAPluginThatPublishesSomethingIsNotReadAsDropped holds that a plugin
publishing one entry out of two releases is not read as dropped, which is the
ordinary state of every run today.

The rule has no real instance and is not expected to get one

for f in sources/*.json; do
  r=$(basename "$f" .json)
  printf "%s %s\n" "$r" "$(gh api "repos/Flowfin/jellyfin-plugin-$r/releases?per_page=100" \
    --jq '[.[] | select(.published_at == null)] | length')"
done
discover 0
invites 0
metadata-sync 0
requests 0
server-pairing 0
share-links 0
smart-collections 0
sso 0
stats 0
watchlist 0
watch-sync 0
whisper-subtitles 0

Run 2026-08-22. Nothing in the declared set publishes a release list without
dates, so this refuses fixtures and is expected to go on doing so. The decision
file says so in the same words, because a rule whose first real instance will
surprise somebody is worth writing down as one.

A correction carried in the same change

The last section of decisions/failure-posture.md said nothing refused any of
the posture, and named #24, #25, #27, #28 and #18 as where the refusals would
arrive. All five are closed and their refusals are in the tree, so the sentence
had stopped being true. It was found while writing the two conditions above,
which had to say what refuses them and could not do that beside a paragraph
claiming nothing does. The replacement hands the reader the command that derives
what is refused rather than a list that will drift the same way.

The means

Go and the suites already here. Both rules are refusable properties over values
these packages already hold, both are proven by fixtures the gate already runs,
and neither needs anything the tree does not carry. No dependency is added and
there is still no go.sum.

The gate

go run . gate
gate examined 14 of 14 legs.
  build                            passed
  test                             passed
  format                           passed
  editorconfig                     passed
  tests-reach-nothing              passed
  no-hardcoded-names               passed
  site-fetches-nothing-outside     passed
  site-links-resolve               passed
  coverage                         passed
  site-declares-its-language       passed
  install-address-is-answered      passed
  colour-stands-off-every-surface  passed
  page-matches-the-token-file      passed
  pr-hygiene                       passed

Run at the commit this branch carries. The harness did not run and was not asked
for, and no leg above depends on any requirement, so that verdict is complete
rather than partial. No server was started and nothing that could ask for
elevation was attempted.

This change has had no second reader. The evidence above stands in place of one:
each guard is shown refusing, each near miss is named, and every number carries
the command that produced it.

`internal/posture` ordered a plugin's releases by publication time and treated
index 0 as the release the run exists to publish. Where no release in the set
carried a time, none of `newerFirst`'s clauses fired, the order fell through to
the tag, and the highest tag arrived at index 0 -- the string
decisions/manifest-schema.md refuses as a version answering the question the
publication time exists to answer. A defect in that release then stopped the
whole run, taking every other plugin's catalogue down with it, over a published
release nobody here can repair.

The classification now says such a set has no newest release, so every defect in
it is a named skip. That alone moves the failure rather than removing it: the
plugin resolves, contributes nothing, and the catalogue is quietly one entry
shorter with a zero exit, which decisions/failure-posture.md spends its longest
section on being the worse of the two. So `catalogue.JudgeDropped` lands with it
and refuses a run in which a plugin that resolved produces no entry and nothing
stops.

decisions/failure-posture.md places both, and one correction beside them. Its
last section said nothing refused any of the posture and named five issues as
where the refusals would arrive; all five are closed and their refusals are in
the tree, so that sentence had stopped being true. It was found while writing
the two conditions above, which had to say what refuses them and could not do
that beside a paragraph claiming nothing does.

The means is Go and the existing suites, because both rules are refusable
properties over values the packages already hold and both are proven by fixtures
the gate already runs. No dependency is added and go.sum is untouched.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex added the bug Something isn't working label Aug 22, 2026
@iderex iderex self-assigned this Aug 22, 2026
@iderex iderex added this to the 3. The manifest generator milestone Aug 22, 2026
@iderex
iderex merged commit 3a86d8a into main Aug 22, 2026
36 checks passed
@iderex
iderex deleted the work/af10-undated-releases branch August 22, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A release nobody can place in time is classified as the newest one

1 participant