feat: enforce proto generation freshness
This commit is contained in:
parent
f968cb31d9
commit
936a7e7ef9
@ -42,7 +42,37 @@ func init() {
|
||||
fmt.Println(" SKIP: no sqlc or buf codegen targets found")
|
||||
}
|
||||
|
||||
protoFreshness, protoFreshnessErr := checkProtoGeneratedFreshness(ctx.repoRoot)
|
||||
if protoFreshnessErr != nil {
|
||||
fmt.Printf(" ERROR: failed to run make proto freshness check: %v\n", protoFreshnessErr)
|
||||
if protoFreshness.output != "" {
|
||||
for line := range strings.SplitSeq(protoFreshness.output, "\n") {
|
||||
fmt.Printf(" %s\n", line)
|
||||
}
|
||||
}
|
||||
rep.Fail()
|
||||
} else if protoFreshness.changed() {
|
||||
fmt.Println(" FAIL: make proto changed generated outputs; run make proto and commit the generated files.")
|
||||
fmt.Println(" before:")
|
||||
printIndentedStatus(protoFreshness.beforeStatus)
|
||||
fmt.Println(" after:")
|
||||
printIndentedStatus(protoFreshness.afterStatus)
|
||||
rep.Fail()
|
||||
} else if protoFreshness.checked {
|
||||
fmt.Println(" OK: make proto freshness check clean")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func printIndentedStatus(status string) {
|
||||
if strings.TrimSpace(status) == "" {
|
||||
fmt.Println(" (clean)")
|
||||
return
|
||||
}
|
||||
for line := range strings.SplitSeq(status, "\n") {
|
||||
fmt.Printf(" %s\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,14 +18,14 @@ type comingSoonViolation struct {
|
||||
}
|
||||
|
||||
var comingSoonAllowedFiles = map[string]bool{
|
||||
"components/pages/system-pages-list.tsx": true, // Real system page type
|
||||
"components/settings/ai-action-matrix.tsx": true, // AI task for site status copy
|
||||
"components/settings/site-settings-form.tsx": true, // Settings navigation text
|
||||
"components/settings/site-status-section.tsx": true, // Legitimate Site Status feature UI
|
||||
"lib/nav-items.ts": true, // Admin navigation metadata
|
||||
"routes/admin/pages.tsx": true, // System page type enum wiring
|
||||
"lib/api/gen/blockninja/v1/pages_pb.d.ts": true, // Generated proto enum SYSTEM_PAGE_TYPE_COMING_SOON
|
||||
"lib/api/gen/blockninja/v1/settings_pb.d.ts": true, // Generated proto enum SITE_MODE_COMING_SOON
|
||||
"components/pages/system-pages-list.tsx": true, // Real system page type
|
||||
"components/settings/ai-action-matrix.tsx": true, // AI task for site status copy
|
||||
"components/settings/site-settings-form.tsx": true, // Settings navigation text
|
||||
"components/settings/site-status-section.tsx": true, // Legitimate Site Status feature UI
|
||||
"lib/nav-items.ts": true, // Admin navigation metadata
|
||||
"routes/admin/pages.tsx": true, // System page type enum wiring
|
||||
"lib/api/gen/blockninja/v1/pages_pb.d.ts": true, // Generated proto enum SYSTEM_PAGE_TYPE_COMING_SOON
|
||||
"lib/api/gen/blockninja/v1/settings_pb.d.ts": true, // Generated proto enum SITE_MODE_COMING_SOON
|
||||
}
|
||||
|
||||
var placeholderCopyPatterns = []*regexp.Regexp{
|
||||
@ -138,6 +138,12 @@ func checkPlaceholderLanguage(roots []todoScanRoot) []comingSoonViolation {
|
||||
if strings.HasSuffix(path, "_test.go") || strings.HasSuffix(path, ".test.tsx") {
|
||||
return nil
|
||||
}
|
||||
slashPath := filepath.ToSlash(path)
|
||||
if strings.Contains(slashPath, "/generated/") ||
|
||||
strings.HasSuffix(slashPath, ".pb.go") ||
|
||||
strings.HasSuffix(slashPath, ".connect.go") {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(filepath.ToSlash(path), "cmd/check-safety/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -28,6 +28,13 @@ type protoOwnershipViolation struct {
|
||||
coreProto string
|
||||
}
|
||||
|
||||
type protoFreshnessResult struct {
|
||||
checked bool
|
||||
beforeStatus string
|
||||
afterStatus string
|
||||
output string
|
||||
}
|
||||
|
||||
var (
|
||||
bufModulePathPattern = regexp.MustCompile(`^(?:-\s*)?path:\s*"?([^"\s]+)"?\s*$`)
|
||||
protoPackagePattern = regexp.MustCompile(`^\s*package\s+([A-Za-z0-9_.]+)\s*;\s*$`)
|
||||
@ -729,6 +736,52 @@ func countRBACRoles(protoMethods []string, roleByMethod map[string]string) map[s
|
||||
return counts
|
||||
}
|
||||
|
||||
func checkProtoGeneratedFreshness(repoRoot string) (protoFreshnessResult, error) {
|
||||
result := protoFreshnessResult{}
|
||||
if !samePath(repoRoot, blockNinjaRepoRoot()) {
|
||||
return result, nil
|
||||
}
|
||||
if !fileExists(filepath.Join(repoRoot, "Makefile")) || !fileExists(filepath.Join(repoRoot, "buf.gen.yaml")) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.checked = true
|
||||
before, err := protoGeneratedStatus(repoRoot)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.beforeStatus = before
|
||||
|
||||
output, runErr := runCommand(repoRoot, "make", "proto")
|
||||
result.output = output
|
||||
|
||||
after, statusErr := protoGeneratedStatus(repoRoot)
|
||||
if statusErr != nil {
|
||||
return result, statusErr
|
||||
}
|
||||
result.afterStatus = after
|
||||
|
||||
if runErr != nil {
|
||||
return result, fmt.Errorf("make proto failed: %w", runErr)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r protoFreshnessResult) changed() bool {
|
||||
return r.checked && r.beforeStatus != r.afterStatus
|
||||
}
|
||||
|
||||
func protoGeneratedStatus(repoRoot string) (string, error) {
|
||||
paths := []string{
|
||||
"backend/internal/api",
|
||||
"backend/internal/mcpserver/generated",
|
||||
"packages/api/src",
|
||||
}
|
||||
args := []string{"status", "--porcelain=v1", "--untracked-files=all", "--"}
|
||||
args = append(args, paths...)
|
||||
return runCommand(repoRoot, "git", args...)
|
||||
}
|
||||
|
||||
func printRoleBreakdown(roleCounts map[string]int) {
|
||||
orderedRoles := []string{"public", "viewer", "user", "admin", "superadmin", "cms"}
|
||||
printed := make(map[string]bool)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user