Skip to content
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
43 changes: 31 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
![screen](screen.webp)

## Features
- rule for title
- rule for approvals
- rule for approvers
- merge on command
- update branch (pull changes from master)
- delete stale branches
- [x] rule for title
- [x] rule for approvals
- [x] rule for approvers
- [x] merge on command
- [x] update branch (pull changes from master)
- [x] delete stale branches


## Table of Contents
Expand All @@ -18,10 +18,13 @@
- [Docker-compose](#Docker-compose)
- [Helm](#helm)
- [CLI](#cli)
- [Required bot permissions](#required-bot-permissions)
- [Webhook secret](#webhook-secret)
- [Stale branches](#stale-branches)
- [Config file](#config-file)
- [Example](#example)
- [Demo project on gitlab](https://gitlab.com/Gasoid/sugar-test)
- [Required bot permissions](#required-bot-permissions)


### Demo repo

Expand All @@ -33,7 +36,7 @@ https://gitlab.com/Gasoid/sugar-test
- `!update`: updates the branch from master/main (default branch) changes

### Use-cases
Given a lot of repos, therefore we various rules for each of them. It is complicated and tedious to run as many bot instances as teams.
Given a lot of repos, therefore we require to set up various rules for each of them. It is complicated and tedious to run as many bot instances as teams.
The Merge-bot checks whether MRs meet rules of the repository (.mrbot.yaml file). Owner of repo can create his own set of rules.

## Installation
Expand Down Expand Up @@ -63,6 +66,7 @@ GITLAB_TOKEN="your_token"
docker-compose up -d
```

3. set up webhook please follow these instruction [Gitlab Cloud](#gitlab-cloud)

### Helm

Expand All @@ -84,6 +88,8 @@ To uninstall the chart:

helm uninstall my-merge-bot

In order to set up webhook, please read [Gitlab Cloud](#gitlab-cloud)

### CLI

Create personal/repo/org token in gitlab, copy it and set as env variable
Expand Down Expand Up @@ -114,6 +120,23 @@ Run bot
go run ./
```

### Required bot permissions
- Bot must have __Maintainer__ role in order to comment, merge and delete branches
- Access Token must have following permissions: api, read_repository, write_repository

### Webhook secret
You can enforce security by using `secret`.

1. You need to create CI/CD var `MERGE_BOT_SECRET` in your project with your secure/random value. This var will be compared with webhook secret.
2. Set up the same webhook secret as `MERGE_BOT_SECRET` value.

The bot will read `MERGE_BOT_SECRET` value, if it doesn't exist, it will be considered as empty string ("").

### Stale branches
If `stale branches deletion` feature is enabled, deletion of stale branches will work.
The bot deletes stale branches once a MR is merged.



## Config file

Expand Down Expand Up @@ -162,7 +185,3 @@ stale_branches_deletion:
```

place it in root of your repo and name it `.mrbot.yaml`

### Required bot permissions
- Bot must have __Maintainer__ role in order to comment, merge and delete branches
- Access Token must have following permissions: api, read_repository, write_repository
6 changes: 6 additions & 0 deletions bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,14 @@ func Handler(c echo.Context) error {
// return
// }

if !command.ValidateSecret(hook.GetProjectID(), hook.GetSecret()) {
slog.Error("webhook secret is not valid", "projectId", hook.GetProjectID(), "provider", providerName)
return
}

if err := f(command, hook); err != nil {
slog.Error("handlerFunc returns err", "provider", providerName, "event", hook.Event, "err", err)
return
}
}()
}
Expand Down
5 changes: 5 additions & 0 deletions bot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type testWebhookProvider struct {
id int
projectID int
cmd string
secret string
err error
}

Expand All @@ -34,6 +35,10 @@ func (p *testWebhookProvider) GetProjectID() int {
return p.projectID
}

func (p *testWebhookProvider) GetSecret() string {
return p.secret
}

func newTestProvider() webhook.Provider {
return &testWebhookProvider{}
}
Expand Down
18 changes: 16 additions & 2 deletions handlers/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package gitlab

import (
b64 "encoding/base64"
"fmt"
"log/slog"
"mergebot/config"
"mergebot/handlers"
"net/http"

"github.com/xanzy/go-gitlab"
)
Expand All @@ -22,8 +24,6 @@ var (
)

const (
// gitlabToken = "GITLAB_TOKEN"
// gitlabUrl = "GITLAB_URL"
tokenUsername = "oauth2"
maxRepoSize = 1000 * 1000 * 500 // 500Mb
)
Expand Down Expand Up @@ -201,6 +201,20 @@ func (g *GitlabProvider) GetMRInfo(projectId, mergeId int, configPath string) (*
return &info, nil
}

func (g *GitlabProvider) GetVar(projectId int, varName string) (string, error) {
secretVar, resp, err := g.client.ProjectVariables.GetVariable(projectId, varName, &gitlab.GetProjectVariableOptions{})
if err != nil {
if resp.StatusCode == http.StatusNotFound {
slog.Debug("variable not found", "varName", varName, "projectId", projectId)
return "", nil
}

return "", fmt.Errorf("couldn't get variable %s because gitlab instance returns err: %w", varName, err)
}

return secretVar.Value, nil
}

func (g *GitlabProvider) ListBranches(projectId int) ([]handlers.Branch, error) {
branches, _, err := g.client.Branches.ListBranches(projectId, &gitlab.ListBranchesOptions{})
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions handlers/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type RequestProvider interface {
UpdateFromMaster(projectId, mergeId int) error
ListBranches(projectId int) ([]Branch, error)
DeleteBranch(projectId int, name string) error
GetVar(projectId int, varName string) (string, error)
}

type Config struct {
Expand Down
14 changes: 14 additions & 0 deletions handlers/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"html/template"
"log/slog"
"strings"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -157,3 +158,16 @@ func (r *Request) UpdateFromMaster(projectId, id int) error {
}
return nil
}

func (r Request) ValidateSecret(projectId int, secret string) bool {
const mergeBotSecret = "MERGE_BOT_SECRET"

secretVar, err := r.provider.GetVar(projectId, mergeBotSecret)
if err != nil {
slog.Error("cound't validate secret", "err", err)

return false
}

return secretVar == secret
}
4 changes: 4 additions & 0 deletions handlers/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ func (p *testProvider) DeleteBranch(projectId int, name string) error {
return nil
}

func (p *testProvider) GetVar(projectId int, varName string) (string, error) {
return "test", nil
}

func (p *testProvider) GetMRInfo(projectId, id int, path string) (*MrInfo, error) {
return &MrInfo{
Title: p.title,
Expand Down
7 changes: 7 additions & 0 deletions webhook/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ type GitlabProvider struct {
action string
projectId int
id int
secret string
}

func New() webhook.Provider {
return &GitlabProvider{}
}

func (g GitlabProvider) GetSecret() string {
return g.secret
}

func (g *GitlabProvider) ParseRequest(request *http.Request) error {
var err error
var ok bool
Expand All @@ -50,6 +55,8 @@ func (g *GitlabProvider) ParseRequest(request *http.Request) error {
return webhook.PayloadError
}

g.secret = request.Header.Get("X-Gitlab-Token")

if comment, ok = event.(*gitlab.MergeCommentEvent); ok {
g.projectId = comment.ProjectID
g.id = comment.MergeRequest.IID
Expand Down
5 changes: 5 additions & 0 deletions webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,18 @@ type Provider interface {
GetID() int
GetProjectID() int
ParseRequest(request *http.Request) error
GetSecret() string
}

type Webhook struct {
provider Provider
Event string
}

func (w Webhook) GetSecret() string {
return w.provider.GetSecret()
}

func (w *Webhook) GetCmd() string {
return w.provider.GetCmd()
}
Expand Down
5 changes: 5 additions & 0 deletions webhook/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type testProvider struct {
id int
projectID int
cmd string
secret string
err error
}

Expand All @@ -39,6 +40,10 @@ func (p *testProvider) GetProjectID() int {
return p.projectID
}

func (p *testProvider) GetSecret() string {
return p.secret
}

func newTestProvider() Provider {
// if p.err != nil {
// return nil
Expand Down
Loading