Skip to content

Commit

Permalink
Add teamcity reporter
Browse files Browse the repository at this point in the history
  • Loading branch information
prymitive committed Nov 7, 2023
1 parent a2a4d0b commit b54f709
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 3 deletions.
1 change: 1 addition & 0 deletions .github/spellcheck/wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ SNI
symlink
symlinked
symlinks
TeamCity
templated
Thanos
TLS
Expand Down
15 changes: 13 additions & 2 deletions cmd/pint/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var (
baseBranchFlag = "base-branch"
devFlag = "dev"
failOnFlag = "fail-on"
teamCityFlag = "teamcity"
)

var ciCmd = &cli.Command{
Expand Down Expand Up @@ -54,6 +55,12 @@ var ciCmd = &cli.Command{
Value: "bug",
Usage: "Exit with non-zero code if there are problems with given severity (or higher) detected",
},
&cli.BoolFlag{
Name: teamCityFlag,
Aliases: []string{"t"},
Value: false,
Usage: "Report problems using TeamCity Service Messages",
},
},
}

Expand Down Expand Up @@ -120,8 +127,12 @@ func actionCI(c *cli.Context) error {
summary.Report(verifyOwners(entries, meta.cfg.Owners.CompileAllowed())...)
}

reps := []reporter.Reporter{
reporter.NewConsoleReporter(os.Stderr, checks.Information),
reps := []reporter.Reporter{}

if c.Bool(teamCityFlag) {
reps = append(reps, reporter.NewTeamCityReporter(os.Stderr))
} else {
reps = append(reps, reporter.NewConsoleReporter(os.Stderr, checks.Information))
}

if meta.cfg.Repository != nil && meta.cfg.Repository.BitBucket != nil {
Expand Down
14 changes: 13 additions & 1 deletion cmd/pint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ var lintCmd = &cli.Command{
Value: "bug",
Usage: "Exit with non-zero code if there are problems with given severity (or higher) detected",
},
&cli.BoolFlag{
Name: teamCityFlag,
Aliases: []string{"t"},
Value: false,
Usage: "Report problems using TeamCity Service Messages",
},
},
}

Expand Down Expand Up @@ -87,7 +93,13 @@ func actionLint(c *cli.Context) error {
return fmt.Errorf("invalid --%s value: %w", failOnFlag, err)
}

r := reporter.NewConsoleReporter(os.Stderr, minSeverity)
var r reporter.Reporter
if c.Bool(teamCityFlag) {
r = reporter.NewTeamCityReporter(os.Stderr)
} else {
r = reporter.NewConsoleReporter(os.Stderr, minSeverity)
}

