From 27058924341721eb20bea43e3f005cf094b17b29 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 8 May 2024 13:26:03 +0200 Subject: [PATCH 1/2] feat: add support for evaluating all open MRs --- .gitlab-ci.yml | 2 +- .scm-engine.example.yml | 34 +++++++++++++++++++++---- cmd/cmd_evaluate.go | 18 +++++++++++-- pkg/scm/gitlab/client_merge_request.go | 35 ++++++++++++++++++++++++++ pkg/scm/interfaces.go | 1 + pkg/scm/types.go | 10 ++++++++ schema/gitlab.go | 9 ++++++- schema/gitlab.schema.graphqls | 32 +++++++++++++++++++++++ 8 files changed, 132 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b325f59..caf4d0e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -run::cli: +scm-engine::evaluate: image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golang:1.22.3 rules: - if: $CI_PIPELINE_SOURCE == 'merge_request_event' diff --git a/.scm-engine.example.yml b/.scm-engine.example.yml index 4428656..b2d4024 100644 --- a/.scm-engine.example.yml +++ b/.scm-engine.example.yml @@ -2,20 +2,44 @@ actions: - name: Warn that the ticket if older than 30 days - if: merge_request.state != "closed" && merge_request.time_since_last_commit > duration("30d") && not merge_request.has_label("do-not-close") + if: | + // ignore MRs already closed + merge_request.state != "closed" + // if last commit happened more than 30 days ago + && merge_request.time_since_last_commit > duration("30d") + // but still less than 45 days ago (where we close the MR) + && merge_request.time_since_last_commit < duration("45d") + // and the label to disable this feature is not on the MR + && not merge_request.has_label("do-not-close") then: - action: comment - message: "Hey, this MR is old, we will close it in 15 days if no activity has happened. If you want to disable this behavior, add the label 'do-not-close' on the MR." + message: | + Hello! + + This Merge Request have not seen any commit activity for 30 days. In an effort to keep our project clean we will automatically close the Merge request after 45 days. + + You can add the "do-not-close" label to the Merge Request to disable this behavior. - name: Close ticket if older than 45 days - if: merge_request.state != "closed" && merge_request.time_since_last_commit > duration("45d") && not merge_request.has_label("do-not-close") + if: | + merge_request.state != "closed" + && merge_request.time_since_last_commit > duration("45d") + && not merge_request.has_label("do-not-close") then: - action: close - action: comment - message: "As promised, we're closing the MR due to inactivity, bye bye" + message: | + Hello! + + This Merge Request have not seen any commit activity for 45 days. In an effort to keep our project clean we will automatically close the Merge request. + + You can add the "do-not-close" label to the Merge Request to disable this behavior. - name: Approve MR if the 'break-glass-approve' label is configured - if: merge_request.state != "closed" && not merge_request.approved && merge_request.has_label("break-glass-approve") + if: | + merge_request.state != "closed" + && not merge_request.approved + && merge_request.has_label("break-glass-approve") then: - action: approve - action: comment diff --git a/cmd/cmd_evaluate.go b/cmd/cmd_evaluate.go index f66f4a0..080dc07 100644 --- a/cmd/cmd_evaluate.go +++ b/cmd/cmd_evaluate.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/jippi/scm-engine/pkg/config" + "github.com/jippi/scm-engine/pkg/scm" "github.com/jippi/scm-engine/pkg/scm/gitlab" "github.com/jippi/scm-engine/pkg/state" "github.com/urfave/cli/v2" @@ -23,6 +24,19 @@ func Evaluate(cCtx *cli.Context) error { } switch { + // If first arg is 'all' we will find all opened MRs and apply the rules to them + case cCtx.Args().First() == "all": + res, err := client.MergeRequests().List(ctx, &scm.ListMergeRequestsOptions{State: "opened", First: 100}) + if err != nil { + return err + } + + for _, mr := range res { + if err := ProcessMR(ctx, client, cfg, mr.ID); err != nil { + return err + } + } + // If the flag is set, use that for evaluation case cCtx.String(FlagMergeRequestID) != "": return ProcessMR(ctx, client, cfg, cCtx.String(FlagMergeRequestID)) @@ -37,7 +51,7 @@ func Evaluate(cCtx *cli.Context) error { return err } } - - return nil } + + return nil } diff --git a/pkg/scm/gitlab/client_merge_request.go b/pkg/scm/gitlab/client_merge_request.go index babb4be..f8e9c76 100644 --- a/pkg/scm/gitlab/client_merge_request.go +++ b/pkg/scm/gitlab/client_merge_request.go @@ -5,9 +5,11 @@ import ( "fmt" "net/http" + "github.com/hasura/go-graphql-client" "github.com/jippi/scm-engine/pkg/scm" "github.com/jippi/scm-engine/pkg/state" go_gitlab "github.com/xanzy/go-gitlab" + "golang.org/x/oauth2" ) var _ scm.MergeRequestClient = (*MergeRequestClient)(nil) @@ -43,3 +45,36 @@ func (client *MergeRequestClient) Update(ctx context.Context, opt *scm.UpdateMer return convertResponse(resp), err } + +func (client *MergeRequestClient) List(ctx context.Context, options *scm.ListMergeRequestsOptions) ([]scm.ListMergeRequest, error) { + httpClient := oauth2.NewClient( + ctx, + oauth2.StaticTokenSource( + &oauth2.Token{ + AccessToken: client.client.token, + }, + ), + ) + + graphqlClient := graphql.NewClient(graphqlBaseURL(client.client.wrapped.BaseURL())+"/api/graphql", httpClient) + + var ( + result *ListMergeRequestsQuery + variables = map[string]any{ + "project_id": graphql.ID(state.ProjectIDFromContext(ctx)), + "state": MergeRequestState(options.State), + "first": options.First, + } + ) + + if err := graphqlClient.Query(ctx, &result, variables); err != nil { + return nil, err + } + + hits := []scm.ListMergeRequest{} + for _, x := range result.Project.MergeRequests.Nodes { + hits = append(hits, scm.ListMergeRequest{ID: x.ID}) + } + + return hits, nil +} diff --git a/pkg/scm/interfaces.go b/pkg/scm/interfaces.go index 9b580e2..e6801b9 100644 --- a/pkg/scm/interfaces.go +++ b/pkg/scm/interfaces.go @@ -19,6 +19,7 @@ type LabelClient interface { type MergeRequestClient interface { Update(ctx context.Context, opt *UpdateMergeRequestOptions) (*Response, error) + List(ctx context.Context, options *ListMergeRequestsOptions) ([]ListMergeRequest, error) } type EvalContext interface { diff --git a/pkg/scm/types.go b/pkg/scm/types.go index eb1bdae..55df2e6 100644 --- a/pkg/scm/types.go +++ b/pkg/scm/types.go @@ -94,6 +94,16 @@ type ListOptions struct { Sort string `json:"sort,omitempty" url:"sort,omitempty"` } +type ListMergeRequestsOptions struct { + ListOptions + State string + First int +} + +type ListMergeRequest struct { + ID string `expr:"id" graphql:"id"` +} + // Response is a GitLab API response. This wraps the standard http.Response // returned from GitLab and provides convenient access to things like // pagination links. diff --git a/schema/gitlab.go b/schema/gitlab.go index befb30e..cec6194 100644 --- a/schema/gitlab.go +++ b/schema/gitlab.go @@ -67,7 +67,12 @@ func main() { func nest(props []*Property) { for _, field := range props { if field.IsCustomType { - for _, nested := range PropMap[field.Type].Attributes { + attr, ok := PropMap[field.Type] + if !ok { + continue + } + + for _, nested := range attr.Attributes { field.AddAttribute(&Property{ Name: nested.Name, Description: nested.Description, @@ -149,6 +154,8 @@ func mutateHook(b *modelgen.ModelBuild) *modelgen.ModelBuild { Description: model.Description, } + fmt.Println("model", modelProperty.Name) + for _, field := range model.Fields { tags, err := structtag.Parse(field.Tag) if err != nil { diff --git a/schema/gitlab.schema.graphqls b/schema/gitlab.schema.graphqls index 4042b8d..f087a14 100644 --- a/schema/gitlab.schema.graphqls +++ b/schema/gitlab.schema.graphqls @@ -21,6 +21,7 @@ scalar Time # Add time.Duration support scalar Duration + type Context { "The project the Merge Request belongs to" Project: ContextProject @graphql(key: "project(fullPath: $project_id)") @@ -32,6 +33,37 @@ type Context { MergeRequest: ContextMergeRequest @generated } +enum MergeRequestState { + all + closed + locked + merged + opened +} + +input ListMergeRequestsQueryInput { + project_id: ID! + state: MergeRequestState! = "opened" + first: Int! = 100 +} + +type ListMergeRequestsQuery { + "The project the Merge Request belongs to" + Project: ListMergeRequestsProject @graphql(key: "project(fullPath: $project_id)") +} + +type ListMergeRequestsProject { + MergeRequests: ListMergeRequestsProjectMergeRequestNodes @graphql(key: "mergeRequests(state: $state, first: $first)") @internal +} + +type ListMergeRequestsProjectMergeRequestNodes { + Nodes: [ListMergeRequestsProjectMergeRequest!] +} + +type ListMergeRequestsProjectMergeRequest { + ID: String! @graphql(key: "iid") @internal +} + type ContextProject { # # Native GraphQL fields - https://docs.gitlab.com/ee/api/graphql/reference/#project From da42ab68d736b0f7d2931952d082e7c021f85817 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 8 May 2024 13:36:09 +0200 Subject: [PATCH 2/2] update example --- .gitlab-ci.yml | 13 ++++++++++++- README.md | 9 ++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index caf4d0e..415ef82 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,18 @@ -scm-engine::evaluate: +# NOTE: This is integration testing file, its not meant for production usage +# The README has a production example + +scm-engine::evaluate::on-change: image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golang:1.22.3 rules: - if: $CI_PIPELINE_SOURCE == 'merge_request_event' script: - go mod tidy - go run ./cmd/scm-engine/ evaluate + +scm-engine::evaluate::on-schedule: + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golang:1.22.3 + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + script: + - go mod tidy + - go run ./cmd/scm-engine/ evaluate all diff --git a/README.md b/README.md index a3fdbaf..fd6d5de 100644 --- a/README.md +++ b/README.md @@ -119,12 +119,19 @@ Using scm-engine within a GitLab CI pipeline is straight forward. 1. Setup a CI job using the `scm-engine` Docker image that will run when a pipeline is created from a Merge Request Event. ```yaml - scm-engine: + scm-engine::evaluate::on-merge-request-event: image: ghcr.io/jippi/scm-engine:latest rules: - if: $CI_PIPELINE_SOURCE == 'merge_request_event' script: - scm-engine evaluate + + scm-engine::evaluate::on-schedule: + image: ghcr.io/jippi/scm-engine:latest + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + script: + - scm-engine evaluate all ``` 1. Done! Every Merge Request change should now re-run scm-engine and apply your label rules