Skip to content

Commit c88a0e4

Browse files
authored
feat: Assign Reviewers from Backstage Owners in GitLab  (#3)
* Install a backstage go client * Add flags for Backstage instance * Initialize backstage client in gitlab client * Client tweaks and ensure flags get passed to the evaluate and server command * Implement Backstage queries for members * Default Backstage namespace is always default * Backstage client package * Also remove namespace from here * Changes to Backstage client initialization * Refactor Backstage client integration * Make source required for assign_reviewer * Remove unnecessary break * Use go-vcr for backstage client tests * Add Backstage client test for getting a user * Add Backstage client test for getting members of group * Add Backstage client test for getting owners by project name * Break out test helpers into testutils and add test for backstage assign action * Fix test * Update action schema * Docs
1 parent d2ac59e commit c88a0e4

File tree

29 files changed

+1152
-9
lines changed

29 files changed

+1152
-9
lines changed

cmd/conventions.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package cmd
22

3+
import "github.com/urfave/cli/v2"
4+
35
const (
46
FlagAPIToken = "api-token"
7+
FlagBackstageURL = "backstage-url"
8+
FlagBackstageToken = "backstage-token"
59
FlagCommitSHA = "commit"
610
FlagConfigFile = "config"
711
FlagDryRun = "dry-run"
@@ -20,3 +24,20 @@ const (
2024
FlagPeriodicEvaluationOnlyProjectsWithMembership = "periodic-evaluation-only-project-membership"
2125
FlagWebhookSecret = "webhook-secret"
2226
)
27+
28+
var (
29+
StringFlagBackstageURL = &cli.StringFlag{
30+
Name: FlagBackstageURL,
31+
Usage: "The Backstage base URL",
32+
EnvVars: []string{
33+
"BACKSTAGE_URL", // Backstage catalog integration
34+
},
35+
}
36+
StringFlagBackstageToken = &cli.StringFlag{
37+
Name: FlagBackstageToken,
38+
Usage: "The Backstage static token with access to the catalog plugin", // https://backstage.io/docs/auth/service-to-service-auth/#static-tokens
39+
EnvVars: []string{
40+
"BACKSTAGE_TOKEN", // Backstage catalog integration
41+
},
42+
}
43+
)

cmd/gitlab.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ var GitLab = &cli.Command{
9393
"CI_COMMIT_SHA", // GitLab CI
9494
},
9595
},
96+
StringFlagBackstageURL,
97+
StringFlagBackstageToken,
9698
},
9799
},
98100
{
@@ -184,6 +186,8 @@ var GitLab = &cli.Command{
184186
"SCM_ENGINE_PERIODIC_EVALUATION_ONLY_PROJECTS_WITH_MEMBERSHIP",
185187
},
186188
},
189+
StringFlagBackstageURL,
190+
StringFlagBackstageToken,
187191
},
188192
},
189193
},

cmd/gitlab_evaluate.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ func Evaluate(cCtx *cli.Context) error {
1717
ctx = state.WithToken(ctx, cCtx.String(FlagAPIToken))
1818
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline), cCtx.String(FlagUpdatePipelineURL))
1919

20+
// Optional Backstage catalog integration
21+
ctx = state.WithBackstageURL(ctx, cCtx.String(FlagBackstageURL))
22+
ctx = state.WithBackstageToken(ctx, cCtx.String(FlagBackstageToken))
23+
2024
cfg, err := config.LoadFile(state.ConfigFilePath(ctx))
2125
if err != nil {
2226
return err

cmd/gitlab_server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ func Server(cCtx *cli.Context) error {
2626
ctx = state.WithConfigFilePath(ctx, cCtx.String(FlagConfigFile))
2727
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline), cCtx.String(FlagUpdatePipelineURL))
2828