err = r.Submit(summary)
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions cmd/pint/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func checkRules(ctx context.Context, workers int, gen *config.PrometheusGenerato
for result := range results {
summary.Report(result)
}
summary.SortReports()
summary.Duration = time.Since(start)
summary.Entries = len(entries)
summary.OnlineChecks = onlineChecksCount.Load()
Expand Down
31 changes: 31 additions & 0 deletions cmd/pint/tests/0158_lint_teamcity.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
env NO_COLOR=1
pint.error --no-color lint --min-severity=info --teamcity rules
! stdout .
cmp stderr stderr.txt

-- stderr.txt --
level=INFO msg="Finding all rules to check" paths=["rules"]
##teamcity[testSuiteStarted name='alerts/comparison']
##teamcity[testSuiteStarted name='Warning']
##teamcity[testStarted name='rules/0001.yml:5']
##teamcity[testStdErr name='rules/0001.yml:5' out='alert query doesn|'t have any condition, it will always fire if the metric exists']
##teamcity[testFinished name='rules/0001.yml:5']
##teamcity[testSuiteFinished name='Warning']
##teamcity[testSuiteFinished name='alerts/comparison']
##teamcity[testSuiteStarted name='promql/syntax']
##teamcity[testSuiteStarted name='Fatal']
##teamcity[testStarted name='rules/0001.yml:7']
##teamcity[testFailed name='rules/0001.yml:7' message='' details='syntax error: unexpected identifier "with"']
##teamcity[testFinished name='rules/0001.yml:7']
##teamcity[testSuiteFinished name='Fatal']
##teamcity[testSuiteFinished name='promql/syntax']
level=INFO msg="Problems found" Fatal=1 Warning=1
level=ERROR msg="Fatal error" err="found 1 problem(s) with severity Bug or higher"
-- rules/0001.yml --
groups:
- name: test
rules:
- alert: Example
expr: up
- alert: Example
expr: sum(xxx) with()
64 changes: 64 additions & 0 deletions cmd/pint/tests/0159_ci_teamcity.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
mkdir testrepo
cd testrepo
exec git init --initial-branch=main .

cp ../src/.pint.hcl .
env GIT_AUTHOR_NAME=pint
env GIT_AUTHOR_EMAIL=pint@example.com
env GIT_COMMITTER_NAME=pint
env GIT_COMMITTER_EMAIL=pint@example.com
exec git add .
exec git commit -am 'import rules and config'

exec git checkout -b v1
cp ../src/a.yml a.yml
exec git add a.yml
exec git commit -am 'v1'

exec git checkout -b v2
cp ../src/b.yml b.yml
exec git add b.yml
exec git commit -am 'v2'

exec git checkout -b v3
exec git rm a.yml
exec git commit -am 'v3'

pint.error --no-color ci -t
! stdout .
cmp stderr ../stderr.txt

-- stderr.txt --
level=INFO msg="Loading configuration file" path=.pint.hcl
level=INFO msg="Finding all rules to check on current git branch using git blame" base=main
level=INFO msg="Problems found" Fatal=1 Warning=1
##teamcity[testSuiteStarted name='promql/syntax']
##teamcity[testSuiteStarted name='Fatal']
##teamcity[testStarted name='b.yml:2']
##teamcity[testFailed name='b.yml:2' message='' details='syntax error: unexpected identifier "bi"']
##teamcity[testFinished name='b.yml:2']
##teamcity[testSuiteFinished name='Fatal']
##teamcity[testSuiteFinished name='promql/syntax']
##teamcity[testSuiteStarted name='alerts/comparison']
##teamcity[testSuiteStarted name='Warning']
##teamcity[testStarted name='b.yml:4']
##teamcity[testStdErr name='b.yml:4' out='alert query doesn|'t have any condition, it will always fire if the metric exists']
##teamcity[testFinished name='b.yml:4']
##teamcity[testSuiteFinished name='Warning']
##teamcity[testSuiteFinished name='alerts/comparison']
level=ERROR msg="Fatal error" err="problems found"
-- src/a.yml --
- record: rule1
expr: sum(foo) bi()
-- src/b.yml --
- record: rule1
expr: sum(foo) bi()
- alert: rule2
expr: sum(foo)
-- src/.pint.hcl --
ci {
baseBranch = "main"
}
parser {
relaxed = [".*"]
}
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Added

- Added [alerts/external_labels](checks/alerts/external_labels.md) check.
- Added support for reporting problems to TeamCity using [Service Messages](https://www.jetbrains.com/help/teamcity/service-messages.html).
To enable run it run `pint --teamcity lint` or `pint --teamcity ci`.

### Changed

Expand Down
19 changes: 19 additions & 0 deletions internal/reporter/reporter.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package reporter

import (
"sort"
"time"

"golang.org/x/exp/slices"
Expand Down Expand Up @@ -75,6 +76,24 @@ func (s Summary) hasReport(r Report) bool {
return false
}

func (s *Summary) SortReports() {
sort.SliceStable(s.reports, func(i, j int) bool {
if s.reports[i].ReportedPath != s.reports[j].ReportedPath {
return s.reports[i].ReportedPath < s.reports[j].ReportedPath
}
if s.reports[i].SourcePath != s.reports[j].SourcePath {
return s.reports[i].SourcePath < s.reports[j].SourcePath
}
if s.reports[i].Problem.Lines[0] != s.reports[j].Problem.Lines[0] {
return s.reports[i].Problem.Lines[0] < s.reports[j].Problem.Lines[0]
}
if s.reports[i].Problem.Reporter != s.reports[j].Problem.Reporter {
return s.reports[i].Problem.Reporter < s.reports[j].Problem.Reporter
}
return s.reports[i].Problem.Text < s.reports[j].Problem.Text
})
}

func (s Summary) Reports() (reports []Report) {
return s.reports
}
Expand Down
87 changes: 87 additions & 0 deletions internal/reporter/teamcity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package reporter

import (
"fmt"
"io"
"strconv"
"strings"

"github.com/cloudflare/pint/internal/checks"
)

func NewTeamCityReporter(output io.Writer) TeamCityReporter {
return TeamCityReporter{
output: output,
escaper: strings.NewReplacer(
"'", "|'",
"\n", "|n",
"\r", "|r",
"\\uNNNN", "|0xNNNN",
"|", "||",
"[", "|[",
"]", "|]",
),
}
}

type TeamCityReporter struct {
output io.Writer
escaper *strings.Replacer
}

func (tc TeamCityReporter) name(report Report) string {
return fmt.Sprintf("%s:%d", report.ReportedPath, report.Problem.Lines[0])
}

func (tc TeamCityReporter) escape(s string) string {
return tc.escaper.Replace(s)
}

func (tc TeamCityReporter) Submit(summary Summary) error {
var buf strings.Builder
for _, report := range summary.reports {
buf.WriteString("##teamcity[testSuiteStarted name='")
buf.WriteString(report.Problem.Reporter)
buf.WriteString("']\n")

buf.WriteString("##teamcity[testSuiteStarted name='")
buf.WriteString(report.Problem.Severity.String())
buf.WriteString("']\n")

buf.WriteString("##teamcity[testStarted name='")
buf.WriteString(tc.name(report))
buf.WriteString("']\n")

if report.Problem.Severity >= checks.Bug {
buf.WriteString("##teamcity[testFailed name='")
buf.WriteString(tc.name(report))
buf.WriteString("' message='' details='")
buf.WriteString(tc.escape(report.Problem.Text))
buf.WriteString("']\n")
} else {
buf.WriteString("##teamcity[testStdErr name='")
buf.WriteString(tc.name(report))
buf.WriteString("' out='")
buf.WriteString(tc.escape(report.Problem.Text))
buf.WriteString("']\n")
}

buf.WriteString("##teamcity[testFinished name='")
buf.WriteString(report.ReportedPath)
buf.WriteRune(':')
buf.WriteString(strconv.Itoa(report.Problem.Lines[0]))
buf.WriteString("']\n")

buf.WriteString("##teamcity[testSuiteFinished name='")
buf.WriteString(report.Problem.Severity.String())
buf.WriteString("']\n")

buf.WriteString("##teamcity[testSuiteFinished name='")
buf.WriteString(report.Problem.Reporter)
buf.WriteString("']\n")

fmt.Fprint(tc.output, buf.String())
buf.Reset()
}
return nil
}
Loading

0 comments on commit b54f709

Please sign in to comment.