cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.71.3

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

cmd/pint/ci.go

371lines · modecode

1package main
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "log/slog"
8 "os"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/prometheus/common/model"
14
15 "github.com/cloudflare/pint/internal/checks"
16 "github.com/cloudflare/pint/internal/config"
17 "github.com/cloudflare/pint/internal/discovery"
18 "github.com/cloudflare/pint/internal/git"
19 "github.com/cloudflare/pint/internal/parser"
20 "github.com/cloudflare/pint/internal/reporter"
21
22 "github.com/urfave/cli/v2"
23)
24
25var (
26 baseBranchFlag = "base-branch"
27 failOnFlag = "fail-on"
28 teamCityFlag = "teamcity"
29 checkStyleFlag = "checkstyle"
30 jsonFlag = "json"
31)
32
33var ciCmd = &cli.Command{
34 Name: "ci",
35 Usage: "Run checks on all git changes.",
36 Action: actionCI,
37 Flags: []cli.Flag{
38 &cli.BoolFlag{
39 Name: requireOwnerFlag,
40 Aliases: []string{"r"},
41 Value: false,
42 Usage: "Require all rules to have an owner set via comment.",
43 },
44 &cli.StringFlag{
45 Name: baseBranchFlag,
46 Aliases: []string{"b"},
47 Value: "",
48 Usage: "Set base branch to use for PR checks (main, master, ...).",
49 },
50 &cli.StringFlag{
51 Name: failOnFlag,
52 Aliases: []string{"w"},
53 Value: "bug",
54 Usage: "Exit with non-zero code if there are problems with given severity (or higher) detected.",
55 },
56 &cli.BoolFlag{
57 Name: teamCityFlag,
58 Aliases: []string{"t"},
59 Value: false,
60 Usage: "Print found problems using TeamCity Service Messages format.",
61 },
62 &cli.StringFlag{
63 Name: checkStyleFlag,
64 Aliases: []string{"c"},
65 Value: "",
66 Usage: "Write a checkstyle xml formatted report of all problems to this path.",
67 },
68 &cli.StringFlag{
69 Name: jsonFlag,
70 Aliases: []string{"j"},
71 Value: "",
72 Usage: "Write a JSON formatted report of all problems to this path.",
73 },
74 },
75}
76
77func actionCI(c *cli.Context) error {
78 meta, err := actionSetup(c)
79 if err != nil {
80 return err
81 }
82
83 meta.cfg.CI = detectCI(meta.cfg.CI)
84 baseBranch := meta.cfg.CI.BaseBranch
85 if c.String(baseBranchFlag) != "" {
86 baseBranch = c.String(baseBranchFlag)
87 }
88 currentBranch, err := git.CurrentBranch(git.RunGit)
89 if err != nil {
90 return errors.New("failed to get the name of current branch")
91 }
92 slog.Debug("Got branch information", slog.String("base", baseBranch), slog.String("current", currentBranch))
93 if currentBranch == strings.Split(baseBranch, "/")[len(strings.Split(baseBranch, "/"))-1] {
94 slog.Info("Running from base branch, skipping checks", slog.String("branch", currentBranch))
95 return nil
96 }
97
98 slog.Info("Finding all rules to check on current git branch", slog.String("base", baseBranch))
99
100 filter := git.NewPathFilter(
101 config.MustCompileRegexes(meta.cfg.Parser.Include...),
102 config.MustCompileRegexes(meta.cfg.Parser.Exclude...),
103 config.MustCompileRegexes(meta.cfg.Parser.Relaxed...),
104 )
105
106 schema := parseSchema(meta.cfg.Parser.Schema)
107 names := parseNames(meta.cfg.Parser.Names)
108 allowedOwners := meta.cfg.Owners.CompileAllowed()
109 var entries []discovery.Entry
110 entries, err = discovery.NewGlobFinder([]string{"*"}, filter, schema, names, allowedOwners).Find()
111 if err != nil {
112 return err
113 }
114
115 entries, err = discovery.NewGitBranchFinder(git.RunGit, filter, baseBranch, meta.cfg.CI.MaxCommits, schema, names, allowedOwners).Find(entries)
116 if err != nil {
117 return err
118 }
119
120 ctx := context.WithValue(context.Background(), config.CommandKey, config.CICommand)
121
122 gen := config.NewPrometheusGenerator(meta.cfg, metricsRegistry)
123 defer gen.Stop()
124
125 if err = gen.GenerateStatic(); err != nil {
126 return err
127 }
128
129 slog.Debug("Generated all Prometheus servers", slog.Int("count", gen.Count()))
130
131 summary, err := checkRules(ctx, meta.workers, meta.isOffline, gen, meta.cfg, entries)
132 if err != nil {
133 return err
134 }
135
136 if c.Bool(requireOwnerFlag) {
137 summary.Report(verifyOwners(entries, allowedOwners)...)
138 }
139
140 reps := []reporter.Reporter{}
141 if c.Bool(teamCityFlag) {
142 reps = append(reps, reporter.NewTeamCityReporter(os.Stderr))
143 } else {
144 reps = append(reps, reporter.NewConsoleReporter(os.Stderr, checks.Information, c.Bool(noColorFlag)))
145 }
146 if c.String(checkStyleFlag) != "" {
147 var f *os.File
148 f, err = os.Create(c.String(checkStyleFlag))
149 if err != nil {
150 return err
151 }
152 defer f.Close()
153 reps = append(reps, reporter.NewCheckStyleReporter(f))
154 }
155 if c.String(jsonFlag) != "" {
156 var j *os.File
157 j, err = os.Create(c.String(jsonFlag))
158 if err != nil {
159 return err
160 }
161 defer j.Close()
162 reps = append(reps, reporter.NewJSONReporter(j))
163 }
164
165 if meta.cfg.Repository != nil && meta.cfg.Repository.BitBucket != nil {
166 token, ok := os.LookupEnv("BITBUCKET_AUTH_TOKEN")
167 if !ok {
168 return errors.New("BITBUCKET_AUTH_TOKEN env variable is required when reporting to BitBucket")
169 }
170
171 timeout, _ := time.ParseDuration(meta.cfg.Repository.BitBucket.Timeout)
172 br := reporter.NewBitBucketReporter(
173 version,
174 meta.cfg.Repository.BitBucket.URI,
175 timeout,
176 token,
177 meta.cfg.Repository.BitBucket.Project,
178 meta.cfg.Repository.BitBucket.Repository,
179 meta.cfg.Repository.BitBucket.MaxComments,
180 git.RunGit,
181 )
182 reps = append(reps, br)
183 }
184
185 if meta.cfg.Repository != nil && meta.cfg.Repository.GitLab != nil {
186 token, ok := os.LookupEnv("GITLAB_AUTH_TOKEN")
187 if !ok {
188 return errors.New("GITLAB_AUTH_TOKEN env variable is required when reporting to GitLab")
189 }
190
191 timeout, _ := time.ParseDuration(meta.cfg.Repository.GitLab.Timeout)
192 var gl reporter.GitLabReporter
193 if gl, err = reporter.NewGitLabReporter(
194 version,
195 currentBranch,
196 meta.cfg.Repository.GitLab.URI,
197 timeout,
198 token,
199 meta.cfg.Repository.GitLab.Project,
200 meta.cfg.Repository.GitLab.MaxComments,
201 ); err != nil {
202 return err
203 }
204 reps = append(reps, reporter.NewCommentReporter(gl))
205 }
206
207 meta.cfg.Repository = detectRepository(meta.cfg.Repository)
208 if meta.cfg.Repository != nil && meta.cfg.Repository.GitHub != nil {
209 token, ok := os.LookupEnv("GITHUB_AUTH_TOKEN")
210 if !ok {
211 return errors.New("GITHUB_AUTH_TOKEN env variable is required when reporting to GitHub")
212 }
213
214 prVal, ok := os.LookupEnv("GITHUB_PULL_REQUEST_NUMBER")
215 if !ok {
216 return errors.New("GITHUB_PULL_REQUEST_NUMBER env variable is required when reporting to GitHub")
217 }
218
219 var prNum int
220 if prNum, err = strconv.Atoi(prVal); err != nil {
221 return fmt.Errorf("got not a valid number via GITHUB_PULL_REQUEST_NUMBER: %w", err)
222 }
223
224 var headCommit string
225 headCommit, err = git.HeadCommit(git.RunGit)
226 if err != nil {
227 return errors.New("failed to get the HEAD commit")
228 }
229
230 timeout, _ := time.ParseDuration(meta.cfg.Repository.GitHub.Timeout)
231 var gr reporter.GithubReporter
232 if gr, err = reporter.NewGithubReporter(
233 version,
234 meta.cfg.Repository.GitHub.BaseURI,
235 meta.cfg.Repository.GitHub.UploadURI,
236 timeout,
237 token,
238 meta.cfg.Repository.GitHub.Owner,
239 meta.cfg.Repository.GitHub.Repo,
240 prNum,
241 meta.cfg.Repository.GitHub.MaxComments,
242 headCommit,
243 ); err != nil {
244 return err
245 }
246 reps = append(reps, reporter.NewCommentReporter(gr))
247 }
248
249 minSeverity, err := checks.ParseSeverity(c.String(failOnFlag))
250 if err != nil {
251 return fmt.Errorf("invalid --%s value: %w", failOnFlag, err)
252 }
253
254 problemsFound := false
255 bySeverity := summary.CountBySeverity()
256 for s := range bySeverity {
257 if s >= minSeverity {
258 problemsFound = true
259 break
260 }
261 }
262 if len(bySeverity) > 0 {
263 slog.Info("Problems found", logSeverityCounters(bySeverity)...)
264 }
265
266 summary.SortReports()
267 for _, rep := range reps {
268 err = rep.Submit(summary)
269 if err != nil {
270 return fmt.Errorf("submitting reports: %w", err)
271 }
272 }
273
274 if problemsFound {
275 return errors.New("problems found")
276 }
277
278 return nil
279}
280
281func logSeverityCounters(src map[checks.Severity]int) (attrs []any) {
282 for _, s := range []checks.Severity{checks.Fatal, checks.Bug, checks.Warning, checks.Information} {
283 if c, ok := src[s]; ok {
284 attrs = append(attrs, slog.Attr{Key: s.String(), Value: slog.IntValue(c)})
285 }
286 }
287 return attrs
288}
289
290func detectCI(cfg *config.CI) *config.CI {
291 if bb := os.Getenv("GITHUB_BASE_REF"); bb != "" {
292 cfg.BaseBranch = bb
293 slog.Debug("got base branch from GITHUB_BASE_REF env variable", slog.String("branch", bb))
294 }
295 return cfg
296}
297
298func detectRepository(cfg *config.Repository) *config.Repository {
299 if os.Getenv("GITHUB_ACTION") != "" {
300 cfg.GitHub = detectGithubActions(cfg.GitHub)
301 }
302 if cfg != nil && cfg.GitHub != nil && cfg.GitHub.MaxComments == 0 {
303 cfg.GitHub.MaxComments = 50
304 }
305 return cfg
306}
307
308func detectGithubActions(gh *config.GitHub) *config.GitHub {
309 if os.Getenv("GITHUB_PULL_REQUEST_NUMBER") == "" &&
310 os.Getenv("GITHUB_EVENT_NAME") == "pull_request" &&
311 os.Getenv("GITHUB_REF") != "" {
312 parts := strings.Split(os.Getenv("GITHUB_REF"), "/")
313 if len(parts) >= 4 {
314 slog.Info("Setting GITHUB_PULL_REQUEST_NUMBER from GITHUB_REF env variable", slog.String("pr", parts[2]))
315 os.Setenv("GITHUB_PULL_REQUEST_NUMBER", parts[2])
316 }
317 }
318
319 var isDirty, isNil bool
320
321 if gh == nil {
322 isNil = true
323 gh = &config.GitHub{Timeout: time.Minute.String()} // nolint: exhaustruct
324 }
325
326 if repo := os.Getenv("GITHUB_REPOSITORY"); repo != "" {
327 parts := strings.SplitN(repo, "/", 2)
328 if len(parts) == 2 {
329 if gh.Owner == "" {
330 slog.Info("Setting repository owner from GITHUB_REPOSITORY env variable", slog.String("owner", parts[0]))
331 gh.Owner = parts[0]
332 isDirty = true
333 }
334 if gh.Repo == "" {
335 slog.Info("Setting repository name from GITHUB_REPOSITORY env variable", slog.String("repo", parts[1]))
336 gh.Repo = parts[1]
337 isDirty = true
338 }
339 }
340 }
341
342 if api := os.Getenv("GITHUB_API_URL"); api != "" {
343 if gh.BaseURI == "" {
344 slog.Info("Setting repository base URI from GITHUB_API_URL env variable", slog.String("baseuri", api))
345 gh.BaseURI = api
346 }
347 if gh.UploadURI == "" {
348 slog.Info("Setting repository upload URI from GITHUB_API_URL env variable", slog.String("uploaduri", api))
349 gh.UploadURI = api
350 }
351 }
352
353 if isNil && !isDirty {
354 return nil
355 }
356 return gh
357}
358
359func parseSchema(s string) parser.Schema {
360 if s == config.SchemaThanos {
361 return parser.ThanosSchema
362 }
363 return parser.PrometheusSchema
364}
365
366func parseNames(s string) model.ValidationScheme {
367 if s == config.NamesLegacy {
368 return model.LegacyValidation
369 }
370 return model.UTF8Validation
371}
372