Skip to content

Roll a plugin back to the release and data an update replaced - #6044

Open
alexandre-vl wants to merge 54 commits into
get-bb:mainfrom
alexandre-vl:plugin-update-rollback
Open

alexandre-vl wants to merge 54 commits into
get-bb:mainfrom
alexandre-vl:plugin-update-rollback

Conversation

@alexandre-vl

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

Copy link
Copy Markdown
Contributor

Stacked on #6043 (itself on #6042). Until those merge, this diff includes their commits; review the commits after 682bfc00a (compare).

Human comments

What was wrong

With #6043, users can follow a range across a release that may break, so the next question is how to undo a bad update. bb already snapshots a plugin's state before an update. It restores that snapshot only when the new release fails to start within the 30 s stabilisation window. After that there is no way back. Worse, the snapshot it keeps for 7 days points at nothing: once the update settles, the post-update cleanup deletes the replaced release's artifact regardless of the snapshot's retention. So the "rollback state kept through the grace period" could never be used (the existing test "deletes the replaced version once the update settles and keeps rollback state through the grace period" asserts exactly this: the old artifact is ENOENT while the snapshot remains).

What changed

Server and db

  • Retention. While an update snapshot is ready and inside its 7 days, every cleanup path keeps the artifact it came from: post-update cleanup, bb plugin prune and periodic collection. Removing a plugin deletes its snapshots first.
  • Rollback (apps/server/src/services/plugins/plugin-rollback.ts, POST /api/v1/plugins/:id/rollback) re-activates the replaced release and restores what it had before the update: data.db, KV, settings, secrets and schedules.
    • It reuses the update activation path. It snapshots the post-update state first, so if the old release fails to start, or the server stops mid-rollback, the updated release and its data come back on the next start.
    • It keeps the current tracking range and enabled flag: a range changed with bb plugin track, or a plugin disabled after the update, stays that way.
    • After success both snapshot directories are deleted, and a fresh update check runs outside the server-wide registration lock. So bb plugin update offers the newer release again.
  • Refusals (nothing is changed): no rollback point, path or builtin sources, safe mode, a stale expectedVersion, and a kept copy whose hash or plugin id no longer matches.
  • Rollback point. A kept release whose engine ranges this bb no longer satisfies is not offered.
  • Read-only route. GET /api/v1/plugins/:id/rollback returns the rollback point, or the reason there is none.
  • Schema. New columns plugin_state_snapshots.operation (update | rollback, default update) and failure_detail, in one generated migration 0147_plugin_snapshot_recovery. Only update snapshots are rollback points or retain artifacts.
  • Rollback runs as a background job next to updates (kind: "rollback"), and never joins an update job. A failed rollback is reported through its job and keeps any earlier update failure.

Failure recovery (plugin-activation.ts, plugin-state-snapshot.ts). Rollback is the escape route from a broken release, so restoring a snapshot must never make things worse. This also fixes a bug already on main: when an update moved away from a release that could not start, the snapshot stayed restoring. Every later start then threw before loading any plugin, and removing the plugin did not clear it.

  • Isolation. Each plugin's unfinished restore is recovered on its own at start, so one plugin can no longer stop the others from loading. Removing a plugin deletes its snapshots.
  • Validate, then apply. A restore first checks everything it will need:
    • the saved registration, against this bb's format and consistency rules;
    • the kept artifact;
    • that every saved copy is readable.
    • The registration is written before any file.
  • Unusable snapshot. If the check finds a definite content problem (a missing file, invalid JSON, a format mismatch) before anything was written, the snapshot is marked failed. The plugin keeps its current release and data. The warning appears in reload, bb plugin list and plugin details.
  • Hold. Any other failure keeps the plugin stopped: an I/O error, or a snapshot that had started writing. The hold is read from the database, so enable, reload, safe mode, update, track and rollback all respect it.
    • A reload, or the next start, retries the restore.
    • Removing the plugin is the last resort.
  • Already-written restore. A restore that already wrote is never released onto half-restored data. A rollback's own safety snapshot stays restoring until all its writes are done.
  • Background checks. The update check after a rollback runs after the job completes and never after stop().

