5db3e48262
* 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>
64 lines
2.5 KiB
Go
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)")
|
|
}
|