feat(2c): forbid all first-party imports in standalone plugins; fix --plugin-dir-only scans
Import boundary now covers the whole first-party tree, not just block/cms: block/core and block/orchestrator imports are violations too, with an explicit carve-out for the packages that deliberately stayed core after the pluginsdk extraction (core/captcha, core/backup — calcomblock uses captcha today). The .templ import scan gets the same rule. defaultScanTargetDir: --plugin-dir with no positional target used to leave targetDir at cwd, so the checker scanned its own repo and self-reported failures while never scanning the plugin. It now targets the first plugin root, matching the positional form. Verified green across cms + all 11 v0.2.1 fleet repos (bidmasters still fails the version anchor as expected — it pins v0.2.0 on a detached HEAD, unrelated to these changes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
338c41c3c8
commit
0749c22d30
@ -60,7 +60,7 @@ func init() {
|
||||
if len(pluginImportViolations) > 0 {
|
||||
rep.Fail("%d standalone plugin import violation(s)", len(pluginImportViolations))
|
||||
for _, v := range pluginImportViolations {
|
||||
rep.Findingf("%s:%d imports BlockNinja CMS package %q", v.file, v.line, v.importPath)
|
||||
rep.Findingf("%s:%d imports first-party BlockNinja package %q (plugins may only use block/pluginsdk)", v.file, v.line, v.importPath)
|
||||
}
|
||||
}
|
||||
if len(pluginGoModViolations) > 0 {
|
||||
|
||||
@ -104,7 +104,7 @@ func Example() string {
|
||||
if !strings.Contains(got, "FAIL 2c ") {
|
||||
t.Fatalf("output missing Check 2c fail line:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "imports BlockNinja CMS package") {
|
||||
if !strings.Contains(got, "imports first-party BlockNinja package") {
|
||||
t.Fatalf("output missing forbidden BlockNinja import message:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
20
main_test.go
20
main_test.go
@ -20,6 +20,26 @@ func TestDefaultScanTargetDirUsesCWDPlugin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultScanTargetDirUsesPluginRootWhenOnlyPluginDirGiven(t *testing.T) {
|
||||
cwd := t.TempDir() // no plugin.mod — simulates running from the check-safety repo itself
|
||||
pluginRoot := t.TempDir()
|
||||
|
||||
got := defaultScanTargetDir(".", []string{pluginRoot}, cwd)
|
||||
if got != pluginRoot {
|
||||
t.Fatalf("defaultScanTargetDir() = %q, want plugin root %q", got, pluginRoot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultScanTargetDirKeepsExplicitTargetOverPluginRoots(t *testing.T) {
|
||||
cwd := t.TempDir()
|
||||
pluginRoot := t.TempDir()
|
||||
|
||||
got := defaultScanTargetDir("/some/backend", []string{pluginRoot}, cwd)
|
||||
if got != "/some/backend" {
|
||||
t.Fatalf("defaultScanTargetDir() = %q, want explicit target /some/backend", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultScanTargetDirFallsBackToCoreBackend(t *testing.T) {
|
||||
cwd := t.TempDir()
|
||||
|
||||
|
||||
@ -14,8 +14,40 @@ const (
|
||||
blockCoreImportPrefix = "git.dev.alexdunmow.com/block/core"
|
||||
pluginSDKImportPrefix = "git.dev.alexdunmow.com/block/pluginsdk"
|
||||
blockNinjaImportPrefix = "git.dev.alexdunmow.com/block/cms"
|
||||
orchestratorImportPrefix = "git.dev.alexdunmow.com/block/orchestrator"
|
||||
firstPartyImportPrefix = "git.dev.alexdunmow.com/block/"
|
||||
)
|
||||
|
||||
// allowedCorePluginImports are the block/core packages that deliberately
|
||||
// stayed on core after the pluginsdk extraction (2026-07-07) and remain
|
||||
// plugin-usable; everything else first-party is host-only.
|
||||
var allowedCorePluginImports = []string{
|
||||
blockCoreImportPrefix + "/captcha",
|
||||
blockCoreImportPrefix + "/backup",
|
||||
}
|
||||
|
||||
func hasImportPrefix(importPath, prefix string) bool {
|
||||
return importPath == prefix || strings.HasPrefix(importPath, prefix+"/")
|
||||
}
|
||||
|
||||
// isForbiddenPluginImport reports whether a standalone plugin may not import
|
||||
// the package: all BlockNinja first-party Go code (cms, orchestrator, core)
|
||||
// is off-limits except the plugin SDK and the explicitly kept core packages.
|
||||
func isForbiddenPluginImport(importPath string) bool {
|
||||
if hasImportPrefix(importPath, blockNinjaImportPrefix) || hasImportPrefix(importPath, orchestratorImportPrefix) {
|
||||
return true
|
||||
}
|
||||
if hasImportPrefix(importPath, blockCoreImportPrefix) {
|
||||
for _, allowed := range allowedCorePluginImports {
|
||||
if hasImportPrefix(importPath, allowed) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type pluginImportViolation struct {
|
||||
file string
|
||||
line int
|
||||
@ -74,13 +106,13 @@ func checkStandalonePluginImports(root string) []pluginImportViolation {
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for i, line := range lines {
|
||||
if !strings.Contains(line, blockNinjaImportPrefix) {
|
||||
if !strings.Contains(line, firstPartyImportPrefix) {
|
||||
continue
|
||||
}
|
||||
matches := templImportPattern.FindAllStringSubmatch(line, -1)
|
||||
for _, match := range matches {
|
||||
importPath := match[1]
|
||||
if !strings.HasPrefix(importPath, blockNinjaImportPrefix) {
|
||||
if !isForbiddenPluginImport(importPath) {
|
||||
continue
|
||||
}
|
||||
violations = append(violations, pluginImportViolation{
|
||||
@ -108,7 +140,7 @@ func checkStandalonePluginImports(root string) []pluginImportViolation {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(importPath, blockNinjaImportPrefix) {
|
||||
if !isForbiddenPluginImport(importPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ func TestCheckStandalonePluginImportsFlagsBlockNinjaCMSImports(t *testing.T) {
|
||||
writeTestFile(t, filepath.Join(root, "main.go"), `package example
|
||||
|
||||
import (
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
|
||||
"git.dev.alexdunmow.com/block/cms/internal/helpers"
|
||||
)
|
||||
|
||||
@ -30,7 +30,7 @@ func Example() {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStandalonePluginImportsAllowsCoreSDKImports(t *testing.T) {
|
||||
func TestCheckStandalonePluginImportsFlagsCoreImports(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
||||
writeTestFile(t, filepath.Join(root, "main.go"), `package example
|
||||
@ -40,6 +40,35 @@ import "git.dev.alexdunmow.com/block/core/plugin"
|
||||
func Example() {
|
||||
_ = plugin.PluginRegistration{}
|
||||
}
|
||||
`, 0644)
|
||||
|
||||
violations := checkStandalonePluginImports(root)
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("checkStandalonePluginImports() returned %d violations, want 1: %#v", len(violations), violations)
|
||||
}
|
||||
if violations[0].importPath != "git.dev.alexdunmow.com/block/core/plugin" {
|
||||
t.Fatalf("importPath = %q, want forbidden core import", violations[0].importPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStandalonePluginImportsAllowsSDKAndKeptCoreImports(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
||||
writeTestFile(t, filepath.Join(root, "main.go"), `package example
|
||||
|
||||
import (
|
||||
"git.dev.alexdunmow.com/block/core/backup"
|
||||
"git.dev.alexdunmow.com/block/core/captcha"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/render"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
_ = plugin.PluginRegistration{}
|
||||
_ = captcha.Config{}
|
||||
_ = backup.Config{}
|
||||
_ = render.BlockNoteToHTML
|
||||
}
|
||||
`, 0644)
|
||||
|
||||
violations := checkStandalonePluginImports(root)
|
||||
@ -48,6 +77,27 @@ func Example() {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStandalonePluginImportsFlagsOrchestratorImports(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
||||
writeTestFile(t, filepath.Join(root, "main.go"), `package example
|
||||
|
||||
import "git.dev.alexdunmow.com/block/orchestrator/backend/internal/services"
|
||||
|
||||
func Example() {
|
||||
_ = services.InstanceService{}
|
||||
}
|
||||
`, 0644)
|
||||
|
||||
violations := checkStandalonePluginImports(root)
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("checkStandalonePluginImports() returned %d violations, want 1: %#v", len(violations), violations)
|
||||
}
|
||||
if violations[0].importPath != "git.dev.alexdunmow.com/block/orchestrator/backend/internal/services" {
|
||||
t.Fatalf("importPath = %q, want forbidden orchestrator import", violations[0].importPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStandalonePluginImportsFlagsBlockNinjaCMSImportsInTemplFiles(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(root, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
||||
@ -64,13 +114,16 @@ templ Page() {
|
||||
`, 0644)
|
||||
|
||||
violations := checkStandalonePluginImports(root)
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("checkStandalonePluginImports() returned %d violations, want 1: %#v", len(violations), violations)
|
||||
if len(violations) != 2 {
|
||||
t.Fatalf("checkStandalonePluginImports() returned %d violations, want 2: %#v", len(violations), violations)
|
||||
}
|
||||
if violations[0].importPath != "git.dev.alexdunmow.com/block/cms/internal/templates" {
|
||||
t.Fatalf("importPath = %q, want forbidden BlockNinja templ import", violations[0].importPath)
|
||||
if violations[0].importPath != "git.dev.alexdunmow.com/block/core/templates/bn" {
|
||||
t.Fatalf("importPath[0] = %q, want forbidden core templ import", violations[0].importPath)
|
||||
}
|
||||
if violations[0].line != 5 {
|
||||
t.Fatalf("line = %d, want 5", violations[0].line)
|
||||
if violations[1].importPath != "git.dev.alexdunmow.com/block/cms/internal/templates" {
|
||||
t.Fatalf("importPath[1] = %q, want forbidden BlockNinja templ import", violations[1].importPath)
|
||||
}
|
||||
if violations[1].line != 5 {
|
||||
t.Fatalf("line = %d, want 5", violations[1].line)
|
||||
}
|
||||
}
|
||||
|
||||
10
targets.go
10
targets.go
@ -99,9 +99,17 @@ func resolveScanRoots(target string) (backendDir string, repoRoot string, err er
|
||||
}
|
||||
|
||||
func defaultScanTargetDir(targetDir string, pluginRoots []string, cwd string) string {
|
||||
if len(pluginRoots) > 0 || targetDir != "." {
|
||||
if targetDir != "." {
|
||||
return targetDir
|
||||
}
|
||||
// --plugin-dir with no positional target means "scan that plugin", not
|
||||
// "scan the cwd" (which is usually the check-safety repo itself).
|
||||
if len(pluginRoots) > 0 {
|
||||
if expanded, err := expandPath(pluginRoots[0]); err == nil {
|
||||
return expanded
|
||||
}
|
||||
return pluginRoots[0]
|
||||
}
|
||||
if fileExists(filepath.Join(cwd, "plugin.mod")) {
|
||||
return cwd
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user