diff --git a/check_sdkboundaries.go b/check_sdkboundaries.go index d34817e..207e669 100644 --- a/check_sdkboundaries.go +++ b/check_sdkboundaries.go @@ -31,7 +31,9 @@ func init() { } } for _, target := range ctx.backendTargets { - if samePath(target.root, ctx.backendDir) { + // A codeless plugin repo (plugin.mod, no Go) has no go.mod by + // design — nothing for the replace-directive check to parse. + if samePath(target.root, ctx.backendDir) && !isCodelessPluginRepo(target.root) { for _, v := range checkCMSCoreSDKGoMod(target.root) { v.file = prefixDisplayPath(target.displayOrRoot(), v.file) cmsGoModViolations = append(cmsGoModViolations, v) diff --git a/plugin_sdk_versions.go b/plugin_sdk_versions.go index 8ffcddf..3596ba7 100644 --- a/plugin_sdk_versions.go +++ b/plugin_sdk_versions.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "golang.org/x/mod/modfile" ) @@ -121,7 +122,30 @@ func checkCMSCoreSDKGoMod(root string) []pluginGoModViolation { return checkGoModForModuleReplaceDirective(filepath.Join(root, "go.mod"), blockCoreImportPrefix) } +// isCodelessPluginRepo mirrors the ninja CLI classifier (core +// cmd/ninja/internal/bnp.IsCodelessRepo): a plugin.mod with no Go source at +// the repo root is a codeless plugin (WO-WZ-020) — declarative-only, no +// go.mod and no SDK version to pin. +func isCodelessPluginRepo(root string) bool { + if !fileExists(filepath.Join(root, "plugin.mod")) { + return false + } + entries, err := os.ReadDir(root) + if err != nil { + return false + } + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".go") { + return false + } + } + return true +} + func checkStandalonePluginGoMod(root, requiredSDKVersion string) []pluginGoModViolation { + if isCodelessPluginRepo(root) { + return nil + } goModPath := filepath.Join(root, "go.mod") parsed, violations := parseGoModForSafety(goModPath) if parsed == nil { diff --git a/plugin_sdk_versions_test.go b/plugin_sdk_versions_test.go index 32b5990..216272c 100644 --- a/plugin_sdk_versions_test.go +++ b/plugin_sdk_versions_test.go @@ -102,3 +102,31 @@ require git.dev.alexdunmow.com/block/core v0.2.1 t.Fatalf("checkStandalonePluginGoMod() returned %d violations, want 0: %#v", len(violations), violations) } } + +func TestCheckStandalonePluginGoModSkipsCodelessRepo(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "plugin.mod"), "[plugin]\nname = \"fixture\"\nversion = \"0.1.0\"\n", 0644) + writeTestFile(t, filepath.Join(root, "blocks", "blocks.yaml"), "blocks: []\n", 0644) + + if !isCodelessPluginRepo(root) { + t.Fatal("isCodelessPluginRepo() = false, want true for plugin.mod with no root Go source") + } + violations := checkStandalonePluginGoMod(root, "v0.2.1") + if len(violations) != 0 { + t.Fatalf("checkStandalonePluginGoMod() returned %d violations, want 0 for codeless repo: %#v", len(violations), violations) + } +} + +func TestCheckStandalonePluginGoModStillRequiresGoModWithRootGoSource(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "plugin.mod"), "[plugin]\nname = \"fixture\"\nversion = \"0.1.0\"\n", 0644) + writeTestFile(t, filepath.Join(root, "main.go"), "package main\nfunc main() {}\n", 0644) + + if isCodelessPluginRepo(root) { + t.Fatal("isCodelessPluginRepo() = true, want false with root Go source") + } + violations := checkStandalonePluginGoMod(root, "v0.2.1") + if len(violations) != 1 || violations[0].rule != "missing-go-mod" { + t.Fatalf("checkStandalonePluginGoMod() = %#v, want single missing-go-mod violation", violations) + } +}