diff --git a/assets/build/deploy.mk b/assets/build/deploy.mk index 7c05b31..9b37e21 100644 --- a/assets/build/deploy.mk +++ b/assets/build/deploy.mk @@ -5,7 +5,7 @@ ## Builds and installs the plugin to a server. .PHONY: deploy deploy: dist - ./build/bin/pluginctl deploy --bundle-path dist/$(BUNDLE_NAME) + pluginctl deploy --bundle-path dist/$(BUNDLE_NAME) ## Builds and installs the plugin to a server, updating the webapp automatically when changed. .PHONY: watch diff --git a/cmd/pluginctl/main.go b/cmd/pluginctl/main.go index 1031cbb..ff96fc2 100644 --- a/cmd/pluginctl/main.go +++ b/cmd/pluginctl/main.go @@ -52,6 +52,8 @@ func runCommand(command string, args []string, pluginPath string) error { return runDisableCommand(args, pluginPath) case "reset": return runResetCommand(args, pluginPath) + case "deploy": + return runDeployCommand(args, pluginPath) case "updateassets": return runUpdateAssetsCommand(args, pluginPath) case "manifest": @@ -103,6 +105,10 @@ func runLogsCommand(args []string, pluginPath string) error { return pluginctl.RunLogsCommand(args, pluginPath) } +func runDeployCommand(args []string, pluginPath string) error { + return pluginctl.RunDeployCommand(args, pluginPath) +} + func showUsage() { usageText := `pluginctl - Mattermost Plugin Development CLI @@ -117,6 +123,7 @@ Commands: enable Enable plugin from current directory in Mattermost server disable Disable plugin from current directory in Mattermost server reset Reset plugin from current directory (disable then enable) + deploy Upload and enable plugin bundle to Mattermost server updateassets Update plugin files from embedded assets manifest Get plugin manifest information (id, version, has_server, has_webapp, check) logs View plugin logs (use --watch to follow logs in real-time) @@ -129,6 +136,8 @@ Examples: pluginctl enable # Enable plugin from current directory pluginctl disable # Disable plugin from current directory pluginctl reset # Reset plugin from current directory (disable then enable) + pluginctl deploy # Upload and enable plugin bundle from ./dist/ + pluginctl deploy --bundle-path ./bundle.tar.gz # Deploy specific bundle file pluginctl updateassets # Update plugin files from embedded assets pluginctl manifest id # Get plugin ID pluginctl manifest version # Get plugin version diff --git a/deploy.go b/deploy.go new file mode 100644 index 0000000..8997c39 --- /dev/null +++ b/deploy.go @@ -0,0 +1,98 @@ +package pluginctl + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/mattermost/mattermost/server/public/model" +) + +func RunDeployCommand(args []string, pluginPath string) error { + var bundlePath string + + // Parse flags + i := 0 + for i < len(args) { + switch args[i] { + case "--bundle-path": + if i+1 >= len(args) { + return fmt.Errorf("--bundle-path flag requires a value") + } + bundlePath = args[i+1] + i += 2 + default: + i++ + } + } + + // If no bundle path provided, auto-discover from dist folder + if bundlePath == "" { + manifest, err := LoadPluginManifestFromPath(pluginPath) + if err != nil { + return fmt.Errorf("failed to load plugin manifest: %w", err) + } + + expectedBundleName := fmt.Sprintf("%s-%s.tar.gz", manifest.Id, manifest.Version) + bundlePath = filepath.Join(pluginPath, "dist", expectedBundleName) + + if _, err := os.Stat(bundlePath); os.IsNotExist(err) { + return fmt.Errorf("bundle not found at %s - run 'make bundle' to build the plugin first", bundlePath) + } + } + + // Validate bundle file exists + if _, err := os.Stat(bundlePath); os.IsNotExist(err) { + return fmt.Errorf("bundle file not found: %s", bundlePath) + } + + // Load manifest to get plugin ID + manifest, err := LoadPluginManifestFromPath(pluginPath) + if err != nil { + return fmt.Errorf("failed to load plugin manifest: %w", err) + } + + pluginID := manifest.Id + if pluginID == "" { + return fmt.Errorf("plugin ID not found in manifest") + } + + ctx, cancel := context.WithTimeout(context.Background(), commandTimeout) + defer cancel() + + client, err := getClient(ctx) + if err != nil { + return err + } + + return deployPlugin(ctx, client, pluginID, bundlePath) +} + +func deployPlugin(ctx context.Context, client *model.Client4, pluginID, bundlePath string) error { + pluginBundle, err := os.Open(bundlePath) + if err != nil { + return fmt.Errorf("failed to open bundle file %s: %w", bundlePath, err) + } + defer func() { + if closeErr := pluginBundle.Close(); closeErr != nil { + Logger.Error("Failed to close plugin bundle", "error", closeErr) + } + }() + + Logger.Info("Uploading plugin bundle", "bundle_path", bundlePath) + _, _, err = client.UploadPluginForced(ctx, pluginBundle) + if err != nil { + return fmt.Errorf("failed to upload plugin bundle: %w", err) + } + + Logger.Info("Enabling plugin", "plugin_id", pluginID) + _, err = client.EnablePlugin(ctx, pluginID) + if err != nil { + return fmt.Errorf("failed to enable plugin: %w", err) + } + + Logger.Info("Plugin deployed successfully", "plugin_id", pluginID) + + return nil +}