1 package owners
2
3 import (
4 "bytes"
5 "context"
6 "flag"
7 "fmt"
8 "io/fs"
9 "os"
10 "path/filepath"
11 "sort"
12 "strings"
13
14 "slices"
15
16 "github.com/peterbourgon/ff/v3"
17 "github.com/peterbourgon/ff/v3/ffcli"
18 "gopkg.in/yaml.v2"
19
20 repoowners "edge-infra.dev/pkg/f8n/devinfra/repo/owners"
21 "edge-infra.dev/pkg/lib/text/drawing"
22 )
23
24 var ignore = []string{".git", "build", "tmp", ".vscode"}
25
26 type verify struct {
27 *owners
28
29 validate bool
30 upToDate bool
31 unowned bool
32 }
33
34 func newVerify(o *owners) *ffcli.Command {
35 u := &verify{owners: o}
36
37 fs := flag.NewFlagSet("hack owners verify", flag.ExitOnError)
38 u.owners.RegisterFlags(fs)
39 fs.BoolVar(&u.validate, "validate", false,
40 "verify that the policy-bot config is valid")
41 fs.BoolVar(&u.upToDate, "up-to-date", false,
42 "verify that the policy-bot config is up-to-date")
43 fs.BoolVar(&u.unowned, "unowned", false,
44 "gather a list of currently unowned directories")
45 return &ffcli.Command{
46 Name: "verify",
47 FlagSet: fs,
48 Exec: u.Exec,
49 Options: []ff.Option{
50 ff.WithEnvVarNoPrefix(),
51 },
52 }
53 }
54
55 func (v *verify) Exec(_ context.Context, _ []string) error {
56 if v.unowned {
57 _, err := gatherUnowned(".")
58 if err != nil {
59 return err
60 }
61 }
62
63 if v.upToDate {
64
65 u := &update{owners: v.owners}
66 data, err := u.generatePBotCfg()
67 if err != nil {
68 return err
69 }
70
71
72 pbotData, err := v.getPolicyBotConfig()
73 if err != nil {
74 return err
75 }
76
77
78 if !bytes.Equal(data, pbotData) {
79 return fmt.Errorf("pbot file is not up-to-date. try running: just update-policy-bot")
80 }
81 fmt.Println("✅ pbot file is up to date")
82 }
83
84 if v.validate {
85 return v.valid()
86 }
87 return nil
88 }
89
90 func (v *verify) valid() error {
91
92 p := filepath.Join(v.Paths.RepoRoot, v.policyBotFile)
93 bytes, err := os.ReadFile(p)
94 if err != nil {
95 return fmt.Errorf("failed to read file %s: %w", v.policyBotFile, err)
96 }
97
98
99 ok, err := isValidLocalPolicy(bytes)
100 if err != nil {
101 return fmt.Errorf("failed to validate %s: %w", v.policyBotFile, err)
102 }
103 if !ok {
104 return fmt.Errorf("%s is not valid", p)
105 }
106 fmt.Printf("✅ %s is a valid config", p)
107 return nil
108 }
109
110 func gatherUnowned(root string) ([]string, error) {
111
112 fileNames := []string{}
113
114 rootNode := &drawing.StringTree{
115 Data: "/",
116 }
117
118 nodes := map[string]*drawing.StringTree{}
119
120 err := filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error {
121 if err != nil {
122 return err
123 }
124
125
126 if info.IsDir() && slices.Contains(ignore, path) {
127 return filepath.SkipDir
128 }
129
130
131 if !info.IsDir() {
132 return nil
133 }
134
135 d, f := filepath.Split(path)
136 d = strings.TrimSuffix(d, "/")
137
138 if d == "" {
139 node := &drawing.StringTree{
140 Data: f,
141 }
142 rootNode.Children = append(rootNode.Children, node)
143 nodes[f] = node
144 return nil
145 }
146
147 dNode, ok := nodes[d]
148 if !ok {
149 dNode = &drawing.StringTree{
150 Data: f,
151 }
152 nodes[d] = dNode
153 }
154
155 pathWithOwners := filepath.Join(path, repoowners.FileName)
156
157
158 _, err = os.Stat(pathWithOwners)
159 if os.IsNotExist(err) {
160 fileNames = append(fileNames, path)
161 fNode, ok := nodes[path]
162 if !ok {
163 fNode = &drawing.StringTree{
164 Data: f,
165 Labels: map[string]string{"🚫": ""},
166 }
167 nodes[path] = fNode
168 }
169 dNode.Children = append(dNode.Children, fNode)
170 return nil
171 }
172
173 data, err := os.ReadFile(pathWithOwners)
174 if err != nil {
175 return fmt.Errorf("failed to read %s: %w", pathWithOwners, err)
176 }
177
178 file := &repoowners.File{}
179 if err := yaml.UnmarshalStrict(data, file); err != nil {
180 return fmt.Errorf("failed to parse %s: %w", pathWithOwners, err)
181 }
182
183
184 for _, rule := range file.Rules {
185
186 for _, regexPath := range rule.Predicates.ChangedFiles.Paths {
187
188 if regexPath.String() == repoowners.Regex {
189 return filepath.SkipDir
190 }
191
192 fNode, ok := nodes[path]
193 if !ok {
194 fNode = &drawing.StringTree{
195 Data: f,
196 }
197 nodes[path] = fNode
198 }
199 dNode.Children = append(dNode.Children, fNode)
200
201 }
202 }
203
204 return nil
205 })
206 if err != nil {
207 return nil, fmt.Errorf("failed to walk filesystem: %w", err)
208 }
209
210 rootNode.Print()
211
212
213 sort.Slice(fileNames, func(i, j int) bool {
214 return fileNames[i] < fileNames[j]
215 })
216
217 return fileNames, nil
218 }
219
View as plain text