feat(plugins): implement protocol version negotiation in server
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 6m51s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m6s
Build and Release / Lint (push) Successful in 7m42s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m9s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m31s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m54s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m40s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m9s
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 6m51s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m6s
Build and Release / Lint (push) Successful in 7m42s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m9s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m31s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 5m54s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m40s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m9s
Complete protocol versioning implementation on the server side, enabling forward-compatible plugin protocol evolution. Server Changes: - Send ProtocolVersion = 1 in InitializeRequest - Store plugin's reported protocol version in ManagedPlugin - Treat version 0 (pre-versioning plugins) as version 1 - Add SupportsProtocol() method to check before calling version-gated RPCs - Log plugin's protocol version during initialization Generated Code: - Regenerate plugin.pb.go with protocol_version fields - Add getter methods for new fields Documentation: - Add Protocol Versioning section to PLUGINS.md - Explain version negotiation flow - Document when plugins need to update vs. when they don't - Add version history table (currently only v1) - Update all example code to return protocol_version = 1 Benefits: - Server can safely add new RPCs in future versions without breaking old plugins - Plugins can detect newer servers and opt into advanced features - Zero-value (0) provides backwards compatibility with pre-versioning plugins - Clear upgrade path documented for plugin developers This completes the protocol versioning feature started in the previous commit.
This commit is contained in:
33
PLUGINS.md
33
PLUGINS.md
@@ -14,6 +14,7 @@ This guide explains how to build external plugins for GitCaddy. Plugins are stan
|
||||
- [Events](#events)
|
||||
- [Permissions](#permissions)
|
||||
- [Health Monitoring](#health-monitoring)
|
||||
- [Protocol Versioning](#protocol-versioning)
|
||||
- [Configuration](#configuration)
|
||||
- [External Mode](#external-mode)
|
||||
- [Managed Mode](#managed-mode)
|
||||
@@ -96,6 +97,7 @@ Plugin process is sent SIGINT (managed mode only)
|
||||
|-------|------|-------------|
|
||||
| `server_version` | string | The GitCaddy server version (e.g., `"3.0.0"`) |
|
||||
| `config` | map<string, string> | Server-provided configuration key-value pairs |
|
||||
| `protocol_version` | int32 | Plugin protocol version the server supports (current: `1`). `0` means pre-versioning. |
|
||||
|
||||
#### InitializeResponse
|
||||
|
||||
@@ -104,6 +106,7 @@ Plugin process is sent SIGINT (managed mode only)
|
||||
| `success` | bool | Whether initialization succeeded |
|
||||
| `error` | string | Error message if `success` is false |
|
||||
| `manifest` | PluginManifest | The plugin's capability manifest |
|
||||
| `protocol_version` | int32 | Plugin protocol version the plugin supports (current: `1`). `0` means pre-versioning, treated as `1`. |
|
||||
|
||||
#### HealthCheckRequest
|
||||
|
||||
@@ -222,6 +225,28 @@ If `HealthCheckResponse.healthy` is `false` (the RPC succeeds but the plugin rep
|
||||
|
||||
**Health check timeout** is configured per-plugin via `HEALTH_TIMEOUT` (default: 5 seconds).
|
||||
|
||||
## Protocol Versioning
|
||||
|
||||
The plugin protocol uses explicit version negotiation to ensure forward compatibility. Both the server and plugin exchange their supported protocol version during `Initialize`:
|
||||
|
||||
1. The server sends `protocol_version = 1` in `InitializeRequest`
|
||||
2. The plugin returns `protocol_version = 1` in `InitializeResponse`
|
||||
3. The server stores the plugin's version and checks it before calling any RPCs added in later versions
|
||||
|
||||
**What this means for plugin developers:**
|
||||
|
||||
- **You don't need to recompile** when the server adds new fields to existing messages. Protobuf handles this automatically — unknown fields are ignored, missing fields use zero-value defaults.
|
||||
- **You don't need to recompile** when the server adds new event types. Your plugin only receives events it subscribed to.
|
||||
- **You only need to update** if you want to use features from a newer protocol version (e.g., new RPCs added in protocol v2).
|
||||
|
||||
**Version history:**
|
||||
|
||||
| Version | RPCs | Notes |
|
||||
|---------|------|-------|
|
||||
| 1 | Initialize, Shutdown, HealthCheck, GetManifest, OnEvent, HandleHTTP | Initial release |
|
||||
|
||||
**Pre-versioning plugins** (those that don't set `protocol_version` in their response) return `0`, which the server treats as version `1`. This means all existing plugins are compatible without changes.
|
||||
|
||||
## Configuration
|
||||
|
||||
Plugins are configured in the server's `app.ini`.
|
||||
@@ -330,9 +355,10 @@ func (p *myPlugin) Initialize(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pluginv1.InitializeRequest],
|
||||
) (*connect.Response[pluginv1.InitializeResponse], error) {
|
||||
log.Printf("Initialized by server %s", req.Msg.ServerVersion)
|
||||
log.Printf("Initialized by server %s (protocol v%d)", req.Msg.ServerVersion, req.Msg.ProtocolVersion)
|
||||
return connect.NewResponse(&pluginv1.InitializeResponse{
|
||||
Success: true,
|
||||
Success: true,
|
||||
ProtocolVersion: 1,
|
||||
Manifest: &pluginv1.PluginManifest{
|
||||
Name: "My Plugin",
|
||||
Version: "1.0.0",
|
||||
@@ -416,6 +442,7 @@ public class MyPlugin : PluginService.PluginServiceBase
|
||||
return Task.FromResult(new InitializeResponse
|
||||
{
|
||||
Success = true,
|
||||
ProtocolVersion = 1,
|
||||
Manifest = manifest
|
||||
});
|
||||
}
|
||||
@@ -479,7 +506,7 @@ class MyPlugin(plugin_pb2_grpc.PluginServiceServicer):
|
||||
description="A Python plugin for GitCaddy",
|
||||
subscribed_events=["repo:push"],
|
||||
)
|
||||
return plugin_pb2.InitializeResponse(success=True, manifest=manifest)
|
||||
return plugin_pb2.InitializeResponse(success=True, protocol_version=1, manifest=manifest)
|
||||
|
||||
def HealthCheck(self, request, context):
|
||||
return plugin_pb2.HealthCheckResponse(
|
||||
|
||||
Reference in New Issue
Block a user