Skip to content

fix(registry): continue the tail from the last stored release - #160

Merged
roxblnfk merged 5 commits into
1.xfrom
fix/registry-listing-shift
Oct 12, 2026
Merged

roxblnfk merged 5 commits into
1.xfrom
fix/registry-listing-shift

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

🔍 What was changed

  • Loading older releases no longer skips one when the listing has shifted under the stored tail — a release deleted upstream, a vanished draft placeholder, an unreadable release or new ones published while nothing was stored. The stored count is only a hint now: the tail continues after the last stored release found on the page holding that position.
  • A server serving another number of releases per page than requested no longer makes every run fetch the stored pages again or lose releases: a first page holding at most half of what was asked tells a smaller page size, and one serving more is handled by the search.
  • A repository without releases costs one request instead of two.

How it works

  • The page holding the last stored position shows which way the listing moved: stored releases on it → continue after the last one; none → look further up the listing. Nothing is dropped or reloaded.
  • A release deleted upstream stays stored until its download fails and it is forgotten — one 404, then the next release is tried.
  • This also runs within the TTL; it costs one request when the stored list ends exactly on a page boundary, and nothing when it ends inside a page or the check has just seen the whole list.

Review notes

  • Offset paging gives no way to detect a shift without looking at the page of the last stored release: a cursor (Link rel=next) shifts the same way, and neither GitHub nor GitLab offers keyset pagination for releases.
  • ReleaseSource::pages($offset) now yields whole pages carrying the listing position of their first release, starting with the page that holds $offset.

Checklist

fix(repository): map a release offset onto the page size the server serves

A release deleted upstream below the checked span, or a vanished draft placeholder, shifted the listing and `extend()` skipped a release for good; with offset paging the position can only be confirmed by finding the last stored release. Within the TTL the record is trusted whole, like its head, so the confirmation costs a request only after a check and only on a page boundary.

The sources assumed 100 releases per page; the real size is learned from a page followed by another one rather than stored, to keep provider paging out of the record format.

Closes #159

Assisted-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added bug Something isn't working tests labels Oct 12, 2026
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
...Module/Registry/Internal/StoredVersionRegistry.php 100.00% <100.00%> (ø)
src/Module/Registry/Record/RepositoryRecord.php 100.00% <ø> (ø)
...le/Repository/Internal/GitHub/GitHubRepository.php 97.36% <100.00%> (ø)
...le/Repository/Internal/GitLab/GitLabRepository.php 97.36% <100.00%> (ø)
.../Module/Repository/Internal/PagedReleaseSource.php 100.00% <100.00%> (ø)

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

refactor(repository): share the paged release source between providers

A check that failed or bailed out has nothing to compare the stored list against, so the tail is taken as it is: a confirmation there would spend a request where a rate limit just refused one, and a tail dropped before its reload could fail left an empty record behind. The dropped tail is now written only along with the first page that replaces it.

A source that answers the anchor position with no page at all has shifted too; taking it for a complete listing kept losing the release.

The page size is learned from the first page only, the one page whose start is known: a shorter page further down is a release filtered out after paging, and taking it for the page size dropped releases within the TTL.

Assisted-By: Claude Fable 5.1 <noreply@anthropic.com>
fix(repository): count unreadable releases among the skipped positions
refactor(repository): own the paged release source

A check that failed or bailed out on an unreadable release compared nothing, so taking the tail as confirmed let a deletion below the head lose a release for good; the confirmation lies on the page the tail needs anyway, and a dropped tail is written only with its replacement, so skipping it spared nothing.

An unreadable release holds a listing position but is missing from a page's list, so the check reports such positions for the anchor, and the source no longer drops a readable release behind them when it skips into a page.

The registry knows nothing of pages, so the paged source belongs to the repository module; its page size is learned per instance, which rules out servers serving more releases per page than asked for, as GitHub and GitLab never do.

Assisted-By: Claude Fable 5.1 <noreply@anthropic.com>
…ext one

A release the record does not hold above the stored tail, an unreadable one or one published while the check could not store it, moves the last stored release down the listing, so expecting it exactly at the stored count took every such release for a deletion and reloaded the tail on every check.

The count of unreadable releases the check saw could not tell those apart from a stored release that turned unreadable, so the position is searched for instead of computed.

A shift down is bounded by what lies above the tail, less than a page in practice, while a shift up has no bound, so one further page is the place to look before giving the tail up.

Assisted-By: Claude Fable 5.1 <noreply@anthropic.com>
fix(repository): take a first page for the page size only when it holds at most half

The page holding the last stored position shows which way the listing moved, so the tail continues after the last stored release found there; dropping and reloading the unverified tail cost a request per stored page, and a release deleted upstream stays stored until its download fails and it is forgotten.

Within the TTL the tail is located the same way: trusting it whole lost a release deleted in that window for good.

A first page short of a release or two is one filtered out after paging; taking it for the page size made every check reload the tail.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
@roxblnfk
roxblnfk merged commit b345838 into 1.x Oct 12, 2026
23 checks passed
@roxblnfk
roxblnfk deleted the fix/registry-listing-shift branch October 12, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Version registry skips a release when the listing shifts under the stored tail

1 participant