diff --git a/README.md b/README.md index 80480d7..446193f 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ tool prints in each check header): | 1 | Secret env var reads | Secret env vars (`JWT_SECRET`, `VAULT_*`, encryption keys, …) are read only inside `config.Load()` (`config/config.go`) or `_test.go` files. | | 2 | RBAC registration | Every RPC method is registered in the RBAC interceptor. Internal / plugin-managed services (e.g. `ManagementService`, Symposium's `WikiService`) are exempted. | | 2b | Plugin proto ownership | Plugins own their proto/RBAC definitions correctly (no poaching of core proto packages). | -| 2c | Plugin SDK boundaries | Standalone plugins import only the published `block/core` SDK boundary — verified via imports, `go.mod`, and version. | +| 2c | Plugin SDK boundaries | Standalone plugins build against the published `block/pluginsdk` SDK (a coexisting `block/core` require is allowed) — verified via imports, `go.mod`, and version. | | 2d | sqlc UUID overrides | sqlc UUID overrides in standalone plugins and `cmd` configs must use `github.com/google/uuid`. | | 2e | `any` usage | Warns on `any` usage in Go and TypeScript (capped at 120 printed warnings). | | 2f | Codegen freshness | `sqlc compile` and `buf generate` must succeed for every scanned root that defines them (generated code is up to date). | diff --git a/check_rbac.go b/check_rbac.go index f8f5636..9e9b02a 100644 --- a/check_rbac.go +++ b/check_rbac.go @@ -23,12 +23,15 @@ const coreSDKModulePath = "git.dev.alexdunmow.com/block/core" // "missing from MethodRoles", a not-applicable result rather than a finding. // // - core: the shared proto SDK. +// - pluginsdk: the standalone-plugin SDK (2026-07-07 extraction). It +// carries proto definitions but serves nothing. // - cli: the ninja developer CLI (WO-WZ-023). It vendors // orchestrator/v1/plugin_registry.proto to generate a CLIENT for the // registry; it has no server and no RBAC interceptor. var definitionsOnlyModulePaths = map[string]bool{ - coreSDKModulePath: true, - "git.dev.alexdunmow.com/block/cli": true, + coreSDKModulePath: true, + "git.dev.alexdunmow.com/block/pluginsdk": true, + "git.dev.alexdunmow.com/block/cli": true, } // isDefinitionsOnlySDKTarget reports whether the backend target carries proto diff --git a/check_sdkboundaries.go b/check_sdkboundaries.go index 207e669..22eaae9 100644 --- a/check_sdkboundaries.go +++ b/check_sdkboundaries.go @@ -11,7 +11,9 @@ func init() { var pluginGoModViolations []pluginGoModViolation var standalonePluginLabels []string checkedStandalonePluginRoots := make(map[string]bool) - requiredSDKVersion, requiredSDKVersionErr := currentCMSCoreSDKVersion() + // Version anchor = the CMS backend's pluginsdk pin; empty (skip + // version enforcement) until the CMS itself migrates to pluginsdk. + requiredSDKVersion, requiredSDKVersionErr := currentCMSPluginSDKVersion() if requiredSDKVersionErr != nil { rep.Fatal("failed to resolve CMS SDK version: %v", requiredSDKVersionErr) } @@ -72,7 +74,11 @@ func init() { } } } else if len(standalonePluginLabels) > 0 { - rep.OK("Standalone plugin imports and go.mod stay on SDK version %s", requiredSDKVersion) + if requiredSDKVersion != "" { + rep.OK("Standalone plugin imports and go.mod stay on SDK version %s", requiredSDKVersion) + } else { + rep.OK("Standalone plugin imports and go.mod build against the plugin SDK") + } } else if ctx.includeCoreTargets { rep.OK("%s/go.mod does not locally replace %s", ctx.backendTargets[0].displayOrRoot(), blockCoreImportPrefix) } else { diff --git a/lint_test.go b/lint_test.go index 1fd0042..3745bae 100644 --- a/lint_test.go +++ b/lint_test.go @@ -16,7 +16,7 @@ func TestCheckSafetySkipsRPCCheckForPluginWithoutRPCs(t *testing.T) { t.Fatalf("currentCMSCoreSDKVersion: %v", err) } writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644) - writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644) + writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/pluginsdk "+sdkVersion+"\n", 0644) writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644) cwd, err := os.Getwd() @@ -54,7 +54,7 @@ func TestCheckSafetyTypechecksExternalPluginFrontendWithWorkspaceDeps(t *testing } writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644) - writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644) + writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/pluginsdk "+sdkVersion+"\n", 0644) writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644) writeTestFile(t, filepath.Join(pluginWebDir, "package.json"), "{\n \"name\": \"external-plugin-test\",\n \"private\": true,\n \"type\": \"module\",\n \"dependencies\": {\n \"@block-ninja/ui\": \"workspace:*\"\n }\n}\n", 0644) writeTestFile(t, filepath.Join(pluginWebDir, "tsconfig.json"), "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n \"jsx\": \"react-jsx\",\n \"strict\": true,\n \"skipLibCheck\": true,\n \"noEmit\": true\n },\n \"include\": [\"*.tsx\", \"*.ts\"]\n}\n", 0644) @@ -122,7 +122,7 @@ func TestCheckSafetyFailsStandalonePluginReplaceDirective(t *testing.T) { t.Fatalf("currentCMSCoreSDKVersion: %v", err) } writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644) - writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n\nreplace git.dev.alexdunmow.com/block/core => ../block-core\n", 0644) + writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/pluginsdk "+sdkVersion+"\n\nreplace git.dev.alexdunmow.com/block/pluginsdk => ../block-pluginsdk\n", 0644) writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644) cwd, err := os.Getwd() @@ -150,7 +150,7 @@ func TestCheckSafetyFailsStandalonePluginSQLCUUIDStringOverrides(t *testing.T) { t.Fatalf("currentCMSCoreSDKVersion: %v", err) } writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644) - writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644) + writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/pluginsdk "+sdkVersion+"\n", 0644) writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644) writeTestFile(t, filepath.Join(pluginRoot, "sqlc.yaml"), `version: "2" sql: @@ -198,7 +198,7 @@ func TestCheckSafetyWarnsOnStandalonePluginGoAnyUsage(t *testing.T) { t.Fatalf("currentCMSCoreSDKVersion: %v", err) } writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644) - writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644) + writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/pluginsdk "+sdkVersion+"\n", 0644) writeTestFile(t, filepath.Join(pluginRoot, "main.go"), `package example type Payload struct { @@ -236,7 +236,7 @@ func TestCheckSafetyFailsStandalonePluginSQLCCompile(t *testing.T) { t.Fatalf("currentCMSCoreSDKVersion: %v", err) } writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644) - writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644) + writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/pluginsdk "+sdkVersion+"\n", 0644) writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644) writeTestFile(t, filepath.Join(pluginRoot, "sqlc.yaml"), `version: "2" sql: diff --git a/plugin_imports.go b/plugin_imports.go index 8187f68..4c31c14 100644 --- a/plugin_imports.go +++ b/plugin_imports.go @@ -12,6 +12,7 @@ import ( const ( blockCoreImportPrefix = "git.dev.alexdunmow.com/block/core" + pluginSDKImportPrefix = "git.dev.alexdunmow.com/block/pluginsdk" blockNinjaImportPrefix = "git.dev.alexdunmow.com/block/cms" ) diff --git a/plugin_sdk_versions.go b/plugin_sdk_versions.go index 3596ba7..7d9f597 100644 --- a/plugin_sdk_versions.go +++ b/plugin_sdk_versions.go @@ -20,6 +20,17 @@ func currentCMSCoreSDKVersion() (string, error) { return readRequiredModuleVersion(filepath.Join(blockNinjaRepoRoot(), "backend", "go.mod"), blockCoreImportPrefix) } +// currentCMSPluginSDKVersion anchors the fleet's pluginsdk pin to the CMS +// backend's. Empty (no error) while the CMS itself hasn't migrated to +// pluginsdk yet — version enforcement is skipped during that transition. +func currentCMSPluginSDKVersion() (string, error) { + v, err := readRequiredModuleVersion(filepath.Join(blockNinjaRepoRoot(), "backend", "go.mod"), pluginSDKImportPrefix) + if err != nil { + return "", nil + } + return v, nil +} + func parseGoModForSafety(goModPath string) (*modfile.File, []pluginGoModViolation) { if !fileExists(goModPath) { return nil, []pluginGoModViolation{{ @@ -157,9 +168,12 @@ func checkStandalonePluginGoMod(root, requiredSDKVersion string) []pluginGoModVi violations = append(violations, checkGoModForAnyReplaceDirectives(goModPath)...) + // The standalone-plugin SDK is block/pluginsdk (2026-07-07 extraction). + // A block/core require may coexist for the packages that stayed core + // (captcha, backup) — allowed, not required. currentSDKVersion := "" for _, req := range parsed.Require { - if req.Mod.Path == blockCoreImportPrefix { + if req.Mod.Path == pluginSDKImportPrefix { currentSDKVersion = req.Mod.Version break } @@ -167,8 +181,8 @@ func checkStandalonePluginGoMod(root, requiredSDKVersion string) []pluginGoModVi if currentSDKVersion == "" { violations = append(violations, pluginGoModViolation{ file: "go.mod", - rule: "missing-block-core-require", - detail: fmt.Sprintf("missing required module %s", blockCoreImportPrefix), + rule: "missing-pluginsdk-require", + detail: fmt.Sprintf("missing required module %s (standalone plugins build against the plugin SDK, not block/core)", pluginSDKImportPrefix), }) return violations } @@ -176,7 +190,7 @@ func checkStandalonePluginGoMod(root, requiredSDKVersion string) []pluginGoModVi if requiredSDKVersion != "" && currentSDKVersion != requiredSDKVersion { violations = append(violations, pluginGoModViolation{ file: "go.mod", - rule: "block-core-version-mismatch", + rule: "pluginsdk-version-mismatch", detail: fmt.Sprintf("requires %s, want %s", currentSDKVersion, requiredSDKVersion), }) } diff --git a/plugin_sdk_versions_test.go b/plugin_sdk_versions_test.go index 216272c..e07256a 100644 --- a/plugin_sdk_versions_test.go +++ b/plugin_sdk_versions_test.go @@ -50,9 +50,9 @@ func TestCheckStandalonePluginGoModFlagsReplaceDirectives(t *testing.T) { go 1.26.2 -require git.dev.alexdunmow.com/block/core v0.2.1 +require git.dev.alexdunmow.com/block/pluginsdk v0.2.1 -replace git.dev.alexdunmow.com/block/core => ../block-core +replace git.dev.alexdunmow.com/block/pluginsdk => ../block-pluginsdk `, 0644) violations := checkStandalonePluginGoMod(root, "v0.2.1") @@ -67,34 +67,79 @@ replace git.dev.alexdunmow.com/block/core => ../block-core } } -func TestCheckStandalonePluginGoModFlagsOutdatedSDKVersion(t *testing.T) { +// A replace directive for the coexisting block/core module still fails — +// replace directives of any module are forbidden in standalone plugin repos. +func TestCheckStandalonePluginGoModFlagsCoreReplaceDirective(t *testing.T) { root := t.TempDir() writeTestFile(t, filepath.Join(root, "go.mod"), `module example.com/plugin go 1.26.2 -require git.dev.alexdunmow.com/block/core v0.2.0 +require git.dev.alexdunmow.com/block/pluginsdk v0.2.1 + +replace git.dev.alexdunmow.com/block/core => ../block-core `, 0644) violations := checkStandalonePluginGoMod(root, "v0.2.1") if len(violations) != 1 { t.Fatalf("checkStandalonePluginGoMod() returned %d violations, want 1: %#v", len(violations), violations) } - if violations[0].rule != "block-core-version-mismatch" { - t.Fatalf("rule = %q, want block-core-version-mismatch", violations[0].rule) + if violations[0].rule != "no-replace-directives" { + t.Fatalf("rule = %q, want no-replace-directives", violations[0].rule) + } +} + +func TestCheckStandalonePluginGoModFlagsOutdatedSDKVersion(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "go.mod"), `module example.com/plugin + +go 1.26.2 + +require git.dev.alexdunmow.com/block/pluginsdk v0.2.0 +`, 0644) + + violations := checkStandalonePluginGoMod(root, "v0.2.1") + if len(violations) != 1 { + t.Fatalf("checkStandalonePluginGoMod() returned %d violations, want 1: %#v", len(violations), violations) + } + if violations[0].rule != "pluginsdk-version-mismatch" { + t.Fatalf("rule = %q, want pluginsdk-version-mismatch", violations[0].rule) } if !strings.Contains(violations[0].detail, "want v0.2.1") { t.Fatalf("detail = %q, want target version", violations[0].detail) } } -func TestCheckStandalonePluginGoModAllowsMatchingSDKVersionWithoutReplace(t *testing.T) { +func TestCheckStandalonePluginGoModFlagsMissingPluginSDKRequire(t *testing.T) { root := t.TempDir() writeTestFile(t, filepath.Join(root, "go.mod"), `module example.com/plugin go 1.26.2 require git.dev.alexdunmow.com/block/core v0.2.1 +`, 0644) + + violations := checkStandalonePluginGoMod(root, "v0.2.1") + if len(violations) != 1 { + t.Fatalf("checkStandalonePluginGoMod() returned %d violations, want 1: %#v", len(violations), violations) + } + if violations[0].rule != "missing-pluginsdk-require" { + t.Fatalf("rule = %q, want missing-pluginsdk-require", violations[0].rule) + } +} + +// A coexisting block/core require is allowed alongside the required pluginsdk +// require (e.g. calcomblock keeps core for captcha). +func TestCheckStandalonePluginGoModAllowsMatchingSDKVersionWithoutReplace(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "go.mod"), `module example.com/plugin + +go 1.26.2 + +require ( + git.dev.alexdunmow.com/block/core v0.5.0 + git.dev.alexdunmow.com/block/pluginsdk v0.2.1 +) `, 0644) violations := checkStandalonePluginGoMod(root, "v0.2.1") diff --git a/registry_test.go b/registry_test.go index 057bd73..3a1e863 100644 --- a/registry_test.go +++ b/registry_test.go @@ -18,7 +18,7 @@ func TestRegistryOrder(t *testing.T) { "1", "2", "2b", "2c", "2d", "2e", "2f", "3", "3b", "4", "5", "6", "7", "8", "9", "10", "10b", "10disc", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", - "28", "29", + "28", "29", "30", "31", } ordered := make([]Check, len(registry)) diff --git a/testdata/golden/clean/expected.stdout b/testdata/golden/clean/expected.stdout index 9c225c7..fc4d2ef 100644 --- a/testdata/golden/clean/expected.stdout +++ b/testdata/golden/clean/expected.stdout @@ -1,2 +1,2 @@ check-safety FIXTURE_DIR -32 checks: 20 ok 12 skip -> OK +34 checks: 20 ok 14 skip -> OK diff --git a/testdata/golden/nomod/expected.stdout b/testdata/golden/nomod/expected.stdout index 58caba7..a9ab573 100644 --- a/testdata/golden/nomod/expected.stdout +++ b/testdata/golden/nomod/expected.stdout @@ -10,4 +10,4 @@ FAIL 15 1 err.Error() leak(s) to HTTP clients — log via slog.Error() and retur FAIL 17 1 TODO marker(s) found — ship explicit behavior, not placeholders internal/service/handler.go:14 // TODO: add proper initialisation -32 checks: 14 ok 13 skip 5 fail -> FAIL +34 checks: 14 ok 15 skip 5 fail -> FAIL