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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Try the bot on our [demo repository](https://gitlab.com/Gasoid/sugar-test) or in
- `!merge` - Merges MR if all repository rules are satisfied
- `!check` - Validates whether the MR meets all rules
- `!update` - Updates the branch from the target branch (e.g., main/master)
- `!rerun` - Re-run pipeline, e.g. `!rerun #123123333` or `!rerun 123123333`, command will run pipeline against the branch of the merge request with variables of provided pipeline (e.g. 123123333)

## Table of Contents

Expand Down
12 changes: 6 additions & 6 deletions bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func start() {
}

e.GET(HealthyEndpoint, healthcheck)
e.POST("/merge-bot/webhook/:provider/", Handler)
e.POST("/mergebot/webhook/:provider/", Handler)

if tlsEnabled {
tmpDir := path.Join(os.TempDir(), "tls", ".cache")
Expand All @@ -82,7 +82,7 @@ func start() {
}

var (
handlerFuncs = map[string]func(*handlers.Request) error{}
handlerFuncs = map[string]func(*handlers.Request, string) error{}
handlerMu sync.RWMutex
)

Expand Down Expand Up @@ -125,7 +125,7 @@ func Handler(c echo.Context) error {
return
}

if err := f(command); err != nil {
if err := f(command, hook.Args); err != nil {
logger.Error("handlerFunc returns err", "provider", providerName, "event", hook.Event, "err", err)
return
}
Expand All @@ -135,11 +135,11 @@ func Handler(c echo.Context) error {
return nil
}

func handle(onEvent string, funcHandler func(*handlers.Request) error) {
func handle(onEvent string, funcHandler func(*handlers.Request, string) error) {
handlerMu.Lock()
defer handlerMu.Unlock()

handlerFuncs[onEvent] = func(command *handlers.Request) error {
return funcHandler(command)
handlerFuncs[onEvent] = func(command *handlers.Request, args string) error {
return funcHandler(command, args)
}
}
8 changes: 4 additions & 4 deletions bot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestHandler(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/merge-bot/webhook/"+tt.provider+"/", strings.NewReader(tt.body))
req := httptest.NewRequest(http.MethodPost, "/mergebot/webhook/"+tt.provider+"/", strings.NewReader(tt.body))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand Down Expand Up @@ -139,7 +139,7 @@ func TestHandlerConcurrency(t *testing.T) {
// Test multiple concurrent requests
for i := 0; i < numRequests; i++ {
go func() {
req := httptest.NewRequest(http.MethodPost, "/merge-bot/webhook/concurrent/", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodPost, "/mergebot/webhook/concurrent/", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand Down Expand Up @@ -175,7 +175,7 @@ func TestHandlerWithDifferentMethods(t *testing.T) {
for _, tt := range tests {
t.Run(tt.method, func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(tt.method, "/merge-bot/webhook/methodtest/", strings.NewReader(`{}`))
req := httptest.NewRequest(tt.method, "/mergebot/webhook/methodtest/", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand All @@ -194,7 +194,7 @@ func TestHandlerWithEmptyBody(t *testing.T) {
defer cleanup()

e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/merge-bot/webhook/emptybody/", bytes.NewReader([]byte{}))
req := httptest.NewRequest(http.MethodPost, "/mergebot/webhook/emptybody/", bytes.NewReader([]byte{}))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand Down
33 changes: 28 additions & 5 deletions commands.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package main

import (
"errors"
"fmt"
"strconv"
"strings"

"github.com/Gasoid/merge-bot/handlers"
"github.com/Gasoid/merge-bot/logger"
Expand All @@ -12,11 +15,12 @@ func init() {
handle("!merge", MergeCmd)
handle("!check", CheckCmd)
handle("!update", UpdateBranchCmd)
handle("!rerun", RerunPipeline)
handle(webhook.OnNewMR, NewMR)
handle(webhook.OnMerge, MergeEvent)
}

func UpdateBranchCmd(command *handlers.Request) error {
func UpdateBranchCmd(command *handlers.Request, args string) error {
if err := command.UpdateFromMaster(); err != nil {
logger.Error("command.UpdateFromMaster failed", "error", err)
return command.LeaveComment("❌ i couldn't update branch from master")
Expand All @@ -25,7 +29,7 @@ func UpdateBranchCmd(command *handlers.Request) error {
return nil
}

func MergeCmd(command *handlers.Request) error {
func MergeCmd(command *handlers.Request, args string) error {
ok, text, err := command.Merge()
if err != nil {
return fmt.Errorf("command.Merge returns err: %w", err)
Expand All @@ -37,7 +41,7 @@ func MergeCmd(command *handlers.Request) error {
return err
}

func CheckCmd(command *handlers.Request) error {
func CheckCmd(command *handlers.Request, args string) error {
ok, text, err := command.IsValid()
if err != nil {
return fmt.Errorf("command.IsValid returns err: %w", err)
Expand All @@ -50,15 +54,15 @@ func CheckCmd(command *handlers.Request) error {
}
}

func NewMR(command *handlers.Request) error {
func NewMR(command *handlers.Request, args string) error {
if err := command.Greetings(); err != nil {
return fmt.Errorf("command.Greetings returns err: %w", err)
}

return nil
}

func MergeEvent(command *handlers.Request) error {
func MergeEvent(command *handlers.Request, args string) error {
if err := command.CreateLabels(); err != nil {
return fmt.Errorf("command.CreateLabels returns err: %w", err)
}
Expand All @@ -72,3 +76,22 @@ func MergeEvent(command *handlers.Request) error {
}
return nil
}

func RerunPipeline(command *handlers.Request, args string) error {
arg := strings.TrimPrefix(args, "#")
pipelineId, err := strconv.Atoi(arg)
if err != nil {
logger.Debug("rerun", "args", args, "arg", arg)
return command.LeaveComment("> [!important]\n> Pipeline ID is invalid or wrong")
}

logger.Debug("rerun", "args", args, "arg", arg)
if err := command.RerunPipeline(pipelineId); err != nil {
if errors.Is(err, handlers.NotFoundError) {
return command.LeaveComment("> [!important]\n> Provided pipeline was not found")
}

return command.LeaveComment("> [!important]\n> Validate your pipeline syntax")
}
return nil
}
4 changes: 2 additions & 2 deletions commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func TestHandle(t *testing.T) {
// Save original state
handlerMu.Lock()
originalHandlers := make(map[string]func(*handlers.Request) error)
originalHandlers := make(map[string]func(*handlers.Request, string) error)
for k, v := range handlerFuncs {
originalHandlers[k] = v
}
Expand All @@ -24,7 +24,7 @@ func TestHandle(t *testing.T) {
handlerMu.Unlock()
}()

testFunc := func(command *handlers.Request) error {
testFunc := func(command *handlers.Request, args string) error {
return nil
}

Expand Down
22 changes: 22 additions & 0 deletions handlers/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ func (g *GitlabProvider) GetMRInfo(projectId, mergeId int, configPath string) (*

info.Labels = g.mr.Labels
info.TargetBranch = g.mr.TargetBranch
info.SourceBranch = g.mr.SourceBranch
info.Author = g.mr.Author.Username

info.ConfigContent, err = g.GetFile(projectId, configPath)
Expand Down Expand Up @@ -367,6 +368,27 @@ func (g GitlabProvider) AssignLabel(projectId, mergeId int, name, color string)
return nil
}

func (g GitlabProvider) RerunPipeline(projectId, pipelineId int, ref string) error {
pipelineVars, _, err := g.client.Pipelines.GetPipelineVariables(projectId, pipelineId)
if err != nil {
return err
}

runVars := make([]*gitlab.PipelineVariableOptions, 0, len(pipelineVars))
for _, v := range pipelineVars {
runVars = append(runVars, &gitlab.PipelineVariableOptions{Key: &v.Key, Value: &v.Value, VariableType: &v.VariableType})
}

if _, _, err := g.client.Pipelines.CreatePipeline(projectId, &gitlab.CreatePipelineOptions{
Variables: &runVars,
Ref: &ref,
}); err != nil {
return err
}

return nil
}

func New() handlers.RequestProvider {
var err error
var p GitlabProvider
Expand Down
4 changes: 2 additions & 2 deletions handlers/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ func MergeMaster(username, password, repoUrl, branchName, master string) error {
return fmt.Errorf("git checkout error: %w, output: %s", err, output)
}

if output, err := git.Merge(workingDir, merge.Commits(master), merge.M("update from master")); err != nil {
if output, err := git.Merge(workingDir, merge.Commits(master), merge.M(fmt.Sprintf("✨ merged %s", master))); err != nil {
logger.Debug("git merge error", "output", output)
if output, err := git.Merge(workingDir, merge.NoFf, merge.Commits(master), merge.M("update from master")); err != nil {
if output, err := git.Merge(workingDir, merge.NoFf, merge.Commits(master), merge.M(fmt.Sprintf("✨ merged %s", master))); err != nil {
logger.Debug("git merge --noff error", "output", output)
return fmt.Errorf("git merge --noff error: %w, output: %s", err, output)
}
Expand Down
3 changes: 3 additions & 0 deletions handlers/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var (
StatusError = &Error{"Is it opened?"}
ValidError = &Error{"Your request can't be merged, because either it has conflicts or state is not opened"}
RepoSizeError = &Error{"Repository size is greater than allowed size"}
NotFoundError = &Error{"Resource is not found"}
)

type Error struct {
Expand All @@ -36,6 +37,7 @@ type MrInfo struct {
Id int
Labels []string
TargetBranch string
SourceBranch string
Approvals map[string]struct{}
Author string
FailedPipelines int
Expand Down Expand Up @@ -67,6 +69,7 @@ type MergeRequest interface {
type Project interface {
CreateLabel(projectId int, name, color string) error
GetVar(projectId int, varName string) (string, error)
RerunPipeline(projectId, pipelineId int, ref string) error
}

type RequestProvider interface {
Expand Down
5 changes: 5 additions & 0 deletions handlers/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ func (r Request) CreateLabels() error {
return nil
}

func (r Request) RerunPipeline(pipelineId int) error {
logger.Debug("rerun", "pipelineId", pipelineId)
return r.provider.RerunPipeline(r.info.ProjectId, pipelineId, r.info.SourceBranch)
}

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

Expand Down
4 changes: 4 additions & 0 deletions handlers/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func (p *testProvider) CreateLabel(projectId int, name, color string) error {
return p.err
}

func (p *testProvider) RerunPipeline(projectId, pipelineId int, ref string) error {
return p.err
}

func Test_Merge(t *testing.T) {
type args struct {
pr *Request
Expand Down
6 changes: 3 additions & 3 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestIntegrationWebhookFlow(t *testing.T) {

// Save original handlers
handlerMu.Lock()
originalHandlers := make(map[string]func(*handlers.Request) error)
originalHandlers := make(map[string]func(*handlers.Request, string) error)
for k, v := range handlerFuncs {
originalHandlers[k] = v
}
Expand All @@ -72,12 +72,12 @@ func TestIntegrationWebhookFlow(t *testing.T) {
}()

// Register a test handler
handle("!merge", func(command *handlers.Request) error {
handle("!merge", func(command *handlers.Request, args string) error {
return nil
})

e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/merge-bot/webhook/integration/", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodPost, "/mergebot/webhook/integration/", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Expand Down
17 changes: 14 additions & 3 deletions webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package webhook

import (
"net/http"
"strings"
"sync"
)

const (
OnNewMR = "\anewMREvent"
OnMerge = "\amergeEvent"
OnNewMR = "\anewMREvent"
OnMerge = "\amergeEvent"
spaceSymbol = " "
)

var (
Expand Down Expand Up @@ -44,6 +46,7 @@ type Provider interface {
type Webhook struct {
provider Provider
Event string
Args string
}

func (w Webhook) GetSecret() string {
Expand Down Expand Up @@ -71,7 +74,15 @@ func (w *Webhook) ParseRequest(request *http.Request) error {
return err
}

w.Event = w.provider.GetCmd()
if w.provider.GetCmd() != "" {
result := strings.SplitN(w.provider.GetCmd(), spaceSymbol, 2)
if len(result) > 0 {
w.Event = result[0]
}
if len(result) > 1 {
w.Args = strings.TrimSpace(result[1])
}
}

return nil
}
Expand Down
Loading