Skip to content

Report plugin releases outside the installed range and apply only the reviewed release - #6042

Open
alexandre-vl wants to merge 15 commits into
get-bb:mainfrom
alexandre-vl:plugin-updates-outside-range
Open

alexandre-vl wants to merge 15 commits into
get-bb:mainfrom
alexandre-vl:plugin-updates-outside-range

Conversation

@alexandre-vl

@alexandre-vl alexandre-vl commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Human comments

What was wrong

A plugin installed from git or npm records a semver range, and update checks only look inside it (satisfyingGitSemverTags). Most marketplace entries declare ^0.y.z, and on 0.x a caret range stops before the next minor: ^0.1.0 is >=0.1.0 <0.2.0-0. A plugin installed at 0.1.x therefore reports current when 0.2.0 exists, and the plugin page draws no Update control, because it only renders when an in-range release exists (PluginUpdatesCard.tsx#L61). Users read this as the plugin being pinned (#5570).

Reproducing it turned up three neighbouring defects in the same path:

  • Apply installs a release nobody reviewed. applyUpdate resolves again when it runs (plugin-updates.ts#L582) and takes no arguments (pluginApplyUpdateRequestSchema). If a release is tagged between the check and the click, the CLI, UI and API install it, not the one shown.
  • One bad source fails every check. The update sweep has no per-plugin error boundary (checkRows). One unreachable repository (HTTP 422: git ls-remote failed (exit 128)) fails bb plugin outdated and the background check for all plugins.
  • An "update" can be a downgrade. Neither resolver checks that the candidate is newer than the installed release (git, npm). On npm this already happens today: a plugin that follows a dist-tag is "updated" back to an older version when its maintainer moves the tag back (allowedNpmVersions). On git it becomes reachable once the recorded range can change, which is the follow-up PR.

What changed

Server (apps/server/src/services/plugins/)

  • update-resolver.ts:
    • Each check now also reports outsideRange: the newest stable release that is newer than the installed one and than every release the range admits. A release in a gap of the range is skipped once a newer admitted release exists. For example, ^0.1.0||>=0.2.1 does not name the excluded 0.2.0 while it offers 0.2.5. It is computed from the tag list or packument the check already fetched, so it costs no extra request.
    • breaking follows caret semantics: a new major; a new minor while the major is 0; a new patch while both major and minor are 0.
    • Downgrade guard for git ranges, npm ranges and default specs, and npm dist-tags. A dist-tag that points below the installed version answers current with a "bb does not downgrade" detail.
  • plugin-updates.ts:
    • A failure while checking one plugin is stored as unavailable with the error as its detail; the rest of the sweep continues.
    • Apply takes an optional expectedVersion and refuses before staging anything when the fresh candidate differs from it (HTTP 422, naming both releases).
    • After an apply, an outsideRange that is still newer is carried over with breaking recomputed.
  • plugin-service.ts: updateState gains availableDisplay, availableBreaking and outsideRange, and forwards the detail of a current check.
  • plugin-update-jobs.ts: job deduplication includes expectedVersion.

Contract (packages/server-contract/src/api/plugins.ts): additive only.

  • outsideRange {version, display, breaking, range} and candidateBreaking on check entries.
  • expectedVersion on the apply request.
  • The new updateState fields.

SDK (packages/sdk/src/areas/plugins.ts)

  • applyUpdate and experimental_startUpdate take PluginApplyUpdateArgs { expectedVersion? }.
  • A server that predates the field raises the typed PluginUpdateExpectedVersionUnsupportedError.
  • docs/api_to_audit.md and the plugin SDK bundled declarations are updated.

CLI (apps/cli/src/commands/plugin.ts)

  • bb plugin outdated adds an "Outside range" column. Under the table, it prints each plugin's detail line, such as the reason a source is unavailable.
  • bb plugin update:
    • prints <id>: current within ^0.1.0; 0.2.0 (may break) is outside the range (compatibility not checked).;
    • warns before confirming a breaking candidate;
    • always sends expectedVersion;
    • tells the user to upgrade a server that predates expectedVersion.

UI (apps/app/src/components/plugin/management/)

  • Plugin details show the outside-range release when there is no in-range update, plus a breaking-change note and the check's detail.
  • Installed-plugin rows show a muted "0.2.0 outside range" hint.
  • The update dialog names a git release by its tag (v0.1.3) instead of a short SHA.
  • Update and Retry send expectedVersion.

Docs: bb guide plugins (template bb-guide-plugins.md) explains the outside-range report, the 0.x caret rule and reviewed-release updates.

No daemon wire change, so HOST_DAEMON_PROTOCOL_VERSION is unchanged. Changing the recorded range is the follow-up PR (bb plugin track); this PR only reports what the range excludes.

How you verified

Tests added (they fail without the fix):

  • apps/server/test/services/plugins/update-resolver.test.ts: outsideRange for git and npm, gapped and || ranges (a gap release below a newer admitted release is not named), a range tracked above the installed release, prereleases, tag prefixes, the caret breaking rule including 0.0.x, and the downgrade guard (git range, npm range or default, npm dist-tag).
  • plugin-update.test.ts: per-plugin isolation in the sweep, the expectedVersion refusal before staging, and the outsideRange carry-over after an apply.
  • plugin-update-jobs.test.ts: deduplication by expectedVersion.
  • SDK, CLI output, app (details, dialog, row hint, queries) and the plugin SDK bundled declarations.
  • To prove the tests bite, 12 fixes were reverted one at a time, and each revert failed its test: 5 server (including the gap rule: expected '0.2.0' to be null), 2 app, 4 CLI, 1 SDK.

Suites, on this branch before its rebase onto 9fad6b625 (pnpm exec turbo run …):

Filter Result
lint typecheck --filter=...@bb/server-contract --filter=...@bb/sdk --filter=...@get-bb/plugin-sdk --filter=...@bb/templates (every dependent package) Tasks: 120 successful, 120 total
test --filter=@get-bb/plugin-sdk Tests 385 passed (385)
test --filter=@bb/sdk Tests 128 passed (128)
test --filter=@bb/server-contract Tests 78 passed (78)
test --filter=@bb/cli Tests 886 passed | 1 skipped (887)
test --filter=@bb/templates Tests 46 passed (46)
test --filter=@bb/server Tests 3635 passed | 1 skipped (3636)
test --filter=@bb/app Tests 6239 passed | 4 skipped (6243)

Rebase. The branch is rebased onto 9fad6b625, plus one commit that bumps @get-bb/plugin-sdk to 0.6.66: this PR changes the SDK's bundled declarations, and CI's npm version guard refuses a changed package under a published version. git range-diff shows the other 14 commits unchanged, and the guard prints PASS — @get-bb/plugin-sdk@0.6.66 is not on npm yet.

End to end, with an isolated dev instance (its own data dir and ports) driven through the CLI, the HTTP API and the browser. Fixtures: a local marketplace and HTTPS git repositories tagged v0.1.0…v0.2.0:

Journey Before (main) After
Catalog install with ^0.1.0 while v0.2.0 exists current, nothing shown rangefix: current within ^0.1.0; 0.2.0 (may break) is outside the range (compatibility not checked). The detail and row hints say the same. 8/8 checks
A newer release tagged between review and apply The CLI and UI install the newer release, not the reviewed one; the API cannot name the reviewed release (expectedVersion → HTTP 400) update refused: expected 1b38c744…, but the current candidate is …@v0.1.2 (460860148a20); check for updates again and review it. HTTP 422 from the API; nothing changes. 20/20 checks
One plugin's repository unreachable The whole check fails: HTTP 422: git ls-remote failed (exit 128) That plugin is unavailable with the git error; the others are still checked. 8/8 checks

Part of #5570

AGENT GENERATED

…eviewed release

Update checks now name the newest stable release that the installed range
excludes (outsideRange, with a breaking flag for a new major or a new 0.x
minor) and flag in-range candidates that cross the same boundary
(candidateBreaking). Both are computed from the tag list or packument the
check already fetched and are exposed on InstalledPlugin.updateState with the
candidate's display.

Resolvers never offer a release older than the installed one: a git range
stops at the first tag that is not newer, an npm range or default spec
answers current, and a dist-tag that moved below the installed version
answers current with a detail saying bb does not downgrade.

The apply request accepts expectedVersion; the server refuses, without
touching the installation, unless the freshly resolved candidate matches it.
Update jobs only join an active job that applies the same reviewed version.

A full check isolates each plugin: one that throws is persisted as
unavailable with the error as its detail, and the others are still checked.
A successful apply keeps a still-newer outside-range release on the entry.
applyUpdate and experimental_startUpdate accept an optional expectedVersion
and send it in the apply request, so the server can refuse to activate a
different release than the one the caller confirmed. Omitting it keeps the
previous behaviour of applying the newest compatible release in range.
…release

bb plugin outdated gains an Outside range column (the version, marked "may
break" across a major or a 0.x minor) and a note that its compatibility is
not checked; Status marks a breaking in-range candidate. bb plugin update
sends the checked candidate as expectedVersion, warns before confirming a
breaking candidate, mentions a release outside the range, and explains a
current plugin as "current within <range>" when a newer release is excluded.
…wed release

The plugin detail release status names a newer release that the installed
range excludes (with a "may include breaking changes" note and "Compatibility
not checked") when there is no in-range update, and marks an available
release that may break. The update dialog names a git release by its tag
rather than a short commit, shows the full release display in its details,
and warns before applying a breaking release. Update and Retry send the
reviewed version as expectedVersion so the server refuses a different one.
The plugins guide explains that updates stay inside the installed range and
never downgrade, that outdated, update, and the plugin page name a newer
release outside the range without checking its compatibility, and that the
SDK update calls accept expectedVersion. The update-jobs audit entry records
the expectedVersion contract and job deduplication by expected version.
When a plugin has no in-range update but a newer release exists outside its
installed range, the installed-plugins row shows a muted "<version> outside
range" hint in its footer, with a hover explanation naming the range, whether
the release may include breaking changes, and that compatibility is not
checked. The hint is plain text, not an Update control, and an in-range
update, a failed update, a runtime status, or a check that needs attention
still take the row's single signal.
BB servers before reviewed-release updates reject any update request field
with a 400 "expected an empty JSON object". When applyUpdate or
experimental_startUpdate sent expectedVersion and gets that rejection, the
SDK now throws an error saying the update was not started because the server
predates reviewed-release updates, instead of the opaque HTTP 400. Other
rejections still surface as the server reported them.
The bundled declarations now type applyUpdate as
applyUpdate(args: PluginApplyUpdateArgs), so the declaration test looks for
that signature instead of the old PluginIdArgs one.
…ange skips

A release is breaking when it changes the major version, the minor version
before 1.0, or the patch version before 0.1, which is what a caret range
excludes; 0.0.x patches were not flagged before. The release reported outside
the installed range is now the newest stable release newer than the installed
one that the range does not admit, so a disjoint range such as
^1.0.0 || ^3.0.0 names 2.0.0 instead of dropping the report because 3.0.0
satisfies it. Tests cover both, and an incompatible npm release older than the
installed one staying out of the blocked report. The update dialog, the CLI
breaking warning, and the plugins guide describe the 0.0.x rule.
A current check can carry a detail, such as an npm dist-tag that moved below
the installed version ("bb does not downgrade"). The plugin list now forwards
that detail for current outcomes as it already did for unavailable ones. The
plugin page shows it as a note under Update, and a detail alone now opens that
section, which also makes an unavailable check's detail visible there.
`bb plugin outdated` prints one "<id>: <detail>" line under the table for
every result that has a detail.
Both `bb plugin update` lines that name a release outside the installed
range ("current within <range>; <version> is outside the range" and
"<version> is outside the range <range> and is not part of this update") now
add "(may break)" when that release is breaking, as `bb plugin outdated`
already did. The plugins guide says a refused reviewed-release update leaves
the installation untouched while the fresh check result is still saved.
…updates

The SDK now raises a typed PluginUpdateExpectedVersionUnsupportedError,
exported from @bb/sdk with the plugin id and expected version, when a server
rejects expectedVersion with its old "expected an empty JSON object" 400. SDK
callers keep the message that suggests retrying without expectedVersion.
`bb plugin update`, which always sends the reviewed version, recognizes the
error by type in both the waiting and --no-wait paths and tells the user to
upgrade bb on the server and run the update again.
The update-jobs audit entry now names applyUpdate.expectedVersion as a
plugin-visible field on the stable applyUpdate member, describes its refusal
and job deduplication, the CLI and app callers that send it through the same
route, the typed SDK error from older servers, and what stabilizing it
requires.
The release outside the installed range could be one in a gap of the range
even when a newer release the range admits exists. A ^0.1.0||>=0.2.1 install
at 0.1.9 with tags v0.2.0 to v0.2.5 named 0.2.0, the release the marketplace
entry excludes on purpose, while the check offered 0.2.5, and an install at
1.5.0 tracking >=2.0.0 <3.0.0 named the older 1.8.0 next to 2.1.0.

The reported release must now be newer than both the installed version and
the newest release that satisfies the range, using semver.satisfies so
prerelease handling matches range matching, on the git and npm paths. A gap
release is still named when every release the range admits is older than it.
When the newest admitted release is incompatible, it stays reported as blocked
and nothing is named outside the range.
@alexandre-vl
alexandre-vl force-pushed the plugin-updates-outside-range branch from a3680a1 to a81ef17 Compare October 21, 2026 15:55
@alexandre-vl
alexandre-vl marked this pull request as ready for review October 21, 2026 16:09

This branch has not been deployed

No deployments
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.

1 participant