Files
kandev/apps/backend/internal/backendapp/plugins.go
T
João Salavisa 5db3e48262 feat: add opt-in plugin auto-updater (#1903)
* feat: add opt-in plugin auto-updater

Automatically update installed plugins in the background, off by default
and opt-in. Builds on the existing verified install pipeline — an
auto-update is just an automatic reinstall of a newer catalog package_url.

Two-tier toggle on Settings > Plugins: an instance-wide default (SQLite
plugin_settings) plus a per-plugin override (tri-state auto_update on the
plugin record, carried forward on upgrade). The AutoUpdatePoller
(~90-min cadence + a boot sweep) upgrades only plugins that are currently
active, opted in, and report update_available; disabled/errored plugins
stay on their installed version.

New endpoints: GET/PUT /api/plugins/settings and
PUT /api/plugins/:id/auto-update.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): recheck auto-update eligibility at install and fix test mock

Address PR #1903 review feedback:
- Re-check each plugin's current status + effective opt-in immediately before
  InstallFromURL (eligibleForAutoUpdate), closing the TOCTOU window between the
  candidate snapshot / catalog fetch and the install so a plugin disabled or
  opted out mid-sweep is never reinstalled and reactivated.
- Correct the marketplace spec to describe the real failure behavior (shared
  with manual updates) instead of promising a version rollback the install
  pipeline does not perform.
- Fix the frontend CI failure: the plugins page test mocked the whole
  plugins-api module without the new settings/auto-update functions, so
  useAutoUpdateSettings threw at mount.
- Add an aria-label to the per-plugin auto-update Switch and Reset control.
- Add a use-auto-update-settings hook test (load, failure, optimistic setDefault,
  rollback) and an eligibleForAutoUpdate unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:04:54 +01:00

64 lines
2.5 KiB
Go

package backendapp
import (
"context"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/events/bus"
"github.com/kandev/kandev/internal/plugins"
"github.com/kandev/kandev/internal/plugins/delivery"
pluginstore "github.com/kandev/kandev/internal/plugins/store"
)
// pluginActivePluginsAdapter adapts *plugins.Service to delivery.PluginLister
// so the Deliverer can track which plugins should receive live deliveries
// (StatusActive) or buffer them (StatusError) without internal/plugins/delivery
// importing internal/plugins — see that package's doc comment ("backendapp
// adapts *plugins.Service to satisfy them").
type pluginActivePluginsAdapter struct {
svc *plugins.Service
}
// ActivePlugins implements delivery.PluginLister.
func (a pluginActivePluginsAdapter) ActivePlugins() []delivery.PluginRecord {
records := a.svc.Registry().List()
out := make([]delivery.PluginRecord, 0, len(records))
for _, rec := range records {
if rec.Status != pluginstore.StatusActive && rec.Status != pluginstore.StatusError {
continue
}
out = append(out, delivery.PluginRecord{
ID: rec.ID,
EventSubjects: rec.Capabilities.Events,
Status: rec.Status,
})
}
return out
}
// startPluginsSubsystems attaches event delivery to svc and spawns every
// already-active, runtime-managed plugin (resuming plugins that were active
// before a backend restart), registering shutdown via addCleanup. Mirrors
// how the Jira/Linear/Sentry pollers are started in
// startAgentInfrastructure: construction happens in initPluginsService
// (services.go), lifecycle start happens here once ctx/addCleanup exist.
func startPluginsSubsystems(ctx context.Context, svc *plugins.Service, eventBus bus.EventBus, log *logger.Logger, addCleanup func(func() error)) {
transport := plugins.NewRuntimeTransport(svc.Runtime())
deliverer := delivery.New(eventBus, transport, pluginActivePluginsAdapter{svc: svc}, log)
svc.SetDeliverer(deliverer)
deliverer.Refresh()
addCleanup(func() error { deliverer.Stop(); return nil })
addCleanup(func() error { svc.Shutdown(); return nil })
svc.StartActivePlugins(ctx)
// Start the opt-in auto-update poller after the active plugins are spawned,
// so its immediate first sweep sees them as StatusActive (only active,
// opted-in plugins auto-update).
autoUpdatePoller := plugins.NewAutoUpdatePoller(svc, log)
autoUpdatePoller.Start(ctx)
addCleanup(func() error { autoUpdatePoller.Stop(); return nil })
log.Info("Plugins subsystems started (event delivery + active plugin spawn + auto-update poller)")
}