feat: enforce proto generation freshness

This commit is contained in:
Alex Dunmow 2026-06-20 12:40:06 +08:00
parent f968cb31d9
commit 936a7e7ef9
3 changed files with 97 additions and 8 deletions

View File

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

View File

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

View File

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