diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-15 00:49:58 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-15 00:49:58 +0300 |
| commit | c91348e68b9e438159ffecc3e26bc6f971e0fa2a (patch) | |
| tree | 30d2abbe5c001d044e66436effa5470783571d77 | |
| parent | 3015ce0f9d120df7583f838d1a47cb469fe5a727 (diff) | |
fix: require plugin min herdr version
| -rw-r--r-- | docs/next/CHANGELOG.md | 2 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/cli-reference.mdx | 2 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/plugins.mdx | 10 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/socket-api.mdx | 4 | ||||
| -rw-r--r-- | src/api/schema/plugins.rs | 2 | ||||
| -rw-r--r-- | src/api/schema/tests.rs | 1 | ||||
| -rw-r--r-- | src/app/api/plugins/manifest.rs | 35 | ||||
| -rw-r--r-- | src/app/api/plugins/mod.rs | 74 | ||||
| -rw-r--r-- | src/cli/plugin.rs | 1 | ||||
| -rw-r--r-- | src/persist/plugin_registry.rs | 2 | ||||
| -rw-r--r-- | tests/cli_wrapper.rs | 19 | ||||
| -rw-r--r-- | tests/fixtures/plugin-smoke/herdr-plugin.toml | 1 |
12 files changed, 148 insertions, 5 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 57251781..267a033e 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -23,6 +23,8 @@ - Plugin runtime config directories now use stable, readable plugin-id paths instead of checkout hashes; existing legacy config directories are copied into the new location when first seen. +- Plugin manifests must now declare `min_herdr_version`; install and link fail + when a plugin requires a newer Herdr binary. - Public workspace, tab, and pane ids are now short stable handles such as `w1`, `w1:t1`, and `w1:p1`; closed tab and pane ids no longer retarget later resources. (#569) ### Fixed diff --git a/docs/next/website/src/content/docs/cli-reference.mdx b/docs/next/website/src/content/docs/cli-reference.mdx index 106823b3..35255393 100644 --- a/docs/next/website/src/content/docs/cli-reference.mdx +++ b/docs/next/website/src/content/docs/cli-reference.mdx @@ -282,7 +282,7 @@ herdr plugin enable <plugin_id> herdr plugin disable <plugin_id> ``` -`plugin install` accepts GitHub shorthand only, such as `ogulcancelik/herdr-plugin-examples/worktree-bootstrap`. It uses `git`, shows a trust preview in interactive terminals, runs supported manifest build commands, and stores GitHub installs in a Herdr-managed directory. Use `--yes` for noninteractive installs. Reinstalling a GitHub-managed plugin replaces that managed checkout. Installing over a locally linked plugin is refused. `plugin list` is human-readable by default; pass `--json` for the raw API response. +`plugin install` accepts GitHub shorthand only, such as `ogulcancelik/herdr-plugin-examples/worktree-bootstrap`. It uses `git`, shows a trust preview in interactive terminals, runs supported manifest build commands, and stores GitHub installs in a Herdr-managed directory. Use `--yes` for noninteractive installs. Reinstalling a GitHub-managed plugin replaces that managed checkout. Installing over a locally linked plugin is refused. Plugin manifests must declare `min_herdr_version`; install and link fail when the plugin requires a newer Herdr binary. `plugin list` is human-readable by default; pass `--json` for the raw API response. Local development: diff --git a/docs/next/website/src/content/docs/plugins.mdx b/docs/next/website/src/content/docs/plugins.mdx index cf5b4ff2..e892fd01 100644 --- a/docs/next/website/src/content/docs/plugins.mdx +++ b/docs/next/website/src/content/docs/plugins.mdx @@ -42,6 +42,7 @@ Herdr can run. id = "example.layout" name = "Layout" version = "0.1.0" +min_herdr_version = "0.6.10" description = "Apply project layouts" platforms = ["linux", "macos", "windows"] @@ -75,8 +76,12 @@ pattern = "^https://github\\.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$" action = "apply" ``` -Top-level `id`, `name`, and `version` are required. `description` is optional. -Plugin ids may use ASCII letters, digits, dot, colon, underscore, and hyphen. +Top-level `id`, `name`, `version`, and `min_herdr_version` are required. +Set `min_herdr_version` to the oldest Herdr version that supports the plugin +APIs, event names, and manifest fields your plugin uses. Herdr refuses to link +or install a plugin when its minimum version is newer than the current binary. +`description` is optional. Plugin ids may use ASCII letters, digits, dot, +colon, underscore, and hyphen. Action ids, pane ids, and link handler ids are local ids inside the plugin. They may use ASCII letters, digits, colon, underscore, and hyphen, but not @@ -107,6 +112,7 @@ my-plugin/ id = "example.workspace-tools" name = "Workspace Tools" version = "0.1.0" +min_herdr_version = "0.6.10" description = "Small workspace helpers" platforms = ["linux", "macos", "windows"] diff --git a/docs/next/website/src/content/docs/socket-api.mdx b/docs/next/website/src/content/docs/socket-api.mdx index 2bfe41df..0331d560 100644 --- a/docs/next/website/src/content/docs/socket-api.mdx +++ b/docs/next/website/src/content/docs/socket-api.mdx @@ -323,6 +323,7 @@ manifest path. The manifest shape is: id = "example.worktree-bootstrap" name = "Worktree Bootstrap" version = "0.1.0" +min_herdr_version = "0.6.10" description = "Prepare new worktrees" platforms = ["linux", "macos", "windows"] @@ -352,6 +353,9 @@ pattern = "^https://github\\.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$" action = "bootstrap" ``` +`min_herdr_version` is required. The server refuses to link a plugin when the +field is missing, invalid, or newer than the running Herdr binary. + Declare `platforms` at the top level with the OS identifiers (`linux`, `macos`, `windows`) your plugin supports. Omitting `platforms` is allowed for local development — `plugin.link` succeeds but the response includes a warning. diff --git a/src/api/schema/plugins.rs b/src/api/schema/plugins.rs index 907f243b..c809be58 100644 --- a/src/api/schema/plugins.rs +++ b/src/api/schema/plugins.rs @@ -37,6 +37,8 @@ pub struct InstalledPluginInfo { pub plugin_id: String, pub name: String, pub version: String, + #[serde(default)] + pub min_herdr_version: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option<String>, pub manifest_path: String, diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs index 05ac9291..3a3684e5 100644 --- a/src/api/schema/tests.rs +++ b/src/api/schema/tests.rs @@ -566,6 +566,7 @@ fn plugin_link_list_unlink_round_trip() { plugin_id: "example.worktree-bootstrap".into(), name: "Worktree Bootstrap".into(), version: "0.1.0".into(), + min_herdr_version: crate::build_info::BASE_VERSION.into(), description: Some("Prepare new worktrees".into()), manifest_path: "/plugins/worktree-bootstrap/herdr-plugin.toml".into(), plugin_root: "/plugins/worktree-bootstrap".into(), diff --git a/src/app/api/plugins/manifest.rs b/src/app/api/plugins/manifest.rs index 92799aae..92ab4754 100644 --- a/src/app/api/plugins/manifest.rs +++ b/src/app/api/plugins/manifest.rs @@ -13,6 +13,8 @@ struct RawPluginManifest { name: String, version: String, #[serde(default)] + min_herdr_version: Option<String>, + #[serde(default)] description: Option<String>, #[serde(default)] platforms: Option<Vec<RawPlatform>>, @@ -133,6 +135,7 @@ pub(crate) fn load_plugin_manifest( "invalid_plugin_version", "plugin version is required", )?; + let min_herdr_version = validate_min_herdr_version(raw.min_herdr_version.as_deref())?; let description = raw .description .map(|description| description.trim().to_string()) @@ -180,6 +183,7 @@ pub(crate) fn load_plugin_manifest( plugin_id, name, version, + min_herdr_version, description, manifest_path: manifest_path.display().to_string(), plugin_root: plugin_root.display().to_string(), @@ -195,6 +199,37 @@ pub(crate) fn load_plugin_manifest( }) } +fn validate_min_herdr_version(value: Option<&str>) -> Result<String, (&'static str, String)> { + let Some(value) = value else { + return Err(( + "invalid_plugin_min_herdr_version", + "plugin min_herdr_version is required".to_string(), + )); + }; + let value = non_empty_trimmed( + value, + "invalid_plugin_min_herdr_version", + "plugin min_herdr_version is required", + )?; + let required = crate::update::Version::parse(&value).ok_or_else(|| { + ( + "invalid_plugin_min_herdr_version", + format!( + "plugin min_herdr_version must be a semantic version like {}", + crate::build_info::BASE_VERSION + ), + ) + })?; + let current = crate::update::Version::current(); + if required > current { + return Err(( + "plugin_requires_newer_herdr", + format!("plugin requires Herdr {required} or newer; current Herdr is {current}"), + )); + } + Ok(required.to_string()) +} + fn normalize_manifest_build( build: RawPluginManifestBuild, ) -> Result<PluginManifestBuild, (&'static str, String)> { diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs index 89f7a23d..1938589c 100644 --- a/src/app/api/plugins/mod.rs +++ b/src/app/api/plugins/mod.rs @@ -704,6 +704,7 @@ mod tests { id = "example.worktree-bootstrap" name = "Worktree Bootstrap" version = "0.1.0" +min_herdr_version = "0.6.10" description = "Prepare new worktrees" platforms = ["linux", "macos", "windows"] @@ -772,6 +773,7 @@ action = "bootstrap" id = "example.config-dirs" name = "Config Dirs" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] "#, ); @@ -806,6 +808,7 @@ platforms = ["linux", "macos", "windows"] id = "example.legacy-config" name = "Legacy Config" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] "#, ); @@ -925,6 +928,56 @@ platforms = ["linux", "macos", "windows"] } #[test] + fn link_rejects_invalid_min_herdr_versions() { + let cases = [ + ( + "plugin-missing-min-herdr", + r#" +id = "example.missing-min-herdr" +name = "Missing Min Herdr" +version = "0.1.0" +platforms = ["linux", "macos", "windows"] +"#, + "invalid_plugin_min_herdr_version", + ), + ( + "plugin-invalid-min-herdr", + r#" +id = "example.invalid-min-herdr" +name = "Invalid Min Herdr" +version = "0.1.0" +min_herdr_version = "soon" +platforms = ["linux", "macos", "windows"] +"#, + "invalid_plugin_min_herdr_version", + ), + ( + "plugin-future-min-herdr", + r#" +id = "example.future-min-herdr" +name = "Future Min Herdr" +version = "0.1.0" +min_herdr_version = "999.0.0" +platforms = ["linux", "macos", "windows"] +"#, + "plugin_requires_newer_herdr", + ), + ]; + + for (name, manifest, expected_code) in cases { + let root = unique_temp_path(name); + write_manifest_content(&root, manifest); + + let result = load_plugin_manifest(&root.display().to_string(), true); + assert!( + matches!(result, Err((code, _)) if code == expected_code), + "{name}: expected {expected_code}, got {result:?}" + ); + let _ = std::fs::remove_dir_all(root); + } + } + + #[test] fn link_rejects_duplicate_action_ids() { let root = unique_temp_path("plugin-duplicate-action"); write_manifest_content( @@ -933,6 +986,7 @@ platforms = ["linux", "macos", "windows"] id = "example.duplicate" name = "Duplicate" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -961,6 +1015,7 @@ command = ["echo", "b"] id = "example.dotted-action" name = "Dotted Action" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -984,6 +1039,7 @@ command = ["echo", "build"] id = "example.duplicate-pane" name = "Duplicate Pane" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[panes]] @@ -1099,6 +1155,7 @@ command = ["echo", "b"] id = "example.pane" name = "Pane Plugin" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[panes]] @@ -1205,6 +1262,7 @@ command = ["sh", "-c", "printf '%s\n%s\n%s\n%s\n%s\n%s\n%s\n' \"$PWD\" \"$HERDR_ id = "example.path-env" name = "Path Env" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[panes]] @@ -1310,6 +1368,7 @@ command = ["sh", "-c", "printf '%s\n%s\n%s\n' \"$HERDR_PLUGIN_ROOT\" \"$HERDR_PL id = "example.tab" name = "Tab Plugin" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[panes]] @@ -1514,6 +1573,7 @@ command = ["sh", "-c", "sleep 1"] id = "example.runner" name = "Runner" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[actions]] @@ -1580,6 +1640,7 @@ command = ["sh", "-c", "printf '%s' \"$HERDR_PLUGIN_ACTION_ID\""] id = "example.action-paths" name = "Action Paths" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[actions]] @@ -1702,6 +1763,7 @@ command = ["sh", "-c", "printf '%s\n%s\n%s' \"$HERDR_PLUGIN_ROOT\" \"$HERDR_PLUG id = "example.event-context" name = "Event Context" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[events]] @@ -1851,6 +1913,7 @@ command = ["sh", "-c", "printf '%s' \"$HERDR_PLUGIN_CONTEXT_JSON\" > {}"] id = "example.links" name = "Links" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[actions]] @@ -1918,6 +1981,7 @@ action = "open" id = "example.link-order" name = "Link Order" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -1964,6 +2028,7 @@ action = "generic" id = "example.bad-links" name = "Bad Links" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -2006,6 +2071,7 @@ action = "open" id = "example.bad-link-action" name = "Bad Link Action" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -2079,6 +2145,7 @@ action = "missing" id = "example.context" name = "Context" version = "0.1.0" +min_herdr_version = "0.6.10" [[actions]] id = "show" @@ -2175,6 +2242,7 @@ command = ["show-ctx"] id = "example.bad-event" name = "Bad Event Plugin" version = "0.1.0" +min_herdr_version = "0.6.10" [[events]] on = "worktree.craeted" @@ -2458,6 +2526,7 @@ command = ["sh", "-c", "echo ok"] id = "example.platforms" name = "Platforms" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[actions]] @@ -2540,6 +2609,7 @@ command = ["run.bat"] id = "example.reject" name = "Reject" version = "0.1.0" +min_herdr_version = "0.6.10" {excluded_platforms} [[actions]] @@ -2604,6 +2674,7 @@ command = ["act"] id = "example.override" name = "Override" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -2654,6 +2725,7 @@ command = ["act"] id = "example.nodecl" name = "No Decl" version = "0.1.0" +min_herdr_version = "0.6.10" [[actions]] id = "act" @@ -2711,6 +2783,7 @@ command = ["act"] id = "example.badplatform" name = "Bad Platform" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "beos"] [[actions]] @@ -2742,6 +2815,7 @@ command = ["act"] id = "example.platform-rt" name = "Platform RT" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos"] [[actions]] diff --git a/src/cli/plugin.rs b/src/cli/plugin.rs index ffdcd711..261540c7 100644 --- a/src/cli/plugin.rs +++ b/src/cli/plugin.rs @@ -1631,6 +1631,7 @@ mod tests { plugin_id: id.to_string(), name: "Test Plugin".to_string(), version: "0.1.0".to_string(), + min_herdr_version: crate::build_info::BASE_VERSION.to_string(), description: None, manifest_path: format!("/tmp/{id}/herdr-plugin.toml"), plugin_root: format!("/tmp/{id}"), diff --git a/src/persist/plugin_registry.rs b/src/persist/plugin_registry.rs index b2eaac9f..34796a61 100644 --- a/src/persist/plugin_registry.rs +++ b/src/persist/plugin_registry.rs @@ -116,6 +116,7 @@ mod tests { plugin_id: id.to_string(), name: "Test Plugin".to_string(), version: "0.1.0".to_string(), + min_herdr_version: crate::build_info::BASE_VERSION.to_string(), description: None, manifest_path: format!("/tmp/{id}/herdr-plugin.toml"), plugin_root: format!("/tmp/{id}"), @@ -199,6 +200,7 @@ mod tests { plugin_id: "example.reload".to_string(), name: "Fresh Name".to_string(), version: "0.2.0".to_string(), + min_herdr_version: crate::build_info::BASE_VERSION.to_string(), description: Some("refreshed".to_string()), manifest_path: "/tmp/example.reload/herdr-plugin.toml".to_string(), plugin_root: "/tmp/example.reload".to_string(), diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index 95d14860..33cea043 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -2889,6 +2889,7 @@ fn plugin_link_list_unlink_cli_smoke_test() { id = "example.layout" name = "Layout" version = "0.1.0" +min_herdr_version = "0.6.10" description = "Apply a preferred Herdr layout" [[actions]] @@ -3029,6 +3030,7 @@ fn plugin_install_list_uninstall_offline_cli_smoke_test() { id = "example.worktree-bootstrap" name = "Worktree Bootstrap" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[build]] @@ -3158,6 +3160,7 @@ fn plugin_install_build_failure_does_not_register_or_create_checkout() { id = "example.build-fail" name = "Build Fail" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[build]] @@ -3251,6 +3254,7 @@ fn plugin_install_build_spawn_failure_prints_clean_error() { id = "example.missing-tool" name = "Missing Tool" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[build]] @@ -3339,6 +3343,7 @@ fn plugin_install_rejects_manifest_changed_by_build() { id = "example.manifest-mutator" name = "Manifest Mutator" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[build]] @@ -3357,12 +3362,16 @@ command = ["sh", "-c", "echo reviewed"] id = "example.manifest-mutator" name = "Manifest Mutator" version = "0.1.0" +min_herdr_version = "0.0.1" platforms = ["linux", "macos", "windows"] +[[build]] +command = ["sh", "mutate.sh"] + [[actions]] id = "run" -title = "Run changed command" -command = ["sh", "-c", "echo changed"] +title = "Run reviewed command" +command = ["sh", "-c", "echo reviewed"] EOF "#, ) @@ -3439,6 +3448,7 @@ fn plugin_install_restores_previous_checkout_when_registration_fails() { id = "example.worktree-bootstrap" name = "Worktree Bootstrap" version = "0.2.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -3494,6 +3504,7 @@ command = ["sh", "-c", "echo new"] "plugin_id": "example.worktree-bootstrap", "name": "Worktree Bootstrap", "version": "0.1.0", + "min_herdr_version": "0.6.10", "manifest_path": managed_checkout_for_server.join("herdr-plugin.toml").display().to_string(), "plugin_root": managed_checkout_for_server.display().to_string(), "enabled": true, @@ -3569,6 +3580,7 @@ fn plugin_install_rejects_server_that_drops_source_metadata() { id = "example.worktree-bootstrap" name = "Worktree Bootstrap" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -3633,6 +3645,7 @@ command = ["sh", "-c", "echo install"] "plugin_id": "example.worktree-bootstrap", "name": "Worktree Bootstrap", "version": "0.1.0", + "min_herdr_version": "0.6.10", "manifest_path": managed_checkout_for_server.join("herdr-plugin.toml").display().to_string(), "plugin_root": managed_checkout_for_server.display().to_string(), "enabled": true, @@ -3704,6 +3717,7 @@ fn plugin_install_keeps_checkout_when_incompatible_server_cleanup_fails() { id = "example.worktree-bootstrap" name = "Worktree Bootstrap" version = "0.1.0" +min_herdr_version = "0.6.10" platforms = ["linux", "macos", "windows"] [[actions]] @@ -3764,6 +3778,7 @@ command = ["sh", "-c", "echo install"] "plugin_id": "example.worktree-bootstrap", "name": "Worktree Bootstrap", "version": "0.1.0", + "min_herdr_version": "0.6.10", "manifest_path": managed_checkout_for_server.join("herdr-plugin.toml").display().to_string(), "plugin_root": managed_checkout_for_server.display().to_string(), "enabled": true, diff --git a/tests/fixtures/plugin-smoke/herdr-plugin.toml b/tests/fixtures/plugin-smoke/herdr-plugin.toml index 20794c4b..013e1189 100644 --- a/tests/fixtures/plugin-smoke/herdr-plugin.toml +++ b/tests/fixtures/plugin-smoke/herdr-plugin.toml @@ -1,6 +1,7 @@ id = "example.smoke" name = "Smoke Plugin" version = "0.1.0" +min_herdr_version = "0.6.10" description = "Portable plugin smoke fixture" platforms = ["linux", "macos", "windows"] |