29+
// Optional Backstage catalog integration
30+
ctx = state.WithBackstageURL(ctx, cCtx.String(FlagBackstageURL))
31+
ctx = state.WithBackstageToken(ctx, cCtx.String(FlagBackstageToken))
32+
2933
// Add logging context key/value pairs
3034
ctx = slogctx.With(ctx, slog.String("gitlab_url", cCtx.String(FlagSCMBaseURL)))
3135
ctx = slogctx.With(ctx, slog.Duration("server_timeout", cCtx.Duration(FlagServerTimeout)))

cmd/shared.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/jippi/scm-engine/pkg/config"
12+
"github.com/jippi/scm-engine/pkg/integration/backstage"
1213
"github.com/jippi/scm-engine/pkg/scm"
1314
"github.com/jippi/scm-engine/pkg/scm/github"
1415
"github.com/jippi/scm-engine/pkg/scm/gitlab"
@@ -20,12 +21,17 @@ import (
2021
var sid = shortid.MustNew(1, shortid.DefaultABC, 2342)
2122

2223
func getClient(ctx context.Context) (scm.Client, error) {
24+
backstageClient, err := backstage.NewClient(ctx, state.BackstageURL(ctx), state.BackstageToken(ctx), nil)
25+
if err != nil {
26+
slogctx.Warn(ctx, "Backstage client is not available, actions requiring it will be skipped", slog.Any("error", err))
27+
}
28+
2329
switch state.Provider(ctx) {
2430
case "github":
2531
return github.NewClient(ctx), nil
2632

2733
case "gitlab":
28-
return gitlab.NewClient(ctx)
34+
return gitlab.NewClient(ctx, backstageClient)
2935

3036
default:
3137
return nil, fmt.Errorf("unknown provider %q - we only support 'github' and 'gitlab'", state.Provider(ctx))

docs/gitlab/examples.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,48 @@ actions:
186186
/* Remove duplicate values from the output */
187187
| uniq()
188188
```
189+
190+
## Assign reviewers from CODEOWNERS
191+
192+
The `codeowners` source relies on the GitLab [CODE OWNERS](https://docs.gitlab.com/ee/user/project/codeowners/) feature.
193+
194+
For a reviewer to be assigned to a Merge Request, the project must have a CODEOWNERS file and the groups/users mentioned in the file must have [direct membership](https://gitlab.com/gitlab-org/gitlab/-/issues/288851/) to the project.
195+
196+
```yaml
197+
# yaml-language-server: $schema=https://jippi.github.io/scm-engine/scm-engine.schema.json
198+
199+
actions:
200+
- name: "assign"
201+
if: |1
202+
--8<-- "docs/gitlab/snippets/assign-merge-request/assign-if.expr"
203+
then:
204+
- action: assign_reviewers
205+
source: codeowners
206+
limit: 1
207+
mode: random
208+
```
209+
210+
## Assign reviewers from Backstage
211+
212+
The `backstage` source relies on the [Backstage Catalog](https://backstage.io/docs/features/software-catalog/) to derive ownership information.
213+
214+
This assumes [System](https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system) and [User](https://backstage.io/docs/features/software-catalog/descriptor-format/#kind-user) in Backstage can be mapped directly to a GitLab project and user.
215+
216+
For a Backstage System entity to be mapped to a GitLab project, the system must have same name as the GitLab project or the `gitlab.com/project` annotation is set to the GitLab project name.
217+
218+
For a Backstage User entity to be mapped to a GitLab user, the user must have the `gitlab.com/user_id` annotation set to the numeric ID of the user. A plugin that can be used to automatically set this annotation is [@seatgeek/backstage-plugin-gitlab-catalog-backend](https://github.com/seatgeek/backstage-plugins/tree/main/plugins/gitlab-catalog-backend).
219+
220+
221+
```yaml
222+
# yaml-language-server: $schema=https://jippi.github.io/scm-engine/scm-engine.schema.json
223+
224+
actions:
225+
- name: "assign"
226+
if: |1
227+
--8<-- "docs/gitlab/snippets/assign-merge-request/assign-if.expr"
228+
then:
229+
- action: assign_reviewers
230+
source: backstage
231+
limit: 1
232+
mode: random
233+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
merge_request.state_is("opened")
2+
&& not merge_request.approved

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/99designs/gqlgen v0.17.57
77
github.com/aquilax/truncate v1.0.0
88
github.com/charmbracelet/lipgloss v1.0.0
9+
github.com/datolabs-io/go-backstage/v3 v3.0.0
910
github.com/davecgh/go-spew v1.1.1
1011
github.com/expr-lang/expr v1.16.9
1112
github.com/fatih/structtag v1.2.0
@@ -31,7 +32,9 @@ require (
3132
github.com/xanzy/go-gitlab v0.114.0
3233
github.com/xhit/go-str2duration/v2 v2.1.0
3334
golang.org/x/oauth2 v0.24.0
35+
gopkg.in/dnaeon/go-vcr.v4 v4.0.2
3436
gopkg.in/yaml.v3 v3.0.1
37+
gotest.tools/v3 v3.5.1
3538
)
3639

3740
require (
@@ -44,6 +47,7 @@ require (
4447
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
4548
github.com/fatih/color v1.17.0 // indirect
4649
github.com/go-logr/logr v1.4.1 // indirect
50+
github.com/google/go-cmp v0.6.0 // indirect
4751
github.com/google/go-querystring v1.1.0 // indirect
4852
github.com/google/uuid v1.6.0 // indirect
4953
github.com/hashicorp/errwrap v1.0.0 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NA
2222
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
2323
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
2424
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
25+
github.com/datolabs-io/go-backstage/v3 v3.0.0 h1:AaaA5PRhriPp+WM3siGEV5YLlV0IxXe2XIftsoi9wYg=
26+
github.com/datolabs-io/go-backstage/v3 v3.0.0/go.mod h1:xzfVJBuLKDbYXuYWmlimS9fX13OQYkYyl70UbQMeXXU=
2527
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2628
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2729
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -54,6 +56,10 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
5456
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
5557
github.com/guregu/null/v5 v5.0.0 h1:PRxjqyOekS11W+w/7Vfz6jgJE/BCwELWtgvOJzddimw=
5658
github.com/guregu/null/v5 v5.0.0/go.mod h1:SjupzNy+sCPtwQTKWhUCqjhVCO69hpsl2QsZrWHjlwU=
59+
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
60+
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
61+
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
62+
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
5763
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
5864
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
5965
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -156,9 +162,13 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
156162
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
157163
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
158164
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
165+
gopkg.in/dnaeon/go-vcr.v4 v4.0.2 h1:7T5VYf2ifyK01ETHbJPl5A6XTpUljD4Trw3GEDcdedk=
166+
gopkg.in/dnaeon/go-vcr.v4 v4.0.2/go.mod h1:65yxh9goQVrudqofKtHA4JNFWd6XZRkWfKN4YpMx7KI=
159167
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
160168
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
161169
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
170+
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
171+
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
162172
modernc.org/b/v2 v2.1.0 h1:kMD/G43EYnsFJI/0qK1F1X659XlSs41bp01MUDidHC0=
163173
modernc.org/b/v2 v2.1.0/go.mod h1:fQhHWDXrchyUSLjQYCslV/4uw04PW1LeiZ25D4SNmeo=
164174
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=

pkg/config/action_step.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ type AssignReviewers struct {
7777
BaseAction
7878

7979
// The source of the reviewers
80-
Source *string `json:"source,omitempty" yaml:"source,omitempty" jsonschema:"enum=codeowners"`
80+
Source string `json:"source" yaml:"source,omitempty" jsonschema:"enum=codeowners,enum=backstage"`
8181
// The max number of reviewers to assign
8282
Limit int `json:"limit,omitempty" yaml:"limit,omitempty"`
8383
// The mode of assigning reviewers

0 commit comments

Comments
 (0)