Contract and SDK

  • Rollback has its own result (reverted | rolled-back). applyUpdate's result is unchanged.
  • Jobs are a union keyed by kind; a job from an older server without kind reads as an update.
  • updateState.rollback {version, display, until}.
  • SDK experimental_rollbackUpdate, experimental_startRollbackUpdate and experimental_getRollbackPoint. All three are in docs/api_to_audit.md and the Plugin Guide surfaces. An older server raises the typed PluginRollbackUnsupportedError.

CLI

  • bb plugin rollback <id> [--yes] [--json] names the kept release and how long it is kept, and warns which data goes back to its pre-update values. It sends the release it showed as expectedVersion.
  • bb plugin update-jobs gains a Kind column.

UI

  • Plugin details → Details → "Roll back to ", with a confirmation listing what is restored and what is kept.
  • Rollback job toasts.

Docs

  • The plugins guide, command-index.md, distribution.md and backend-sdk.md.
  • They state what a rollback does not restore: thread plugin metadata, and files outside data.db and the secrets directory.

No daemon wire change; HOST_DAEMON_PROTOCOL_VERSION is unchanged.

Known follow-ups (found in the last review, non-blocking):

  • remove deletes a snapshot's row only after its files are deleted. If that rm fails, a reinstall of the same plugin starts out held.
  • Disabling a held plugin replaces the hold text in its status detail with nothing.
  • Safe mode lists held plugins among those that "did not start".
  • bb plugin rollback waits silently for the registration lock while another plugin's update runs.
  • Already on main and untouched here: applyUpdate resolves the npm candidate while holding the registration lock.

How you verified

Tests added (each fix was reverted once to show its test fails; 19 reverts in the last round alone):

  • plugin-rollback.test.ts, against a real migrated DB with real git fixture repos and a local npm registry:
    • update then rollback restores the release and its data;
    • a range changed with track and a disabled flag are kept;
    • stepping back twice, and a monorepo install;
    • a stale expectedVersion, every refusal, and a tampered kept copy (hash, plugin id, engines);
    • a failed rollback brings the updated release and its data back, and keeps an earlier update failure;
    • the update check after a rollback, and a rollback that finishes while bb stops.
  • plugin-update-recovery.test.ts covers recovery:
    • both releases fail to start, and a healthy plugin still loads after restart;
    • remove then restart does not bring the plugin back;
    • unusable snapshots (missing file, invalid JSON, format mismatch, inconsistent registration, a release no longer kept) leave the plugin on its current release and data;
    • failures while writing hold the plugin across enable, reload, safe mode, update, track and rollback;
    • a reload after the fault is fixed finishes the restore.
  • Retention in plugin-update.test.ts and the cache-prune tests; contract (job kind union, older servers), SDK, CLI and UI tests.

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

Filter Result
lint typecheck over every dependent of server-contract, sdk, plugin-sdk, templates, db and plugin-api-docs Tasks: 122 successful, 122 total
test --filter=@bb/server Tests 3734 passed | 1 skipped (3735)
test --filter=@bb/app Tests 6277 passed | 4 skipped (6281)
test --filter=@bb/cli Tests 913 passed | 1 skipped (914)
test db, server-contract, sdk, templates 636, 81, 137, 46 passed
test @get-bb/plugin-sdk, plugin-api-docs, bb-guide 385, 74, 7 passed
node scripts/check-plugin-forks.mjs plugins/plugin-api-docs plugins/bb-guide 2 forkable plugin(s) passed.
drizzle-kit generate against a copy of drizzle/ No schema changes, nothing to migrate

Each of the 22 commits also typechecks, and passes the tests of the packages it touches, on its own.

Rebase. The stack is rebased onto 9fad6b625, plus one commit that bumps @get-bb/plugin-sdk to 0.6.68: 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 22 commits unchanged, and the guard prints PASS — @get-bb/plugin-sdk@0.6.68 is not on npm yet.

End to end, in an isolated dev instance driven through the CLI, the HTTP API and the browser. 105 checks across the whole stack; the 33 rollback checks:

Step Observed
Update to v0.3.1, then roll back through the CLI, the UI and --json each reverted; UI toast "Plugin rolled back"
Data after the rollback setting, secret, KV and data.db back to pre-update values; the schedules the update changed are back to the pre-update set
A range tracked and a plugin disabled after the update range >=0.2.0 <0.4.0 kept; still disabled
Snapshot directories the update's directory deleted, none added
bb plugin update afterwards offers v0.3.1 again (the update check runs after the job, within a second)
Rollback with a stale expectedVersion HTTP 422 rollback refused: …, a failed kind: "rollback" job, nothing changed
Restart after a rollback all 29 enabled plugins running; the version, range and rollback point are unchanged
bb plugin remove snapshot directories 5 → 0

The failure paths need injected faults, so the recovery tests above cover them. Five rounds of independent review went through this PR. The last one confirmed with scratch tests that every earlier high and medium finding is fixed, and found nothing blocking.

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.
…n place

setInstalledPluginTrackingRange rewrites the source and range columns of a
git semver-range or npm row in one compare-and-set UPDATE and clears the
persisted update check and last failure, keeping provenance, the resolved
release, the enabled flag, and every other column.
POST /api/v1/plugins/:id/tracking-range takes { range, dryRun? } and
returns { applied, previousRange, range, check }. Under the registration and
lifecycle locks it rewrites the source and range of a git semver-range row
or an npm row with a managed artifact, keeping provenance, the catalog link,
the enabled flag, and all plugin data, then checks the plugin again and
persists that check. A dry run or the current range checks a copy of the row
and persists nothing.

The range's whitespace collapses to single spaces before it is validated,
compared, and stored. previousRange is the range, exact version, or dist-tag
the install followed, and null for an npm install that named no version.

It refuses path, builtin, and git ref sources, older npm installs whose
directory depends on their spec, invalid ranges, bare versions, a range that
admits no version, and a range that admits neither the installed release nor
a newer one: <1.0.0 || >=2.0.0 at 1.5.0 is accepted, and <=1.5.0 freezes
1.5.0. A pinned npm update now points at bb plugin track instead of remove
and reinstall, and the source detail reports an npm range.
The SDK member posts the range and dry-run flag to the tracking-range route
and parses the result. It is plugin-visible through PluginBbSdk, so it is
experimental, listed in docs/api_to_audit.md, in the backend SDK reference,
and on the Plugin Guide's bb SDK card.
bb plugin track <id> <range> previews what the range offers, asks for
confirmation (--yes skips, non-TTY refuses), then changes the range and
reports the new check; --dry-run only previews and --json prints the result.
bb plugin update now tells a pinned npm plugin to use bb plugin track, which
keeps its settings, secrets, and schedules. The plugins guide chapter and
the bb-cli command index document the command, and the guide explains that
an install keeps the range it was installed with.
A legacy npm row with an exact spec resolves as pinned, so no update ever
moves it to managed storage; its refusal now says so instead of pointing at
bb plugin update. Its update check carries the same refusal as the pinned
check's detail, and bb plugin update prints that detail instead of
suggesting bb plugin track.
The Details section of a git semver-range or npm plugin shows its update
range with a Change range action; an npm install without a range shows the
dist-tag it follows, its latest release, or None for an exact version. The
dialog previews what a range offers through a dry run of
plugins.experimental_setTrackingRange, enables the change only for the
previewed range, clears a failed change once the range is edited or
previewed again, and refreshes the plugin list and source after it applies.

