mirror of https://github.com/grafana/grafana.git
[Prototyping] Lint dashboard in pull requests (#97199)
* Make lint issues comment * Make it work * Delete previous comments * Move linting to separate package * Fix issue with refactoring * Update test fixture
This commit is contained in:
parent
a1eb1863e5
commit
48feba66e6
3
go.mod
3
go.mod
|
|
@ -480,6 +480,8 @@ require github.com/openzipkin/zipkin-go v0.4.3 // @grafana/oss-big-tent
|
||||||
|
|
||||||
require github.com/google/go-github/v66 v66.0.0 // @grafana/grafana-app-platform-squad
|
require github.com/google/go-github/v66 v66.0.0 // @grafana/grafana-app-platform-squad
|
||||||
|
|
||||||
|
require github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752 // @grafana/grafana-app-platform-squad
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/longrunning v0.6.0 // indirect
|
cloud.google.com/go/longrunning v0.6.0 // indirect
|
||||||
github.com/at-wat/mqtt-go v0.19.4 // indirect
|
github.com/at-wat/mqtt-go v0.19.4 // indirect
|
||||||
|
|
@ -501,6 +503,7 @@ require (
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/gammazero/deque v0.2.1 // indirect
|
github.com/gammazero/deque v0.2.1 // indirect
|
||||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||||
|
github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241101005901-83e3491f2a70 // indirect
|
||||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect
|
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect
|
||||||
github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect
|
github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect
|
||||||
github.com/grafana/sqlds/v4 v4.1.0 // indirect
|
github.com/grafana/sqlds/v4 v4.1.0 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -2302,6 +2302,8 @@ github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6P
|
||||||
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws=
|
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws=
|
||||||
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
|
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
|
||||||
github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc=
|
github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc=
|
||||||
|
github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752 h1:pIEPQPua6kCkMib/nOn//WLgtiA4urf2RPBViSF6CcE=
|
||||||
|
github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752/go.mod h1:QRxC8NNAbpGx2vz9/XckJcWXtHU3My57byeudPrf0WU=
|
||||||
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
|
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
|
||||||
github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg=
|
github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg=
|
||||||
github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s=
|
github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s=
|
||||||
|
|
@ -2322,6 +2324,8 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 h1:fV6IgVtViXcYZ4VqTAMuVBTLuGA
|
||||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0=
|
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0=
|
||||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 h1:F0O9eTy4jHjEd1Z3/qIza2GdY7PYpTddUeaq9p3NKGU=
|
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 h1:F0O9eTy4jHjEd1Z3/qIza2GdY7PYpTddUeaq9p3NKGU=
|
||||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0/go.mod h1:bd6Cm06EK0MzRO5ahUpbDz1SxNOKu+fzladbaRPHZPY=
|
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0/go.mod h1:bd6Cm06EK0MzRO5ahUpbDz1SxNOKu+fzladbaRPHZPY=
|
||||||
|
github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241101005901-83e3491f2a70 h1:69GI3KsF851YnwYp6zHdsskcGp3ZnGsWc+ve8vMp1mc=
|
||||||
|
github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241101005901-83e3491f2a70/go.mod h1:WtWosval1KCZP9BGa42b8aVoJmVXSg0EvQXi9LDSVZQ=
|
||||||
github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA=
|
github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA=
|
||||||
github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
|
github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
|
||||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
||||||
|
|
|
||||||
|
|
@ -990,8 +990,6 @@ github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10/go.mod h1:GMLi6d0
|
||||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
|
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
|
||||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
|
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
|
||||||
github.com/grafana/cog v0.0.4/go.mod h1:lzetOuhGUl/JaSACiJoHvBokf9/fS6PEFaWZvnQu2vs=
|
github.com/grafana/cog v0.0.4/go.mod h1:lzetOuhGUl/JaSACiJoHvBokf9/fS6PEFaWZvnQu2vs=
|
||||||
github.com/grafana/cog v0.0.5 h1:BCa+10i3KvV+KMSQuxlN1DS9cZEwN+EAFc7ZmXqHxQE=
|
|
||||||
github.com/grafana/cog v0.0.5/go.mod h1:lzetOuhGUl/JaSACiJoHvBokf9/fS6PEFaWZvnQu2vs=
|
|
||||||
github.com/grafana/cuetsy v0.1.10/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc=
|
github.com/grafana/cuetsy v0.1.10/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc=
|
||||||
github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak=
|
github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak=
|
||||||
github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90=
|
github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90=
|
||||||
|
|
@ -1519,6 +1517,8 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV
|
||||||
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
|
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
|
@ -1545,6 +1545,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI=
|
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI=
|
||||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
||||||
|
github.com/zeitlinger/conflate v0.0.0-20230622100834-279724abda8c h1:PtECnCzGLw8MuQ0tmPRaN5c95ZfNTFZOobvgC6A83zk=
|
||||||
|
github.com/zeitlinger/conflate v0.0.0-20230622100834-279724abda8c/go.mod h1:KsJBt1tGR0Q7u+3T7CLN+zITAI06GiXVi/cgP9Xrpb8=
|
||||||
github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
|
github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
|
||||||
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,9 @@ type GitHubRepositoryConfig struct {
|
||||||
// By default, this is false (i.e. we will not create previews).
|
// By default, this is false (i.e. we will not create previews).
|
||||||
// This option is a no-op if BranchWorkflow is `false` or default.
|
// This option is a no-op if BranchWorkflow is `false` or default.
|
||||||
GenerateDashboardPreviews bool `json:"generateDashboardPreviews,omitempty"`
|
GenerateDashboardPreviews bool `json:"generateDashboardPreviews,omitempty"`
|
||||||
|
|
||||||
|
// PullRequestLinter enables the dashboard linter for this repository in Pull Requests
|
||||||
|
PullRequestLinter bool `json:"pullRequestLinter,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepositoryType defines the types of Repository
|
// RepositoryType defines the types of Repository
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,13 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref
|
||||||
Format: "",
|
Format: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"pullRequestLinter": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "PullRequestLinter enables the dashboard linter for this repository in Pull Requests",
|
||||||
|
Type: []string{"boolean"},
|
||||||
|
Format: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package lint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/grafana/dashboard-linter/lint"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DashboardLinter struct {
|
||||||
|
rules lint.RuleSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: what would be a good place to put all the schema validation?
|
||||||
|
type specData struct {
|
||||||
|
Spec json.RawMessage `json:"spec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDashboardLinter() *DashboardLinter {
|
||||||
|
// TODO: read rules from configuration and pass to the lint function
|
||||||
|
return &DashboardLinter{rules: lint.NewRuleSet()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DashboardLinter) Lint(ctx context.Context, fileData []byte) ([]Issue, error) {
|
||||||
|
var data specData
|
||||||
|
if err := json.Unmarshal(fileData, &data); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal file data into spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard, err := lint.NewDashboard(data.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse dashboard with linter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := l.rules.Lint([]lint.Dashboard{dashboard})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to lint dashboard: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
byRule := results.ByRule()
|
||||||
|
rules := make([]string, 0, len(byRule))
|
||||||
|
for r := range byRule {
|
||||||
|
rules = append(rules, r)
|
||||||
|
}
|
||||||
|
sort.Strings(rules)
|
||||||
|
|
||||||
|
issues := make([]Issue, 0)
|
||||||
|
for _, rule := range rules {
|
||||||
|
for _, rr := range byRule[rule] {
|
||||||
|
for _, r := range rr.Result.Results {
|
||||||
|
if r.Severity != lint.Error && r.Severity != lint.Warning {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
issues = append(issues, Issue{
|
||||||
|
Rule: rule,
|
||||||
|
Severity: r.Severity,
|
||||||
|
Message: r.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package lint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/grafana/dashboard-linter/lint"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Issue struct {
|
||||||
|
Severity lint.Severity
|
||||||
|
Rule string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Linter interface {
|
||||||
|
Lint(ctx context.Context, data []byte) ([]Issue, error)
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/auth"
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/auth"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/lint"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||||
|
|
@ -214,7 +215,8 @@ func (b *ProvisioningAPIBuilder) asRepository(ctx context.Context, obj runtime.O
|
||||||
return nil, fmt.Errorf("invalid base URL: %w", err)
|
return nil, fmt.Errorf("invalid base URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return repository.NewGitHub(ctx, r, b.ghFactory, baseURL), nil
|
linter := lint.NewDashboardLinter()
|
||||||
|
return repository.NewGitHub(ctx, r, b.ghFactory, baseURL, linter), nil
|
||||||
case provisioning.S3RepositoryType:
|
case provisioning.S3RepositoryType:
|
||||||
return repository.NewS3(r), nil
|
return repository.NewS3(r), nil
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
|
||||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/lint"
|
||||||
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@ type githubRepository struct {
|
||||||
config *provisioning.Repository
|
config *provisioning.Repository
|
||||||
gh pgh.Client
|
gh pgh.Client
|
||||||
baseURL *url.URL
|
baseURL *url.URL
|
||||||
|
linter lint.Linter
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Repository = (*githubRepository)(nil)
|
var _ Repository = (*githubRepository)(nil)
|
||||||
|
|
@ -37,12 +39,14 @@ func NewGitHub(
|
||||||
config *provisioning.Repository,
|
config *provisioning.Repository,
|
||||||
factory pgh.ClientFactory,
|
factory pgh.ClientFactory,
|
||||||
baseURL *url.URL,
|
baseURL *url.URL,
|
||||||
|
linter lint.Linter,
|
||||||
) *githubRepository {
|
) *githubRepository {
|
||||||
return &githubRepository{
|
return &githubRepository{
|
||||||
config: config,
|
config: config,
|
||||||
logger: slog.Default().With("logger", "github-repository"),
|
logger: slog.Default().With("logger", "github-repository"),
|
||||||
gh: factory.New(ctx, config.Spec.GitHub.Token),
|
gh: factory.New(ctx, config.Spec.GitHub.Token),
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
|
linter: linter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,18 +488,8 @@ func (r *githubRepository) onPushEvent(ctx context.Context, logger *slog.Logger,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentTemplate = `Hey there! 🎉
|
// changedResource represents a resource that has changed in a pull request.
|
||||||
Grafana spotted some changes for your resources in this pull request:
|
type changedResource struct {
|
||||||
|
|
||||||
| File Name | Type | Path | Action | Links |
|
|
||||||
|-----------|------|------|--------|-------|
|
|
||||||
{{- range .}}
|
|
||||||
| {{.Filename}} | {{.Type}} | {{.Path}} | {{.Action}} | {{if .Original}}[Original]({{.Original}}){{end}}{{if .Current}}, [Current]({{.Current}}){{end}}{{if .Preview}}, [Preview]({{.Preview}}){{end}}|
|
|
||||||
{{- end}}
|
|
||||||
|
|
||||||
Click the preview links above to view how your changes will look and compare them with the original and current versions.`
|
|
||||||
|
|
||||||
type commentFile struct {
|
|
||||||
Filename string
|
Filename string
|
||||||
Path string
|
Path string
|
||||||
Action string
|
Action string
|
||||||
|
|
@ -503,13 +497,27 @@ type commentFile struct {
|
||||||
Original string
|
Original string
|
||||||
Current string
|
Current string
|
||||||
Preview string
|
Preview string
|
||||||
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// onPullRequestEvent is called when a pull request event is received
|
// onPullRequestEvent is called when a pull request event is received
|
||||||
// If the pull request is opened, reponed or synchronize, we read the files changed.
|
// If the pull request is opened, reponed or synchronize, we read the files changed.
|
||||||
func (r *githubRepository) onPullRequestEvent(ctx context.Context, logger *slog.Logger, event *github.PullRequestEvent, factory FileReplicatorFactory) error {
|
func (r *githubRepository) onPullRequestEvent(ctx context.Context, logger *slog.Logger, event *github.PullRequestEvent, factory FileReplicatorFactory) error {
|
||||||
action := event.GetAction()
|
action := event.GetAction()
|
||||||
r.logger.InfoContext(ctx, "processing pull request event", "number", event.GetNumber(), "action", action)
|
logger = logger.With("pull_request", event.GetNumber(), "action", action, "nnumber", event.GetNumber())
|
||||||
|
logger.InfoContext(
|
||||||
|
ctx,
|
||||||
|
"processing pull request event",
|
||||||
|
"linter",
|
||||||
|
r.Config().Spec.GitHub.PullRequestLinter,
|
||||||
|
"previews",
|
||||||
|
r.Config().Spec.GitHub.GenerateDashboardPreviews,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !r.Config().Spec.GitHub.PullRequestLinter && !r.Config().Spec.GitHub.GenerateDashboardPreviews {
|
||||||
|
logger.DebugContext(ctx, "no action required on pull request event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if event.GetRepo() == nil {
|
if event.GetRepo() == nil {
|
||||||
return fmt.Errorf("missing repository in pull request event")
|
return fmt.Errorf("missing repository in pull request event")
|
||||||
|
|
@ -530,7 +538,7 @@ func (r *githubRepository) onPullRequestEvent(ctx context.Context, logger *slog.
|
||||||
return fmt.Errorf("list pull request files: %w", err)
|
return fmt.Errorf("list pull request files: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows := make([]commentFile, 0)
|
changedResources := make([]changedResource, 0)
|
||||||
|
|
||||||
baseBranch := event.GetPullRequest().GetBase().GetRef()
|
baseBranch := event.GetPullRequest().GetBase().GetRef()
|
||||||
mainBranch := r.config.Spec.GitHub.Branch
|
mainBranch := r.config.Spec.GitHub.Branch
|
||||||
|
|
@ -542,9 +550,8 @@ func (r *githubRepository) onPullRequestEvent(ctx context.Context, logger *slog.
|
||||||
|
|
||||||
prURL := event.GetPullRequest().GetHTMLURL()
|
prURL := event.GetPullRequest().GetHTMLURL()
|
||||||
|
|
||||||
// TODO: implement the real handling of the files
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
row := commentFile{
|
resource := changedResource{
|
||||||
Filename: path.Base(file.GetFilename()),
|
Filename: path.Base(file.GetFilename()),
|
||||||
Path: file.GetFilename(),
|
Path: file.GetFilename(),
|
||||||
Action: file.GetStatus(),
|
Action: file.GetStatus(),
|
||||||
|
|
@ -558,23 +565,23 @@ func (r *githubRepository) onPullRequestEvent(ctx context.Context, logger *slog.
|
||||||
// reference: https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit
|
// reference: https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit
|
||||||
switch file.GetStatus() {
|
switch file.GetStatus() {
|
||||||
case "added":
|
case "added":
|
||||||
row.Preview = r.previewURL(ref, path, prURL)
|
resource.Preview = r.previewURL(ref, path, prURL)
|
||||||
case "modified":
|
case "modified":
|
||||||
row.Original = r.previewURL(baseBranch, path, prURL)
|
resource.Original = r.previewURL(baseBranch, path, prURL)
|
||||||
row.Current = r.previewURL(mainBranch, path, prURL)
|
resource.Current = r.previewURL(mainBranch, path, prURL)
|
||||||
row.Preview = r.previewURL(ref, path, prURL)
|
resource.Preview = r.previewURL(ref, path, prURL)
|
||||||
case "removed":
|
case "removed":
|
||||||
row.Original = r.previewURL(baseBranch, path, prURL)
|
resource.Original = r.previewURL(baseBranch, path, prURL)
|
||||||
row.Current = r.previewURL(mainBranch, path, prURL)
|
resource.Current = r.previewURL(mainBranch, path, prURL)
|
||||||
ref = baseBranch
|
ref = baseBranch
|
||||||
case "renamed":
|
case "renamed":
|
||||||
row.Original = r.previewURL(baseBranch, file.GetPreviousFilename(), prURL)
|
resource.Original = r.previewURL(baseBranch, file.GetPreviousFilename(), prURL)
|
||||||
row.Current = r.previewURL(mainBranch, file.GetPreviousFilename(), prURL)
|
resource.Current = r.previewURL(mainBranch, file.GetPreviousFilename(), prURL)
|
||||||
row.Preview = r.previewURL(ref, path, prURL)
|
resource.Preview = r.previewURL(ref, path, prURL)
|
||||||
case "changed":
|
case "changed":
|
||||||
row.Original = r.previewURL(baseBranch, path, prURL)
|
resource.Original = r.previewURL(baseBranch, path, prURL)
|
||||||
row.Current = r.previewURL(mainBranch, path, prURL)
|
resource.Current = r.previewURL(mainBranch, path, prURL)
|
||||||
row.Preview = r.previewURL(ref, path, prURL)
|
resource.Preview = r.previewURL(ref, path, prURL)
|
||||||
case "unchanged":
|
case "unchanged":
|
||||||
logger.InfoContext(ctx, "ignore unchanged file")
|
logger.InfoContext(ctx, "ignore unchanged file")
|
||||||
continue
|
continue
|
||||||
|
|
@ -589,6 +596,7 @@ func (r *githubRepository) onPullRequestEvent(ctx context.Context, logger *slog.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: how does this validation works vs linting?
|
||||||
ok, err := replicator.Validate(ctx, f)
|
ok, err := replicator.Validate(ctx, f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to validate file", "error", err)
|
logger.ErrorContext(ctx, "failed to validate file", "error", err)
|
||||||
|
|
@ -599,34 +607,133 @@ func (r *githubRepository) onPullRequestEvent(ctx context.Context, logger *slog.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource.Data = f.Data
|
||||||
logger.InfoContext(ctx, "resource changed")
|
logger.InfoContext(ctx, "resource changed")
|
||||||
rows = append(rows, row)
|
changedResources = append(changedResources, resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
if err := r.previewPullRequest(ctx, event.GetNumber(), changedResources); err != nil {
|
||||||
|
logger.ErrorContext(ctx, "failed to comment previews", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headSha := event.GetPullRequest().GetHead().GetSHA()
|
||||||
|
if err := r.lintPullRequest(ctx, logger, event.GetNumber(), headSha, changedResources); err != nil {
|
||||||
|
logger.ErrorContext(ctx, "failed to lint pull request resources", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var lintDashboardIssuesTemplate = `Hey there! 👋
|
||||||
|
Grafana found some linting issues in this dashboard you may want to check:
|
||||||
|
{{ range .}}
|
||||||
|
{{ if eq .Severity 4 }}❌{{ else if eq .Severity 3 }}⚠️ {{ end }} [dashboard-linter/{{ .Rule }}](https://github.com/grafana/dashboard-linter/blob/main/docs/rules/{{ .Rule }}.md): {{ .Message }}.
|
||||||
|
{{- end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
// lintPullRequest lints the files changed in the pull request and comments the issues found.
|
||||||
|
// The linter is disabled if the configuration does not have PullRequestLinter enabled.
|
||||||
|
// The only supported type of file to lint is a dashboard.
|
||||||
|
func (r *githubRepository) lintPullRequest(ctx context.Context, logger *slog.Logger, prNumber int, ref string, resources []changedResource) error {
|
||||||
|
if !r.Config().Spec.GitHub.PullRequestLinter {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := template.New("comment").Parse(commentTemplate)
|
logger.InfoContext(ctx, "lint pull request")
|
||||||
|
|
||||||
|
// Clear all previous comments because we don't know if the files have changed
|
||||||
|
if err := r.gh.ClearAllPullRequestFileComments(ctx, r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository, prNumber); err != nil {
|
||||||
|
return fmt.Errorf("clear pull request comments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, resource := range resources {
|
||||||
|
if resource.Action == "removed" || resource.Type != "dashboard" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := logger.With("file", resource.Path)
|
||||||
|
if err := r.lintPullRequestFile(ctx, logger, prNumber, ref, resource); err != nil {
|
||||||
|
logger.ErrorContext(ctx, "failed to lint file", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lintPullRequestFile lints a file and comments the issues found.
|
||||||
|
func (r *githubRepository) lintPullRequestFile(ctx context.Context, logger *slog.Logger, prNumber int, ref string, resource changedResource) error {
|
||||||
|
issues, err := r.linter.Lint(ctx, resource.Data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("lint file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(issues) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: we should not be compiling this all the time
|
||||||
|
tmpl, err := template.New("comment").Parse(lintDashboardIssuesTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse lint comment template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buf, issues); err != nil {
|
||||||
|
return fmt.Errorf("execute lint comment template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
comment := pgh.FileComment{
|
||||||
|
Content: buf.String(),
|
||||||
|
Path: resource.Path,
|
||||||
|
Position: 1, // create a top-level comment
|
||||||
|
Ref: ref,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: comment with Grafana Logo
|
||||||
|
// FIXME: comment author should be written by Grafana and not the user
|
||||||
|
if err := r.gh.CreatePullRequestFileComment(ctx, r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository, prNumber, comment); err != nil {
|
||||||
|
return fmt.Errorf("create pull request comment: %w", err)
|
||||||
|
}
|
||||||
|
logger.InfoContext(ctx, "lint comment created", "issues", len(issues))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewsCommentTemplate = `Hey there! 🎉
|
||||||
|
Grafana spotted some changes for your resources in this pull request:
|
||||||
|
|
||||||
|
| File Name | Type | Path | Action | Links |
|
||||||
|
|-----------|------|------|--------|-------|
|
||||||
|
{{- range .}}
|
||||||
|
| {{.Filename}} | {{.Type}} | {{.Path}} | {{.Action}} | {{if .Original}}[Original]({{.Original}}){{end}}{{if .Current}}, [Current]({{.Current}}){{end}}{{if .Preview}}, [Preview]({{.Preview}}){{end}}|
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
Click the preview links above to view how your changes will look and compare them with the original and current versions.`
|
||||||
|
|
||||||
|
func (r *githubRepository) previewPullRequest(ctx context.Context, prNumber int, resources []changedResource) error {
|
||||||
|
if !r.Config().Spec.GitHub.GenerateDashboardPreviews || len(resources) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: we should not be compiling this all the time
|
||||||
|
tmpl, err := template.New("comment").Parse(previewsCommentTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse comment template: %w", err)
|
return fmt.Errorf("parse comment template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := tmpl.Execute(&buf, rows); err != nil {
|
if err := tmpl.Execute(&buf, resources); err != nil {
|
||||||
return fmt.Errorf("execute comment template: %w", err)
|
return fmt.Errorf("execute comment template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
comment := buf.String()
|
comment := buf.String()
|
||||||
|
|
||||||
// TODO: comment with Grafana Logo
|
// FIXME: comment with Grafana Logo
|
||||||
// TODO: comment author should be written by Grafana and not the user
|
// FIXME: comment author should be written by Grafana and not the user
|
||||||
if err := r.gh.CreatePullRequestComment(ctx, r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository, event.GetNumber(), comment); err != nil {
|
if err := r.gh.CreatePullRequestComment(ctx, r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository, prNumber, comment); err != nil {
|
||||||
return fmt.Errorf("create pull request comment: %w", err)
|
return fmt.Errorf("create pull request comment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.logger.InfoContext(ctx, "comment created", "pull_request", event.GetNumber(), "num_changes", len(rows))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ type Client interface {
|
||||||
|
|
||||||
ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error)
|
ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error)
|
||||||
CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error
|
CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error
|
||||||
|
CreatePullRequestFileComment(ctx context.Context, owner, repository string, number int, comment FileComment) error
|
||||||
|
ClearAllPullRequestFileComments(ctx context.Context, owner, repository string, number int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepositoryContent interface {
|
type RepositoryContent interface {
|
||||||
|
|
@ -106,6 +108,13 @@ type CommitFile interface {
|
||||||
GetStatus() string
|
GetStatus() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileComment struct {
|
||||||
|
Content string
|
||||||
|
Path string
|
||||||
|
Position int
|
||||||
|
Ref string
|
||||||
|
}
|
||||||
|
|
||||||
type CreateFileOptions struct {
|
type CreateFileOptions struct {
|
||||||
// The message of the commit. May be empty, in which case a default value is entered.
|
// The message of the commit. May be empty, in which case a default value is entered.
|
||||||
Message string
|
Message string
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,24 @@ func (_m *MockClient) BranchExists(ctx context.Context, owner string, repository
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearAllPullRequestFileComments provides a mock function with given fields: ctx, owner, repository, number
|
||||||
|
func (_m *MockClient) ClearAllPullRequestFileComments(ctx context.Context, owner string, repository string, number int) error {
|
||||||
|
ret := _m.Called(ctx, owner, repository, number)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for ClearAllPullRequestFileComments")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, string, int) error); ok {
|
||||||
|
r0 = rf(ctx, owner, repository, number)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// CreateBranch provides a mock function with given fields: ctx, owner, repository, sourceBranch, branchName
|
// CreateBranch provides a mock function with given fields: ctx, owner, repository, sourceBranch, branchName
|
||||||
func (_m *MockClient) CreateBranch(ctx context.Context, owner string, repository string, sourceBranch string, branchName string) error {
|
func (_m *MockClient) CreateBranch(ctx context.Context, owner string, repository string, sourceBranch string, branchName string) error {
|
||||||
ret := _m.Called(ctx, owner, repository, sourceBranch, branchName)
|
ret := _m.Called(ctx, owner, repository, sourceBranch, branchName)
|
||||||
|
|
@ -95,6 +113,24 @@ func (_m *MockClient) CreatePullRequestComment(ctx context.Context, owner string
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreatePullRequestFileComment provides a mock function with given fields: ctx, owner, repository, number, comment
|
||||||
|
func (_m *MockClient) CreatePullRequestFileComment(ctx context.Context, owner string, repository string, number int, comment FileComment) error {
|
||||||
|
ret := _m.Called(ctx, owner, repository, number, comment)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for CreatePullRequestFileComment")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, string, int, FileComment) error); ok {
|
||||||
|
r0 = rf(ctx, owner, repository, number, comment)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// CreateWebhook provides a mock function with given fields: ctx, owner, repository, cfg
|
// CreateWebhook provides a mock function with given fields: ctx, owner, repository, cfg
|
||||||
func (_m *MockClient) CreateWebhook(ctx context.Context, owner string, repository string, cfg WebhookConfig) error {
|
func (_m *MockClient) CreateWebhook(ctx context.Context, owner string, repository string, cfg WebhookConfig) error {
|
||||||
ret := _m.Called(ctx, owner, repository, cfg)
|
ret := _m.Called(ctx, owner, repository, cfg)
|
||||||
|
|
|
||||||
|
|
@ -344,6 +344,55 @@ func (r *realImpl) CreatePullRequestComment(ctx context.Context, owner, reposito
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *realImpl) CreatePullRequestFileComment(ctx context.Context, owner, repository string, number int, comment FileComment) error {
|
||||||
|
commentRequest := &github.PullRequestComment{
|
||||||
|
Body: &comment.Content,
|
||||||
|
CommitID: &comment.Ref,
|
||||||
|
Path: &comment.Path,
|
||||||
|
Position: &comment.Position,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := r.gh.PullRequests.CreateComment(ctx, owner, repository, number, commentRequest); err != nil {
|
||||||
|
var ghErr *github.ErrorResponse
|
||||||
|
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||||
|
return ErrServiceUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *realImpl) ClearAllPullRequestFileComments(ctx context.Context, owner, repository string, number int) error {
|
||||||
|
comments, _, err := r.gh.PullRequests.ListComments(ctx, owner, repository, number, nil)
|
||||||
|
if err != nil {
|
||||||
|
var ghErr *github.ErrorResponse
|
||||||
|
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||||
|
return ErrServiceUnavailable
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userLogin, _, err := r.gh.Users.Get(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range comments {
|
||||||
|
// skip if comments were not created by us
|
||||||
|
if c.User.GetLogin() != userLogin.GetLogin() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.gh.PullRequests.DeleteComment(ctx, owner, repository, c.GetID()); err != nil {
|
||||||
|
return fmt.Errorf("delete comment: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type realRepositoryContent struct {
|
type realRepositoryContent struct {
|
||||||
real *github.RepositoryContent
|
real *github.RepositoryContent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,7 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||||
"branchWorkflow": true,
|
"branchWorkflow": true,
|
||||||
"generateDashboardPreviews": true,
|
"generateDashboardPreviews": true,
|
||||||
"owner": "grafana",
|
"owner": "grafana",
|
||||||
|
"pullRequestLinter": true,
|
||||||
"repository": "git-ui-sync-demo",
|
"repository": "git-ui-sync-demo",
|
||||||
"token": "github_pat_dummy",
|
"token": "github_pat_dummy",
|
||||||
"webhookSecret": "dummyWebhookSecret",
|
"webhookSecret": "dummyWebhookSecret",
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,6 @@ spec:
|
||||||
branch: dummy-branch
|
branch: dummy-branch
|
||||||
branchWorkflow: true
|
branchWorkflow: true
|
||||||
generateDashboardPreviews: true
|
generateDashboardPreviews: true
|
||||||
|
pullRequestLinter: true
|
||||||
token: "github_pat_dummy"
|
token: "github_pat_dummy"
|
||||||
webhookSecret: "dummyWebhookSecret"
|
webhookSecret: "dummyWebhookSecret"
|
||||||
Loading…
Reference in New Issue