├── .gitignore ├── MRS └── .gitkeep ├── README.md ├── app.rb ├── assets ├── .gitkeep ├── add_user.png ├── look.png ├── private_token.png └── webhook.png ├── ci-bot.service ├── config.json.example └── lib └── gitlabbot ├── mergebot.rb └── server.rb /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | MRS/* 3 | !MRS/.gitkeep 4 | -------------------------------------------------------------------------------- /MRS/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjanuschka/GitlabBot/ca1628244ee122ad243954063ee2cc4a152d5596/MRS/.gitkeep -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitlabBot 2 | 3 | A Bot that auto-merges merge-requests if enough "LGTMS" have been received (by trusted users) 4 | resets automatically on MR changes or code pushes. 5 | 6 | 7 | ## Setup 8 | * Create a bot user in gitlab 9 | ![add_user](https://raw.githubusercontent.com/hjanuschka/GitlabBot/master/assets/add_user.png) 10 | * Get the Private token of the newly created user 11 | * Impersonate as the bot user, and go to: http://gitlab.krone.at/profile/account, copy the token 12 | ![add_user](https://raw.githubusercontent.com/hjanuschka/GitlabBot/master/assets/private_token.png) 13 | * Install the Bot, on a server that can be reached from your gitlab installation (the bot also needs to be able to reach the gitlab instance) 14 | 15 | 16 | ``` 17 | git clone https://github.com/hjanuschka/GitlabBot.git 18 | cp config.json.example config.json 19 | ``` 20 | 21 | 22 | * Customize the `config.json` 23 | * Customize the `ci-bot.service` to fit your paths 24 | * install the systemd service 25 | 26 | 27 | ``` 28 | cp ci-bot.service /etc/systemd/system/ 29 | systemctl enable ci-bot.service 30 | systemctl start ci-bot.service 31 | ``` 32 | 33 | 34 | * Add the bot user to the desired gitlab project (on Project -> Members (master role, as it needs to be able to merge)) 35 | * Add a webhook to the project (on Project -> Webhooks) 36 | ![add_user](https://raw.githubusercontent.com/hjanuschka/GitlabBot/master/assets/webhook.png) 37 | 38 | 39 | If everthing went well - create a MR and you should see the bot posting to it "Init/Reset MR" 40 | 41 | 42 | ![add_user](https://raw.githubusercontent.com/hjanuschka/GitlabBot/master/assets/look.png) 43 | 44 | 45 | ## Config Options 46 | 47 | * `endpoint`- the URL to your gitlab api (E.g: http://gitlab/api/v3) 48 | * `token` - the private token of the bot user - used to access the api 49 | * `lgtmUsers` - array of users wich are allowed to post LGTM (e.g.: `[ "user1", "user2" ]` 50 | * `lgtmRequired` - number of required LGTM's before bot will hit the merge button. (e.g. `1`) 51 | * `port` - port the bot-server will isten (e.g: `8080`) 52 | * `botUsername` - the username of the bot (e.g: `ci-bot`) 53 | 54 | 55 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require './lib/gitlabbot/mergebot.rb' 3 | require './lib/gitlabbot/server.rb' 4 | 5 | # Read config 6 | if File.exist?('config.json') 7 | config_file = File.read('config.json') 8 | config = JSON.parse(config_file) 9 | server = GitlabBot::Server.new(config) 10 | server.run! 11 | 12 | else 13 | raise 'Config file not found' 14 | end 15 | trap 'INT' do 16 | server.stop 17 | end 18 | -------------------------------------------------------------------------------- /assets/.gitkeep: -------------------------------------------------------------------------------- 1 | keep 2 | -------------------------------------------------------------------------------- /assets/add_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjanuschka/GitlabBot/ca1628244ee122ad243954063ee2cc4a152d5596/assets/add_user.png -------------------------------------------------------------------------------- /assets/look.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjanuschka/GitlabBot/ca1628244ee122ad243954063ee2cc4a152d5596/assets/look.png -------------------------------------------------------------------------------- /assets/private_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjanuschka/GitlabBot/ca1628244ee122ad243954063ee2cc4a152d5596/assets/private_token.png -------------------------------------------------------------------------------- /assets/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjanuschka/GitlabBot/ca1628244ee122ad243954063ee2cc4a152d5596/assets/webhook.png -------------------------------------------------------------------------------- /ci-bot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=CI Bot 3 | After=syslog.target 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | User=root 9 | Group=root 10 | WorkingDirectory=/ci-bot/ 11 | ExecStart=/usr/local/bin/ruby /ci-bot/app.rb 8080 12 | 13 | # Give a reasonable amount of time for the server to start up/shut down 14 | TimeoutSec=300 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "endpoint": "http://gitlab/api/v3", 3 | "token": "XXXX", 4 | "lgtmUsers": [ "hjanuschka" ], 5 | "lgtmRequired": 1, 6 | "port": 8080, 7 | "botUsername": "deploy-user" 8 | } 9 | -------------------------------------------------------------------------------- /lib/gitlabbot/mergebot.rb: -------------------------------------------------------------------------------- 1 | module GitlabBot 2 | class MergeBot 3 | attr_accessor :config 4 | def initialize(opts = {}) 5 | @config = opts 6 | end 7 | 8 | def handlePayload(jso) 9 | if jso['object_kind'] == 'merge_request' 10 | 11 | if jso['user']['username'] != @config['botUsername'] 12 | 13 | # Merge request hook 14 | # Check if we already seen this MR - if so remove it - as MR has been updated 15 | mr_config = "MRS/#{jso['object_attributes']['id']}.json" 16 | project_id = jso['object_attributes']['target_project_id'] 17 | mr_id = jso['object_attributes']['id'] 18 | File.unlink(mr_config) if File.exist?(mr_config) 19 | 20 | file_jso = { 21 | 'lgtm' => 0, 22 | 'lgtmers' => [] 23 | } 24 | puts 'RESET INIT MR' 25 | File.write(mr_config, file_jso.to_json) 26 | 27 | RestClient::Request.execute(method: :post, url: "#{@config['endpoint']}/projects/#{project_id}/merge_requests/#{mr_id}/notes", payload: { body: 'LGTM init/reset' }, headers: { 'PRIVATE-TOKEN' => @config['token'] }) 28 | end 29 | end 30 | if jso['object_kind'] == 'note' && jso['object_attributes']['noteable_type'] == 'MergeRequest' 31 | # Merge request hook 32 | # 33 | 34 | puts 'GOT MR NOTE' 35 | mr_config = "MRS/#{jso['merge_request']['id']}.json" 36 | 37 | # check if note contains LGTM 38 | note = jso['object_attributes']['note'] 39 | 40 | if File.exist?(mr_config) 41 | file = File.read(mr_config) 42 | file_jso = JSON.parse(file) 43 | else 44 | file_jso = { 45 | 'lgtm' => 0, 46 | 'lgtmers' => [] 47 | } 48 | end 49 | 50 | puts file_jso.inspect 51 | 52 | if note =~ /LGTM/ 53 | if @config["lgtmUsers"].include? jso['user']['username'] 54 | unless file_jso['lgtmers'].include?("@#{jso['user']['username']}") 55 | # user is a LGTM user & and has not already lgtm'd 56 | puts 'IN IF' 57 | file_jso['lgtmers'].push("@#{jso['user']['username']}") 58 | file_jso['lgtm'] += 1 59 | 60 | puts "Updated MR with lgtm #{file_jso.inspect}" 61 | 62 | File.write(mr_config, file_jso.to_json) 63 | if file_jso['lgtm'] >= @config["lgtmRequired"] 64 | # POST COMMENT VIA API 65 | # call MERGE via API 66 | project_id = jso['merge_request']['target_project_id'] 67 | mr_id = jso['merge_request']['id'] 68 | 69 | approvers = file_jso['lgtmers'].join(' ') 70 | 71 | # Comment on MR 72 | 73 | RestClient::Request.execute(method: :post, url: "#{@config['endpoint']}/projects/#{project_id}/merge_requests/#{mr_id}/notes", payload: { body: "I will merge this as #{approvers} approved it" }, headers: { 'PRIVATE-TOKEN' => @config['token'] }) 74 | RestClient::Request.execute(method: :put, url: "#{@config['endpoint']}/projects/#{project_id}/merge_request/#{mr_id}/merge?merge_when_build_succeeds=true", headers: { 'PRIVATE-TOKEN' => @config['token'] }) 75 | 76 | end 77 | end 78 | end 79 | end 80 | 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/gitlabbot/server.rb: -------------------------------------------------------------------------------- 1 | module GitlabBot 2 | require 'webrick' 3 | require 'json' 4 | require 'rest-client' 5 | 6 | class Server 7 | attr_accessor :config, :mrbot 8 | def initialize(conf = {}) 9 | @config = conf 10 | @mrbot = MergeBot.new(conf) 11 | end 12 | 13 | def run! 14 | server = WEBrick::HTTPServer.new(Port: @config['port']) 15 | server.mount_proc '/' do |req, _res| 16 | handleRequest req 17 | end 18 | server.start 19 | end 20 | 21 | def stop 22 | server.shutdown 23 | end 24 | 25 | def handleRequest(req) 26 | jso_body = JSON.parse(req.body) 27 | @mrbot.handlePayload(jso_body) 28 | end 29 | end 30 | end 31 | --------------------------------------------------------------------------------