2
0

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

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:
2026-02-13 02:18:20 -05:00
parent e932582f54
commit de7d6a719e
4 changed files with 235 additions and 150 deletions

View File

@@ -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(