Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add teamcity reporter #773

Merged
merged 2 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
1 change: 1 addition & 0 deletions .github/workflows/compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
- name: Compile binaries
uses: goreleaser/goreleaser-action@v5
with:
version: v1.21.2
args: build --snapshot --clean --single-target

- name: Release binaries
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 [email protected]
env GIT_COMMITTER_NAME=pint
env [email protected]
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