-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3584b2c
Showing
5 changed files
with
480 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package client | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/xanzy/go-gitlab" | ||
) | ||
|
||
const MR_MERGE_STATUS_MERGEABLE = "mergeable" | ||
|
||
type GitlabConfig struct { | ||
AccessToken string | ||
BaseURL string | ||
} | ||
|
||
type GitlabClient struct { | ||
client *gitlab.Client | ||
me *gitlab.User | ||
config *GitlabConfig | ||
} | ||
|
||
func NewGitlabClient(config GitlabConfig) (*GitlabClient, error) { | ||
git, err := gitlab.NewClient(config.AccessToken, gitlab.WithBaseURL(config.BaseURL)) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to authenticate to GitLab: %w", err) | ||
} | ||
me, _, err := git.Users.CurrentUser() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get current user information from GitLab: %w", err) | ||
} | ||
return &GitlabClient{ | ||
client: git, | ||
me: me, | ||
config: &config, | ||
}, nil | ||
} | ||
|
||
func (g *GitlabClient) GetConfigFileForMR(mr *gitlab.MergeRequest, filePath string) (*[]byte, error) { | ||
opts := &gitlab.GetRawFileOptions{Ref: &mr.SourceBranch} | ||
file, _, err := g.client.RepositoryFiles.GetRawFile(mr.ProjectID, filePath, opts) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to fetch config file: %w", err) | ||
} | ||
return &file, nil | ||
} | ||
|
||
func (g *GitlabClient) ListMrsWithLabel(label string) ([]*gitlab.MergeRequest, error) { | ||
labels := gitlab.LabelOptions{label} | ||
opts := &gitlab.ListMergeRequestsOptions{ | ||
ListOptions: gitlab.ListOptions { | ||
PerPage: 20, | ||
Page: 1, | ||
}, | ||
State: gitlab.Ptr("opened"), | ||
Labels: &labels, | ||
WithMergeStatusRecheck: gitlab.Ptr(true), | ||
} | ||
var allMrs []*gitlab.MergeRequest | ||
|
||
for { | ||
mrs, resp, err := g.client.MergeRequests.ListMergeRequests(opts) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to list MRs: %w", err) | ||
} | ||
allMrs = append(allMrs, mrs...) | ||
if resp.NextPage == 0 { | ||
break | ||
} | ||
opts.Page = resp.NextPage | ||
} | ||
|
||
return allMrs, nil | ||
} | ||
|
||
func (g *GitlabClient) RefreshMr(mr *gitlab.MergeRequest) (*gitlab.MergeRequest, error) { | ||
opts := &gitlab.GetMergeRequestsOptions{} | ||
mr, _, err := g.client.MergeRequests.GetMergeRequest(mr.ProjectID, mr.IID, opts) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get MR: %w", err) | ||
} | ||
|
||
return mr, nil | ||
} | ||
|
||
func (g *GitlabClient) MergeMr(mr *gitlab.MergeRequest) (error) { | ||
opts := &gitlab.AcceptMergeRequestOptions{ShouldRemoveSourceBranch: gitlab.Ptr(true)} | ||
_, _, err := g.client.MergeRequests.AcceptMergeRequest(mr.ProjectID, mr.IID, opts) | ||
if err != nil { | ||
return fmt.Errorf("failed to merge MR: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func IsMergeable(mr *gitlab.MergeRequest) (bool) { | ||
return mr.DetailedMergeStatus == MR_MERGE_STATUS_MERGEABLE | ||
} | ||
|
||
func (g *GitlabClient) Comment(mr *gitlab.MergeRequest, comment string) error { | ||
nopts := &gitlab.ListMergeRequestNotesOptions{} | ||
notes, _, err := g.client.Notes.ListMergeRequestNotes(mr.ProjectID, mr.IID, nopts) | ||
if err != nil { | ||
return fmt.Errorf("failed to get comments on MR: %w", err) | ||
} | ||
|
||
for _, n := range notes { | ||
if n.Author.ID == g.me.ID { | ||
if n.Body == comment { | ||
return nil | ||
} | ||
break | ||
} | ||
} | ||
|
||
opts := &gitlab.CreateMergeRequestNoteOptions{ | ||
Body: gitlab.Ptr(comment), | ||
} | ||
_, _, err = g.client.Notes.CreateMergeRequestNote(mr.ProjectID, mr.IID, opts) | ||
if err != nil { | ||
return fmt.Errorf("failed to add comment to MR: %w", err) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
module github.com/vshn/gitlab-scheduled-merge | ||
|
||
go 1.22.2 | ||
|
||
require ( | ||
github.com/robfig/cron/v3 v3.0.1 | ||
github.com/spf13/cobra v1.8.1 | ||
github.com/xanzy/go-gitlab v0.106.0 | ||
go.uber.org/multierr v1.11.0 | ||
gopkg.in/yaml.v3 v3.0.1 | ||
) | ||
|
||
require ( | ||
github.com/golang/protobuf v1.5.3 // indirect | ||
github.com/google/go-querystring v1.1.0 // indirect | ||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect | ||
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect | ||
github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||
github.com/spf13/pflag v1.0.5 // indirect | ||
golang.org/x/net v0.8.0 // indirect | ||
golang.org/x/oauth2 v0.6.0 // indirect | ||
golang.org/x/time v0.3.0 // indirect | ||
google.golang.org/appengine v1.6.7 // indirect | ||
google.golang.org/protobuf v1.29.1 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= | ||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= | ||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= | ||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= | ||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= | ||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= | ||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= | ||
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= | ||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= | ||
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= | ||
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= | ||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | ||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= | ||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= | ||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= | ||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= | ||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | ||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | ||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||
github.com/xanzy/go-gitlab v0.106.0 h1:EDfD03K74cIlQo2EducfiupVrip+Oj02bq9ofw5F8sA= | ||
github.com/xanzy/go-gitlab v0.106.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= | ||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= | ||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | ||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= | ||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= | ||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= | ||
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= | ||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= | ||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= | ||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= | ||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= | ||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= | ||
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= | ||
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
|
||
"github.com/vshn/gitlab-scheduled-merge/client" | ||
"github.com/vshn/gitlab-scheduled-merge/task" | ||
"github.com/robfig/cron/v3" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var ( | ||
version = "snapshot" | ||
commit = "unknown" | ||
date = "unknown" | ||
) | ||
|
||
type RepositoryConfig struct { | ||
MergeWindows []MergeWindow `yaml:"mergeWindows"` | ||
} | ||
type MergeWindow struct { | ||
Cron string `yaml:"cron"` | ||
MaxDelay string `yaml:"maxDelay"` | ||
} | ||
|
||
func main() { | ||
cmd := &cobra.Command{ | ||
Use: "gitlab-schedule-merge", | ||
} | ||
|
||
gitlabToken := cmd.Flags().StringP("gitlab-token", "t", "", "Token with which to authenticate with GitLab") | ||
gitlabBaseUrl := cmd.Flags().String("gitlab-base-url", "https://gitlab.com/api/v4", "Base URL of GitLab API to use") | ||
scheduledLabel := cmd.Flags().String("scheduled-label", "scheduled", "Name of the label which indicates a MR should be scheduled") | ||
configFilePath := cmd.Flags().String("config-file-path", ".merge-schedule.yml", "Path of the config file in the repo which is used to configure merge windows") | ||
taskSchedule := cmd.Flags().String("task-schedule", "@every 15m", "Cron schedule for how frequently to process merge requests") | ||
|
||
cmd.Run = func(*cobra.Command, []string) { | ||
gitlabConfig := client.GitlabConfig{ | ||
AccessToken: *gitlabToken, | ||
BaseURL: *gitlabBaseUrl, | ||
} | ||
gitlabClient, err := client.NewGitlabClient(gitlabConfig) | ||
if err != nil { | ||
log.Fatalf("GitLab client error: %s", err.Error()) | ||
} | ||
|
||
task, err := setupCronTask(gitlabClient, *taskSchedule, *scheduledLabel, *configFilePath) | ||
if err != nil { | ||
log.Fatalf("Error setting up cron task: %s", err.Error()) | ||
} | ||
|
||
log.Println("Starting task...") | ||
task.Run() | ||
|
||
} | ||
|
||
if err := cmd.Execute(); err != nil { | ||
fmt.Fprintln(os.Stderr, err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func setupCronTask( | ||
client *client.GitlabClient, | ||
crontab string, | ||
scheduledLabel string, | ||
configFilePath string, | ||
) (*cron.Cron, error) { | ||
config := task.TaskConfig{ | ||
MergeRequestScheduledLabel: scheduledLabel, | ||
ConfigFilePath: configFilePath, | ||
} | ||
periodicTask := task.NewTask(client, config) | ||
|
||
c := cron.New() | ||
_, err := c.AddFunc(crontab, func() { | ||
err := periodicTask.Run() | ||
if err == nil { | ||
return | ||
} | ||
log.Printf("error during periodic job: %s\n", err.Error()) | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return c, nil | ||
} |
Oops, something went wrong.