From 27058924341721eb20bea43e3f005cf094b17b29 Mon Sep 17 00:00:00 2001
From: Christian Winther <christian@seatgeek.com>
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 <christian@seatgeek.com>
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