The source detail reports npmSpecKind and, for a dist-tag install, distTag,
and bb plugin source prints the dist-tag.
The tracking range result gains openEnded, true when some comparator set of
the range has no upper bound, so every future major release would be offered.
bb plugin track warns in its dry run and before it asks, and the Change range
preview shows the warning; both still accept the range.
checkRows records a failed check as unavailable for that plugin, so the
range change takes the entry it returns instead of persisting its own. The
registration lock now covers only reading the row, planning the change, and
writing it; the check after the change runs once it is released, under the
lifecycle lock checkRows takes for that plugin. A dry run takes no lock, so
a preview neither waits behind an install, update, or removal nor blocks one
while it probes the source. The preview, which must not persist, reports a
failure with the same failedCheckEntry.
When the update check found a newer release outside the installed range, the
dialog opens on >=<installed> <(major of that release + 1).0.0, which always
includes it, and says why; the user can edit it before previewing. Previews
name releases with the compact release label, mark a candidate that may break,
and say when a newer release stays outside the previewed range.
The preview and result lines now carry the update check's may-break flag and
the newer release the range still excludes, as bb plugin update does.
A dry run against the proposed range returns the update check's breaking flag
and the newest release outside that range.
The distribution guide's Updates section now covers, together, staying inside
the installed range without downgrading, applying only the reviewed release,
reporting the newest release outside the range, and moving an install to
another range with bb plugin track or Change range. The CLI guide drops the
range explanation PR-C now gives and mentions the Change range suggestion.
outsideRange gains an optional suggestedRange. When the release is newer than
everything the range admits, it is >=<installed> <(major of that release +
1).0.0, where installed is the release bb resolved (the git tag's version or
the npm version, never package.json), so the range does not grow; otherwise,
such as for a release between two || alternatives, it is
<range> || >=<release> <(major + 1).0.0, so no release the range allowed is
dropped. It is recomputed when an update carries the release forward, and
left out for an install bb plugin track refuses, such as a legacy npm
install.

The Change range dialog opens on the suggestion, and bb plugin update's
outside-range lines and bb plugin outdated's per-plugin footnote end with
To follow it: bb plugin track <id> "<suggestedRange>". Double quotes protect
<, >, and | in POSIX shells, PowerShell, and cmd.exe. A server that computes
no suggestion, or a check such a server saved, leaves the field out: the
dialog then opens on the current range and the CLI prints no command.
The plugins guide, the distribution reference, and the audit entry describe
the range bb suggests for a release outside the range and show the
bb plugin track command with the range in double quotes. They also cover the
refusal of a range that admits no version or neither the installed release
nor a newer one, legacy npm installs that cannot change in place and get no
suggestion, previousRange, whitespace collapsing, and a registration lock
that covers only the change itself.
…pdate

A successful update deleted the replaced artifact right away, so the
pre-update snapshot pointed at nothing. The artifact now stays while that
snapshot is retained, and POST /api/v1/plugins/:id/rollback restores the
pre-update registration and state through the update activation path,
as a background job of kind "rollback". The tracking range set after the
update is kept.
…to the SDK

Both send the reviewed rollback version and run the server job; the
first waits for its result. A server without the rollback route gets an
explicit "predates plugin rollback" error instead of a bare 404.
Shows the kept release and its deadline, warns that the plugin's state
returns to its pre-update values, and binds the request to the release it
showed. update-jobs labels each job as an update or a rollback, and the
guide and command index document the command.
The Details section shows the release the last update replaced and how
long bb keeps it. The confirmation warns that settings, stored data,
secrets, and schedules return to their pre-update values, and starts a
rollback job bound to that release. Job toasts name rollbacks.
Checks that the rollback member ships in the portable declarations with
its named argument and result types.
Adds the audit entry for experimental_rollbackUpdate and
experimental_startRollbackUpdate, lists both on the Plugin Guide's bb SDK
card and in the backend SDK reference, explains in distribution.md what a
rollback restores, and corrects the cache-pruning entry: an update now
keeps the version it replaced while its rollback point is retained.
applyUpdate and experimental_rollbackUpdate now share settleUpdateJob
instead of each polling the update job on its own.
…llback points

The registration cache behind updateState.rollback is reconciled with the
retained rollback snapshots after every activation outcome, rollback
recovery, artifact collection, and plugin removal, so discarded, restored,
expired, and deleted snapshots leave it.
A second rollback lands on the release and data from before the first
update; a subdirectory install with a tag prefix returns to its prefixed
tag without touching a sibling plugin from the same checkout. The guide
and audit entry say that each rollback undoes the latest remaining update.
An older CLI or SDK cannot parse the update-jobs list for about ten
minutes after a rollback on a newer server.
Pruning stays. A live entry that pruning dropped would hide the next
rollback point, which the step-back-twice test catches; an entry it
keeps too long has no observable effect besides memory.
Restoring the pre-update flag restarted a plugin the user had disabled
since the update, and flipping it skipped the side effects setEnabled
runs. A rollback now keeps the current enabled and enabledFollowsDefault
values, the way it keeps the tracked range.
A rollback whose old release failed to start recorded that release in
lastFailure, so the plugin page showed "Update failed" with a Retry that
starts an update. State snapshots now record whether an update or a
rollback took them; recovering a rollback snapshot, live or after a
restart, leaves lastFailure alone, and only update snapshots count as
rollback points. The rollback toast shows the failure detail instead.

