feat: codeless plugin repos (plugin.mod, no Go) exempt from go.mod checks (WO-WZ-020)

A codeless .bnp repo has no go.mod by design — mirror the ninja CLI
classifier (core bnp.IsCodelessRepo) and skip both the standalone-plugin
go.mod checks and the backend replace-directive parse for such roots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-04 12:43:54 +08:00
parent 482e4f9d43
commit e03144e12d
3 changed files with 55 additions and 1 deletions

View File

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

View File

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

View File

@ -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)
}
}