cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.65.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

cmd/pint/ci.go

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