pluginctl/pluginctl.go
Felipe Martin 353cc9efc7
Add version tracking and validation to prevent downgrade issues
Store the last pluginctl version used in plugin.json props and validate
before running commands. Prevents issues when using older pluginctl
versions on plugins modified by newer versions.

- Add Version field to PluginCtlConfig struct
- Implement version validation before command execution
- Add WritePluginManifest and SavePluginCtlConfig helper functions
- Add comprehensive tests for version comparison logic
- Skip validation for 'version' command and when no plugin.json exists

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-31 11:54:46 +02:00

169 lines
4.4 KiB
Go

package pluginctl
import (
"encoding/json"
"fmt"
"os"
"github.com/mattermost/mattermost/server/public/model"
)
const (
HelpFlagLong = "--help"
HelpFlagShort = "-h"
VersionCommand = "version"
DevVersion = "dev"
UnknownVersion = "unknown"
)
// IsValidPluginDirectory checks if the current directory contains a valid plugin.
func IsValidPluginDirectory() error {
_, err := LoadPluginManifest()
return err
}
// GetEffectivePluginPath determines the plugin path from flag, environment variable, or current directory.
func GetEffectivePluginPath(flagPath string) string {
const EnvPluginPath = "PLUGINCTL_PLUGIN_PATH"
// Priority: 1. Command line flag, 2. Environment variable, 3. Current directory
if flagPath != "" {
return flagPath
}
if envPath := os.Getenv(EnvPluginPath); envPath != "" {
return envPath
}
// Default to current directory
cwd, err := os.Getwd()
if err != nil {
return "."
}
return cwd
}
// ParsePluginCtlConfig extracts and parses the pluginctl configuration from the manifest props.
func ParsePluginCtlConfig(manifest *model.Manifest) (*PluginCtlConfig, error) {
// Default configuration
config := &PluginCtlConfig{
IgnoreAssets: []string{},
}
// Check if props exist
if manifest.Props == nil {
return config, nil
}
// Check if pluginctl config exists in props
pluginctlData, exists := manifest.Props["pluginctl"]
if !exists {
return config, nil
}
// Convert to JSON and parse
jsonData, err := json.Marshal(pluginctlData)
if err != nil {
return nil, fmt.Errorf("failed to marshal pluginctl config: %w", err)
}
if err := json.Unmarshal(jsonData, config); err != nil {
return nil, fmt.Errorf("failed to parse pluginctl config: %w", err)
}
return config, nil
}
// CheckForHelpFlag checks if --help is in the arguments and shows help if found.
// Returns true if help was shown, false otherwise.
func CheckForHelpFlag(args []string, helpText string) bool {
for _, arg := range args {
if arg == HelpFlagLong || arg == HelpFlagShort {
Logger.Info(helpText)
return true
}
}
return false
}
// ShowErrorWithHelp displays an error message followed by command help.
func ShowErrorWithHelp(errorMsg, helpText string) error {
Logger.Error(errorMsg)
Logger.Info(helpText)
return fmt.Errorf("%s", errorMsg)
}
// SavePluginCtlConfig saves the pluginctl configuration to the manifest props.
func SavePluginCtlConfig(manifest *model.Manifest, config *PluginCtlConfig) {
if manifest.Props == nil {
manifest.Props = make(map[string]interface{})
}
manifest.Props["pluginctl"] = config
}
// ValidateAndUpdateVersion checks the plugin version and updates it if necessary.
func ValidateAndUpdateVersion(pluginPath string) error {
// Load the manifest
manifest, err := LoadPluginManifestFromPath(pluginPath)
if err != nil {
// If there's no plugin.json, skip version validation
Logger.Debug("No plugin.json found, skipping version validation", "error", err)
return nil
}
// Get current pluginctl version
currentVersion := GetVersion()
// Parse existing pluginctl config
config, err := ParsePluginCtlConfig(manifest)
if err != nil {
return fmt.Errorf("failed to parse pluginctl config: %w", err)
}
// Check if stored version is higher than current version
if config.Version != "" && isVersionHigher(config.Version, currentVersion) {
return fmt.Errorf("plugin was last modified with pluginctl version %s, "+
"which is higher than current version %s. Please upgrade pluginctl",
config.Version, currentVersion)
}
// Update version if different
if config.Version != currentVersion {
config.Version = currentVersion
SavePluginCtlConfig(manifest, config)
// Save the updated manifest
if err := WritePluginManifest(manifest, pluginPath); err != nil {
return fmt.Errorf("failed to update version in manifest: %w", err)
}
}
return nil
}
// isVersionHigher compares two version strings and returns true if the stored version is higher
// than the current version.
func isVersionHigher(storedVersion, currentVersion string) bool {
// Handle special cases
if storedVersion == currentVersion {
return false
}
if currentVersion == DevVersion || currentVersion == UnknownVersion {
return false
}
if storedVersion == DevVersion || storedVersion == UnknownVersion {
return false
}
// Simple string comparison for versions
// This is a basic implementation - in production you might want semantic versioning
return storedVersion > currentVersion
}