package owners import ( "bytes" "context" "flag" "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "slices" "github.com/peterbourgon/ff/v3" "github.com/peterbourgon/ff/v3/ffcli" "gopkg.in/yaml.v2" repoowners "edge-infra.dev/pkg/f8n/devinfra/repo/owners" "edge-infra.dev/pkg/lib/text/drawing" ) var ignore = []string{".git", "build", "tmp", ".vscode"} type verify struct { *owners validate bool upToDate bool unowned bool } func newVerify(o *owners) *ffcli.Command { u := &verify{owners: o} fs := flag.NewFlagSet("hack owners verify", flag.ExitOnError) u.owners.RegisterFlags(fs) fs.BoolVar(&u.validate, "validate", false, "verify that the policy-bot config is valid") fs.BoolVar(&u.upToDate, "up-to-date", false, "verify that the policy-bot config is up-to-date") fs.BoolVar(&u.unowned, "unowned", false, "gather a list of currently unowned directories") return &ffcli.Command{ Name: "verify", FlagSet: fs, Exec: u.Exec, Options: []ff.Option{ ff.WithEnvVarNoPrefix(), }, } } func (v *verify) Exec(_ context.Context, _ []string) error { if v.unowned { _, err := gatherUnowned(".") if err != nil { return err } } if v.upToDate { // gather the owners files u := &update{owners: v.owners} data, err := u.generatePBotCfg() if err != nil { return err } // get the pbot config file pbotData, err := v.getPolicyBotConfig() if err != nil { return err } // check if the gathered files match the config file if !bytes.Equal(data, pbotData) { return fmt.Errorf("pbot file is not up-to-date. try running: just update-policy-bot") } fmt.Println("✅ pbot file is up to date") } if v.validate { return v.valid() } return nil } func (v *verify) valid() error { // get the pbot config file p := filepath.Join(v.Paths.RepoRoot, v.policyBotFile) bytes, err := os.ReadFile(p) if err != nil { return fmt.Errorf("failed to read file %s: %w", v.policyBotFile, err) } // check if the config is valid ok, err := isValidLocalPolicy(bytes) if err != nil { return fmt.Errorf("failed to validate %s: %w", v.policyBotFile, err) } if !ok { return fmt.Errorf("%s is not valid", p) } fmt.Printf("✅ %s is a valid config", p) return nil } func gatherUnowned(root string) ([]string, error) { // files := map[string]bool{} fileNames := []string{} rootNode := &drawing.StringTree{ Data: "/", } nodes := map[string]*drawing.StringTree{} err := filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error { if err != nil { return err } // check if the current dir is in the ignore list if info.IsDir() && slices.Contains(ignore, path) { return filepath.SkipDir } // if the current item isnt a dir skip it if !info.IsDir() { return nil } d, f := filepath.Split(path) d = strings.TrimSuffix(d, "/") if d == "" { // pkg/ cmd/ node := &drawing.StringTree{ Data: f, } rootNode.Children = append(rootNode.Children, node) nodes[f] = node return nil } dNode, ok := nodes[d] // cmd pkg/edge if !ok { dNode = &drawing.StringTree{ Data: f, } nodes[d] = dNode } pathWithOwners := filepath.Join(path, repoowners.FileName) // if the owners file doesnt exist add it to the list _, err = os.Stat(pathWithOwners) if os.IsNotExist(err) { fileNames = append(fileNames, path) fNode, ok := nodes[path] if !ok { fNode = &drawing.StringTree{ Data: f, Labels: map[string]string{"🚫": ""}, } nodes[path] = fNode } dNode.Children = append(dNode.Children, fNode) return nil } data, err := os.ReadFile(pathWithOwners) if err != nil { return fmt.Errorf("failed to read %s: %w", pathWithOwners, err) } file := &repoowners.File{} if err := yaml.UnmarshalStrict(data, file); err != nil { return fmt.Errorf("failed to parse %s: %w", pathWithOwners, err) } // range over all rules for _, rule := range file.Rules { // range over all paths for _, regexPath := range rule.Predicates.ChangedFiles.Paths { // if the OWNERS files uses .* then skip the entire dir if regexPath.String() == repoowners.Regex { return filepath.SkipDir } fNode, ok := nodes[path] if !ok { fNode = &drawing.StringTree{ Data: f, } nodes[path] = fNode } dNode.Children = append(dNode.Children, fNode) // todo (s185994): check if the paths listed match all files in dir } } return nil }) if err != nil { return nil, fmt.Errorf("failed to walk filesystem: %w", err) } rootNode.Print() // sort the files sort.Slice(fileNames, func(i, j int) bool { return fileNames[i] < fileNames[j] }) return fileNames, nil }