A restore now checks the saved registration, host state, data.db, and
secrets copies before it changes anything. A snapshot that fails the
check is marked failed with the reason, and the plugin keeps its current
release and data. A write that fails after the check holds the plugin
while it has an unfinished snapshot: reload, enable, safe mode, and the
resume after a server move do not start it, update, track, and rollback
refuse it, and a reload or the next start retries the restore. A start
restores each plugin's oldest unfinished snapshot first and discards the
newer ones once it succeeds.
The restored pre-update snapshot and the discarded post-update safety
snapshot kept their data.db and secrets copies on disk until the 7-day
expiry although nothing reads them again. Their rows stay as history.
… check

A rollback restored the update-check entry recorded before the update,
so the plugin could offer a candidate from a range changed since. The
rollback now leaves the check state empty and runs a fresh check of the
current range before its job completes. Keeping the current enabled
flags made the rollback registration argument of the activation
redundant, so it is gone.
The rollback stays in its stabilization window while the row points at
the previous release; a second service on the same database and data
directory must bring back the updated release and its data, record no
update failure, and still offer the rollback point.
The rollback point cache now keeps the kept release's engine ranges,
read from its manifest when the snapshot is taken and again after a
restart, so the plugin list stops offering a rollback the server would
refuse after a bb upgrade; the server reports it as having no rollback
point with the unmet range. Tests cover that and the refusal of a kept
copy that declares another plugin id.
Rollback added the outcome reverted to the stable applyUpdate result.
Rollback members now return a PluginRollbackUpdateResult (reverted or
rolled-back, with a required target), and PluginUpdateJob is a union
keyed by kind, so a completed job's result has the type its kind names.
A job without kind, from a server that predates rollback, still reads
as an update.
Against a server that predates rollback, bb plugin rollback said the
plugin had no rollback point, because it read the point from the plugin
list. A read-only GET /plugins/:id/rollback, exposed as
experimental_getRollbackPoint, now returns the point and the installed
release or the reason there is none. The SDK raises a 404 from either
rollback route as PluginRollbackUnsupportedError, which the CLI reports
the way it reports an older server for updates.
Thread plugin metadata and files outside data.db and the secrets
directory keep what the updated release wrote. The audit entry now
covers experimental_getRollbackPoint and how bb checks, holds, and
retries a restore that did not finish, and it drops the monorepo
rollback requirement, which "Cover stepping back twice and rolling back
a monorepo install" covers. The guide tells users what a held plugin
needs.
…ased

The fresh check after a rollback reaches the plugin's git remote or npm
registry, and it ran inside the server-wide registration lock, so a slow
source held every other plugin install, update, and removal. The
rollback now releases the lock first and checks the row in the
background. The server tracks that check and waits for it when it stops,
so the check never writes after the plugins have shut down.
… plugin details

Reload returned only the problem of a retried restore and dropped the
warning bb raised when it gave up undoing an update or a rollback, and the
CLI never printed `updateState.lastFailure`, so a give-up left no trace
outside the logs and the plugin page claimed a restore that never happened.

The reload response now carries `warnings`, which `bb plugin reload` prints,
and `lastFailure.restored` says whether bb restored the release it replaced;
the server derives it from the registration, and the SDK reads a response
from an older server as restored with no warnings. `bb plugin list` and
reload print the last failed update with its date and outcome, and the
plugin details and update dialog say bb kept the release when it did.
@alexandre-vl
alexandre-vl force-pushed the plugin-update-rollback branch from c4361b5 to f3aeb2c 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