From ae4b26ef00b12e940089fcf928bdebe257f0f2b5 Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Tue, 8 Sep 2026 20:10:42 -0400 Subject: [PATCH 1/2] Add an api_version field to plugin.json Plugins can declare which plugin api version they target, so the panel can refuse to load plugins written against a future incompatible api instead of failing in unpredictable ways. The field is optional and defaults to 1, plugins declaring a higher version than the panel supports are marked incompatible before loading, plugins missing the field get a warning note on the plugins page, and p:plugin:make scaffolds the field. --- .../Commands/Plugin/MakePluginCommand.php | 2 ++ .../Resources/Plugins/PluginResource.php | 1 + app/Models/Plugin.php | 18 ++++++++++++ app/Services/Helpers/PluginService.php | 7 +++++ lang/en/admin/plugin.php | 1 + tests/Unit/Models/PluginApiVersionTest.php | 29 +++++++++++++++++++ 6 files changed, 58 insertions(+) create mode 100644 tests/Unit/Models/PluginApiVersionTest.php diff --git a/app/Console/Commands/Plugin/MakePluginCommand.php b/app/Console/Commands/Plugin/MakePluginCommand.php index a8f2eb1d46..84d21139dd 100644 --- a/app/Console/Commands/Plugin/MakePluginCommand.php +++ b/app/Console/Commands/Plugin/MakePluginCommand.php @@ -4,6 +4,7 @@ use App\Enums\PluginCategory; use App\Enums\PluginStatus; +use App\Models\Plugin; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Str; @@ -111,6 +112,7 @@ public function handle(): void 'class' => $class, 'panels' => $panels, 'panel_version' => $panelVersion, + 'api_version' => Plugin::SUPPORTED_API_VERSION, 'composer_packages' => $composerPackages, 'meta' => [ 'status' => PluginStatus::Enabled, diff --git a/app/Filament/Admin/Resources/Plugins/PluginResource.php b/app/Filament/Admin/Resources/Plugins/PluginResource.php index 217b447069..2d26c6bb82 100644 --- a/app/Filament/Admin/Resources/Plugins/PluginResource.php +++ b/app/Filament/Admin/Resources/Plugins/PluginResource.php @@ -83,6 +83,7 @@ public static function table(Table $table): Table ->label(trans('admin/plugin.status')) ->badge() ->tooltip(fn (Plugin $plugin) => $plugin->status_message) + ->description(fn (Plugin $plugin) => is_null($plugin->api_version) ? trans('admin/plugin.api_version_missing') : null) ->sortable(), ]) ->recordActions([ diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 01bad4f701..b1bf8ce6b5 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -30,6 +30,7 @@ * @property string $class * @property string|null $panels * @property string|null $panel_version + * @property int|null $api_version * @property string|null $composer_packages * @property PluginStatus $status * @property string|null $status_message @@ -61,6 +62,9 @@ class Plugin extends Model implements HasPluginSettings public const RESOURCE_NAME = 'plugin'; + /** The highest plugin.json api_version this panel supports. */ + public const SUPPORTED_API_VERSION = 1; + protected $primaryKey = 'id'; protected $keyType = 'string'; @@ -89,6 +93,7 @@ public function getSchema(): array 'class' => 'string', 'panels' => 'string', 'panel_version' => 'string', + 'api_version' => 'integer', 'composer_packages' => 'string', 'status' => 'string', 'status_message' => 'string', @@ -110,6 +115,7 @@ public function getSchema(): array * class: string, * panels: ?string, * panel_version: ?string, + * api_version: ?int, * composer_packages: ?string, * status: string, * status_message: ?string, @@ -160,6 +166,7 @@ public function getRows(): array 'class' => $data['class'], 'panels' => $panels, 'panel_version' => Arr::get($data, 'panel_version', null), + 'api_version' => Arr::get($data, 'api_version', null), 'composer_packages' => $composerPackages, 'status' => Str::lower(Arr::get($data, 'meta.status', PluginStatus::NotInstalled->value)), @@ -187,6 +194,7 @@ public function getRows(): array 'class' => 'Error', 'panels' => null, 'panel_version' => null, + 'api_version' => null, 'composer_packages' => null, 'status' => PluginStatus::Errored->value, @@ -235,6 +243,16 @@ public function isCompatible(): bool return !$this->panel_version || $currentPanelVersion === 'canary' || version_compare($currentPanelVersion, str($this->panel_version)->trim('^'), $this->isPanelVersionStrict() ? '=' : '>='); } + public function effectiveApiVersion(): int + { + return $this->api_version ?? 1; + } + + public function isApiVersionSupported(): bool + { + return $this->effectiveApiVersion() <= self::SUPPORTED_API_VERSION; + } + public function isPanelVersionStrict(): bool { if (!$this->panel_version) { diff --git a/app/Services/Helpers/PluginService.php b/app/Services/Helpers/PluginService.php index 429780c4f7..462db1185c 100644 --- a/app/Services/Helpers/PluginService.php +++ b/app/Services/Helpers/PluginService.php @@ -42,6 +42,13 @@ public function loadPlugins(): void $plugins = Plugin::orderBy('load_order')->get(); foreach ($plugins as $plugin) { try { + // Filter out plugins that require a newer plugin api than this panel supports + if (!$plugin->isApiVersionSupported()) { + $this->setStatus($plugin, PluginStatus::Incompatible, 'This Plugin requires plugin api version ' . $plugin->effectiveApiVersion() . ' but this Panel only supports up to version ' . Plugin::SUPPORTED_API_VERSION . '!'); + + continue; + } + // Filter out plugins that are not compatible with the current panel version if (!$plugin->isCompatible()) { $this->setStatus($plugin, PluginStatus::Incompatible, 'This Plugin is only compatible with Panel version ' . $plugin->panel_version . (!$plugin->isPanelVersionStrict() ? ' or newer' : '') . ' but you are using version ' . config('app.version') . '!'); diff --git a/lang/en/admin/plugin.php b/lang/en/admin/plugin.php index 8bcf895e50..c21acaf9de 100644 --- a/lang/en/admin/plugin.php +++ b/lang/en/admin/plugin.php @@ -7,6 +7,7 @@ 'name' => 'Name', 'update_available' => 'An update for this plugin is available', + 'api_version_missing' => '⚠ plugin.json does not declare an api_version, assuming 1', 'author' => 'Author', 'version' => 'Version', 'category' => 'Category', diff --git a/tests/Unit/Models/PluginApiVersionTest.php b/tests/Unit/Models/PluginApiVersionTest.php new file mode 100644 index 0000000000..7f081972de --- /dev/null +++ b/tests/Unit/Models/PluginApiVersionTest.php @@ -0,0 +1,29 @@ +api_version = $apiVersion; + + $this->assertSame($expectedEffective, $plugin->effectiveApiVersion()); + $this->assertSame($expectedSupported, $plugin->isApiVersionSupported()); + } + + public static function apiVersionDataProvider(): array + { + return [ + 'declared supported version' => [Plugin::SUPPORTED_API_VERSION, Plugin::SUPPORTED_API_VERSION, true], + 'missing defaults to 1' => [null, 1, true], + 'newer than supported' => [Plugin::SUPPORTED_API_VERSION + 1, Plugin::SUPPORTED_API_VERSION + 1, false], + ]; + } +} From 8d30f112b99a581fccd75d0bf6c0264a009315c2 Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Tue, 15 Sep 2026 20:42:28 -0400 Subject: [PATCH 2/2] Only enforce plugin api version check in production --- app/Services/Helpers/PluginService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Services/Helpers/PluginService.php b/app/Services/Helpers/PluginService.php index 5f4b98d338..f794155059 100644 --- a/app/Services/Helpers/PluginService.php +++ b/app/Services/Helpers/PluginService.php @@ -43,7 +43,8 @@ public function loadPlugins(): void foreach ($plugins as $plugin) { try { // Filter out plugins that require a newer plugin api than this panel supports - if (!$plugin->isApiVersionSupported()) { + // (only in production, so plugin developers can work on fixing their plugin locally) + if ($this->app->isProduction() && !$plugin->isApiVersionSupported()) { $this->setStatus($plugin, PluginStatus::Incompatible, 'This Plugin requires plugin api version ' . $plugin->effectiveApiVersion() . ' but this Panel only supports up to version ' . Plugin::SUPPORTED_API_VERSION . '!'); continue;