From 04fa4154b3e3cacd8b48cb81d9bd7c6a3c23cff0 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 14 Jul 2025 21:33:58 +0200 Subject: [PATCH 1/2] Upgrade updateassets command to use templates with manifest context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert all asset files to be processed as Go templates - Expose plugin manifest data via .Manifest key in template context - Add template processing infrastructure with TemplateContext struct - Update asset processing pipeline to execute templates instead of direct file copying - Fix info command output format to use plain text instead of structured logging - All tests passing with proper error handling and backward compatibility This enables dynamic asset files that can access plugin manifest properties like {{.Manifest.Id}}, {{.Manifest.Version}}, and conditional logic based on plugin configuration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- info.go | 41 +++++++++++++++++------------- updateassets.go | 67 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 31 deletions(-) diff --git a/info.go b/info.go index 24a12df..fb1e3b8 100644 --- a/info.go +++ b/info.go @@ -28,28 +28,28 @@ func PrintPluginInfo(manifest *model.Manifest) error { // printBasicInfo prints basic plugin information. func printBasicInfo(manifest *model.Manifest) { - Logger.Info("Plugin Information:") - Logger.Info("==================") + fmt.Println("Plugin Information:") + fmt.Println("==================") - Logger.Info("ID:", "value", manifest.Id) - Logger.Info("Name:", "value", manifest.Name) - Logger.Info("Version:", "value", manifest.Version) + fmt.Printf("ID: %s\n", manifest.Id) + fmt.Printf("Name: %s\n", manifest.Name) + fmt.Printf("Version: %s\n", manifest.Version) minVersion := manifest.MinServerVersion if minVersion == "" { minVersion = "Not specified" } - Logger.Info("Min MM Version:", "value", minVersion) + fmt.Printf("Min MM Version: %s\n", minVersion) if manifest.Description != "" { - Logger.Info("Description:", "value", manifest.Description) + fmt.Printf("Description: %s\n", manifest.Description) } } // printCodeComponents prints information about server and webapp code. func printCodeComponents(manifest *model.Manifest) { - Logger.Info("Code Components:") - Logger.Info("================") + fmt.Println("Code Components:") + fmt.Println("================") printServerCodeInfo(manifest) printWebappCodeInfo(manifest) @@ -58,28 +58,33 @@ func printCodeComponents(manifest *model.Manifest) { // printServerCodeInfo prints server code information. func printServerCodeInfo(manifest *model.Manifest) { if HasServerCode(manifest) { - Logger.Info("Server Code:", "value", "Yes") + fmt.Println("Server Code: Yes") if manifest.Server != nil && len(manifest.Server.Executables) > 0 { - var executables []string + fmt.Print("Executables: ") + first := true for platform := range manifest.Server.Executables { - executables = append(executables, platform) + if !first { + fmt.Print(", ") + } + fmt.Print(platform) + first = false } - Logger.Info("Executables:", "platforms", executables) + fmt.Println() } } else { - Logger.Info("Server Code:", "value", "No") + fmt.Println("Server Code: No") } } // printWebappCodeInfo prints webapp code information. func printWebappCodeInfo(manifest *model.Manifest) { if HasWebappCode(manifest) { - Logger.Info("Webapp Code:", "value", "Yes") + fmt.Println("Webapp Code: Yes") if manifest.Webapp != nil && manifest.Webapp.BundlePath != "" { - Logger.Info("Bundle Path:", "value", manifest.Webapp.BundlePath) + fmt.Printf("Bundle Path: %s\n", manifest.Webapp.BundlePath) } } else { - Logger.Info("Webapp Code:", "value", "No") + fmt.Println("Webapp Code: No") } } @@ -89,7 +94,7 @@ func printSettingsSchema(manifest *model.Manifest) { if manifest.SettingsSchema != nil { value = "Yes" } - Logger.Info("Settings Schema:", "value", value) + fmt.Printf("Settings Schema: %s\n", value) } // InfoCommandWithPath implements the 'info' command with a custom path. diff --git a/updateassets.go b/updateassets.go index 60196cd..69ff444 100644 --- a/updateassets.go +++ b/updateassets.go @@ -8,6 +8,9 @@ import ( "os" "path/filepath" "strings" + "text/template" + + "github.com/mattermost/mattermost/server/public/model" ) //go:embed assets/* @@ -45,6 +48,7 @@ func RunUpdateAssetsCommand(args []string, pluginPath string) error { hasWebapp: hasWebapp, updatedCount: &updatedCount, pluginCtlConfig: pluginCtlConfig, + manifest: manifest, } err = fs.WalkDir(assetsFS, "assets", func(path string, d fs.DirEntry, err error) error { @@ -121,6 +125,12 @@ type AssetProcessorConfig struct { hasWebapp bool updatedCount *int pluginCtlConfig *PluginCtlConfig + manifest *model.Manifest +} + +// TemplateContext holds the data available to templates. +type TemplateContext struct { + Manifest *model.Manifest } func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProcessorConfig) error { @@ -151,21 +161,21 @@ func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProces return createDirectory(targetPath) } - return processAssetFile(path, targetPath, relativePath, config.updatedCount) + return processAssetFile(path, targetPath, relativePath, config) } -func processAssetFile(embeddedPath, targetPath, relativePath string, updatedCount *int) error { - shouldUpdate, err := shouldUpdateFile(embeddedPath, targetPath) +func processAssetFile(embeddedPath, targetPath, relativePath string, config AssetProcessorConfig) error { + shouldUpdate, err := shouldUpdateFile(embeddedPath, targetPath, config.manifest) if err != nil { return err } if shouldUpdate { - err = updateFile(embeddedPath, targetPath, relativePath) + err = updateFile(embeddedPath, targetPath, relativePath, config.manifest) if err != nil { return err } - (*updatedCount)++ + (*config.updatedCount)++ } return nil @@ -179,10 +189,11 @@ func createDirectory(targetPath string) error { return nil } -func shouldUpdateFile(embeddedPath, targetPath string) (bool, error) { - content, err := assetsFS.ReadFile(embeddedPath) +func shouldUpdateFile(embeddedPath, targetPath string, manifest *model.Manifest) (bool, error) { + // Process the template to get the final content + processedContent, err := processTemplate(embeddedPath, manifest) if err != nil { - return false, fmt.Errorf("failed to read embedded file %s: %w", embeddedPath, err) + return false, fmt.Errorf("failed to process template %s: %w", embeddedPath, err) } existingContent, err := os.ReadFile(targetPath) @@ -191,13 +202,14 @@ func shouldUpdateFile(embeddedPath, targetPath string) (bool, error) { return true, nil //nolint:nilerr } - return !bytes.Equal(existingContent, content), nil + return !bytes.Equal(existingContent, processedContent), nil } -func updateFile(embeddedPath, targetPath, relativePath string) error { - content, err := assetsFS.ReadFile(embeddedPath) +func updateFile(embeddedPath, targetPath, relativePath string, manifest *model.Manifest) error { + // Process the template to get the final content + processedContent, err := processTemplate(embeddedPath, manifest) if err != nil { - return fmt.Errorf("failed to read embedded file %s: %w", embeddedPath, err) + return fmt.Errorf("failed to process template %s: %w", embeddedPath, err) } parentDir := filepath.Dir(targetPath) @@ -205,7 +217,7 @@ func updateFile(embeddedPath, targetPath, relativePath string) error { return fmt.Errorf("failed to create parent directory %s: %w", parentDir, err) } - if err := os.WriteFile(targetPath, content, filePermissions); err != nil { + if err := os.WriteFile(targetPath, processedContent, filePermissions); err != nil { return fmt.Errorf("failed to write file %s: %w", targetPath, err) } @@ -213,3 +225,32 @@ func updateFile(embeddedPath, targetPath, relativePath string) error { return nil } + +// processTemplate processes a template file with the manifest context. +func processTemplate(embeddedPath string, manifest *model.Manifest) ([]byte, error) { + // Read the template content + templateContent, err := assetsFS.ReadFile(embeddedPath) + if err != nil { + return nil, fmt.Errorf("failed to read embedded file %s: %w", embeddedPath, err) + } + + // Create template context + context := TemplateContext{ + Manifest: manifest, + } + + // Create and parse the template + tmpl, err := template.New(embeddedPath).Parse(string(templateContent)) + if err != nil { + return nil, fmt.Errorf("failed to parse template %s: %w", embeddedPath, err) + } + + // Execute the template + var buf bytes.Buffer + err = tmpl.Execute(&buf, context) + if err != nil { + return nil, fmt.Errorf("failed to execute template %s: %w", embeddedPath, err) + } + + return buf.Bytes(), nil +} From 2e2a95d7d6d8e70b1aeb81779370628770ae99bc Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Mon, 14 Jul 2025 22:16:41 +0200 Subject: [PATCH 2/2] Add GoModule support to template context and update gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GoModule struct with Module and Version fields - Parse go.mod file to extract module name and Go version - Expose GoModule in template context as {{.GoModule}} - Update asset templates to use {{.GoModule}} instead of hardcoded values - Add gitignore pattern for testdata directories (keep only plugin.json files) - All templates now have access to both manifest and Go module information Templates can now use: - {{.GoModule.Module}} for module name - {{.GoModule.Version}} for Go version - {{if .GoModule}}...{{end}} for conditional logic 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 5 ++++ assets/.golangci.yml | 2 +- assets/build/utils.mk | 2 +- updateassets.go | 62 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 028d8fd..8dc59ef 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,8 @@ Thumbs.db .env .env.local .claude + +# Ignore all files in testdata except plugin.json +testdata/**/* +!testdata/**/ +!testdata/**/plugin.json diff --git a/assets/.golangci.yml b/assets/.golangci.yml index 5682ada..59726f7 100644 --- a/assets/.golangci.yml +++ b/assets/.golangci.yml @@ -6,7 +6,7 @@ linters-settings: gofmt: simplify: true goimports: - local-prefixes: github.com/mattermost/mattermost-starter-template + local-prefixes: {{.GoModule}} govet: check-shadowing: true enable-all: true diff --git a/assets/build/utils.mk b/assets/build/utils.mk index 3dd5c52..eb44805 100644 --- a/assets/build/utils.mk +++ b/assets/build/utils.mk @@ -33,7 +33,7 @@ endif mock: ifneq ($(HAS_SERVER),) go install github.com/golang/mock/mockgen@v1.6.0 - mockgen -destination=server/command/mocks/mock_commands.go -package=mocks github.com/mattermost/mattermost-plugin-starter-template/server/command Command + mockgen -destination=server/command/mocks/mock_commands.go -package=mocks {{.GoModule}}/server/command Command endif ## Show help documentation. diff --git a/updateassets.go b/updateassets.go index 69ff444..705c1db 100644 --- a/updateassets.go +++ b/updateassets.go @@ -128,9 +128,16 @@ type AssetProcessorConfig struct { manifest *model.Manifest } +// GoModule represents information from go.mod file. +type GoModule struct { + Module string + Version string +} + // TemplateContext holds the data available to templates. type TemplateContext struct { Manifest *model.Manifest + GoModule *GoModule } func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProcessorConfig) error { @@ -165,13 +172,13 @@ func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProces } func processAssetFile(embeddedPath, targetPath, relativePath string, config AssetProcessorConfig) error { - shouldUpdate, err := shouldUpdateFile(embeddedPath, targetPath, config.manifest) + shouldUpdate, err := shouldUpdateFile(embeddedPath, targetPath, config) if err != nil { return err } if shouldUpdate { - err = updateFile(embeddedPath, targetPath, relativePath, config.manifest) + err = updateFile(embeddedPath, targetPath, relativePath, config) if err != nil { return err } @@ -189,9 +196,9 @@ func createDirectory(targetPath string) error { return nil } -func shouldUpdateFile(embeddedPath, targetPath string, manifest *model.Manifest) (bool, error) { +func shouldUpdateFile(embeddedPath, targetPath string, config AssetProcessorConfig) (bool, error) { // Process the template to get the final content - processedContent, err := processTemplate(embeddedPath, manifest) + processedContent, err := processTemplate(embeddedPath, config.manifest, config.pluginPath) if err != nil { return false, fmt.Errorf("failed to process template %s: %w", embeddedPath, err) } @@ -205,9 +212,9 @@ func shouldUpdateFile(embeddedPath, targetPath string, manifest *model.Manifest) return !bytes.Equal(existingContent, processedContent), nil } -func updateFile(embeddedPath, targetPath, relativePath string, manifest *model.Manifest) error { +func updateFile(embeddedPath, targetPath, relativePath string, config AssetProcessorConfig) error { // Process the template to get the final content - processedContent, err := processTemplate(embeddedPath, manifest) + processedContent, err := processTemplate(embeddedPath, config.manifest, config.pluginPath) if err != nil { return fmt.Errorf("failed to process template %s: %w", embeddedPath, err) } @@ -227,16 +234,23 @@ func updateFile(embeddedPath, targetPath, relativePath string, manifest *model.M } // processTemplate processes a template file with the manifest context. -func processTemplate(embeddedPath string, manifest *model.Manifest) ([]byte, error) { +func processTemplate(embeddedPath string, manifest *model.Manifest, pluginPath string) ([]byte, error) { // Read the template content templateContent, err := assetsFS.ReadFile(embeddedPath) if err != nil { return nil, fmt.Errorf("failed to read embedded file %s: %w", embeddedPath, err) } + // Parse go.mod file to get module information + goMod, err := parseGoModule(pluginPath) + if err != nil { + return nil, fmt.Errorf("failed to parse go.mod file: %w", err) + } + // Create template context context := TemplateContext{ Manifest: manifest, + GoModule: goMod, } // Create and parse the template @@ -254,3 +268,37 @@ func processTemplate(embeddedPath string, manifest *model.Manifest) ([]byte, err return buf.Bytes(), nil } + +// parseGoModule parses the go.mod file to extract module information. +func parseGoModule(pluginPath string) (*GoModule, error) { + goModPath := filepath.Join(pluginPath, "go.mod") + + content, err := os.ReadFile(goModPath) + if err != nil { + // If go.mod doesn't exist, return nil without error + if os.IsNotExist(err) { + return nil, nil + } + + return nil, fmt.Errorf("failed to read go.mod file: %w", err) + } + + goMod := &GoModule{} + lines := strings.Split(string(content), "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + + // Parse module line + if strings.HasPrefix(line, "module ") { + goMod.Module = strings.TrimSpace(strings.TrimPrefix(line, "module ")) + } + + // Parse go version line + if strings.HasPrefix(line, "go ") { + goMod.Version = strings.TrimSpace(strings.TrimPrefix(line, "go ")) + } + } + + return goMod, nil +}