refactor: complete check 2c retarget from block/core to block/pluginsdk
Standalone plugins now build against git.dev.alexdunmow.com/block/pluginsdk. Check 2c requires a pluginsdk module require (rule missing-pluginsdk-require) and enforces its version against the CMS anchor (rule pluginsdk-version-mismatch) once the CMS migrates; a coexisting block/core require stays allowed (calcomblock keeps core for captcha). - check_sdkboundaries.go: degrade the 2c OK message gracefully when the pluginsdk version anchor is empty (transition period) — omit the version clause instead of printing "SDK version ". - check_rbac.go: document pluginsdk in the definitions-only module comment. - plugin_sdk_versions_test.go: retarget fixtures to pluginsdk; add cases for missing-pluginsdk-require, pluginsdk-version-mismatch, a forbidden core replace directive, and an allowed coexisting core require. - lint_test.go: synthesized plugin repos now require pluginsdk (replace- directive case replaces pluginsdk). - registry_test.go: add checks 30 and 31 to the canonical order (were added to the registry without updating this test). - golden: regenerate — check count 32 -> 34 (checks 30/31 SKIP in fixtures). - README.md: describe 2c as the pluginsdk boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
de43bca80f
commit
ff4de855cc
@ -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). |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
12
lint_test.go
12
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:
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
|
||||
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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))
|
||||
|
||||
2
testdata/golden/clean/expected.stdout
vendored
2
testdata/golden/clean/expected.stdout
vendored
@ -1,2 +1,2 @@
|
||||
check-safety FIXTURE_DIR
|
||||
32 checks: 20 ok 12 skip -> OK
|
||||
34 checks: 20 ok 14 skip -> OK
|
||||
|
||||
2
testdata/golden/nomod/expected.stdout
vendored
2
testdata/golden/nomod/expected.stdout
vendored
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user