fastlane-plugin-rooster 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +73 -7
- data/lib/fastlane/plugin/rooster/actions/rooster_merge_request_action.rb +148 -0
- data/lib/fastlane/plugin/rooster/helper/api_request.rb +42 -0
- data/lib/fastlane/plugin/rooster/helper/file_loader.rb +45 -0
- data/lib/fastlane/plugin/rooster/helper/gitlab_api_client.rb +72 -0
- data/lib/fastlane/plugin/rooster/helper/gitlab_merge_request_transformer.rb +60 -0
- data/lib/fastlane/plugin/rooster/helper/slack_file_client.rb +39 -0
- data/lib/fastlane/plugin/rooster/helper/slack_message_client.rb +99 -0
- data/lib/fastlane/plugin/rooster/helper/time_format.rb +66 -0
- data/lib/fastlane/plugin/rooster/version.rb +1 -1
- metadata +11 -5
- data/lib/fastlane/plugin/rooster/actions/rooster_action.rb +0 -420
- data/lib/fastlane/plugin/rooster/helper/rooster_helper.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2bf10f44077348260a73b870ddde68cbfe4496656470f7d49a1b96d49a6676d
|
4
|
+
data.tar.gz: 055ad6a5a639979abc2d9ad512f0d99c32107e25be2ae156f410efafe571ba1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3522e7fae717a366ac58ec7e167987ea1f6c1a674768c8a6a38e686a9e250854bc6f32cbbedaef1feed8f040c41736380f2cc50449a81240cec6135fa42203ef
|
7
|
+
data.tar.gz: e859bcf76d0b80839c6ad84877f9c28f092ee1f3165fab9a1cb2b684d5ef8c4e25686f3f236038198701230e939067db00392a4db08513b44eb8fc8c7e83c8b0
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# rooster plugin
|
2
2
|
|
3
3
|
[](https://rubygems.org/gems/fastlane-plugin-rooster)
|
4
|
+
[](https://github.com/tbetmen/fastlane-plugin-rooster/blob/main/LICENSE)
|
5
|
+
[](https://rubygems.org/gems/fastlane-plugin-rooster)
|
4
6
|
|
5
7
|
## Getting Started
|
6
8
|
|
@@ -10,17 +12,81 @@ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To
|
|
10
12
|
fastlane add_plugin rooster
|
11
13
|
```
|
12
14
|
|
13
|
-
## About
|
15
|
+
## About Rooster
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
**Note to author:** Add a more detailed description about this plugin here. If your plugin contains multiple actions, make sure to mention them here.
|
17
|
+
Rooster is fastlane action to send Gitlab merge request to Slack using webhook, for now only has one action `rooster_merge_request`.
|
18
18
|
|
19
19
|
## Example
|
20
20
|
|
21
|
-
Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use
|
21
|
+
Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use `rooster_merge_request` action. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test_env`.
|
22
|
+
|
23
|
+
There are also some example files that used in `rooster_merge_request` action:
|
24
|
+
1. Sample uses `.env` in [example `.env.example`](example/.env.example) for setting all action parameters or just private variable like Gitlab Api Token.
|
25
|
+
2. [example `slack_message_format.json`](example/slack_message_format.json) used for slack message format to replace default value given in action.
|
26
|
+
3. [example `slack_users.csv`](example/slack_users.csv) slack users and gitlab users mapping format.
|
27
|
+
|
28
|
+
Slack message preview has opened merge request
|
29
|
+
|
30
|
+

|
31
|
+
|
32
|
+
Slack message preview merge request empty
|
33
|
+
|
34
|
+

|
35
|
+
|
36
|
+
## Parameters
|
37
|
+
|
38
|
+
The action parameters `gitlab_token` and others can also be omitted when their values are [set as environment variables](https://docs.fastlane.tools/advanced/#environment-variables).
|
22
39
|
|
23
|
-
|
40
|
+
Here is the list of all existing parameters for `rooster_merge_request` action:
|
41
|
+
|
42
|
+
| Key | Env Var | Default | Optional | Description |
|
43
|
+
|-----|-----------------------|-------------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------|
|
44
|
+
| `gitlab_token` | `ROOSTER_GITLAB_ACCESS_TOKEN` | - | false | API Token for Gitlab [find out here](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) |
|
45
|
+
| `gitlab_project_id` | `ROOSTER_GITLAB_PROJECT_ID` | - | false | Gitlab project id, you can find in Project Overview menu located below project name. |
|
46
|
+
| `slack_webhook_url` | `ROOSTER_SLACK_WEBHOOK_URL` | - | false | Slack webhook url, check this link for more detail [Slack Webhooks](https://api.slack.com/messaging/webhooks) |
|
47
|
+
| `gitlab_milestones_path` | `ROOSTER_GITLAB_MILESTONES_PATH` | - | true | Gitlab group or project milestones with given format either `groups/:group_id` or `projects/:project_id`. You can find `group id` similar with project id in Group Overview. [Gitlab Milestones](https://docs.gitlab.com/ee/user/project/milestones/) |
|
48
|
+
| `gitlab_merge_request_total` | `ROOSTER_GITLAB_MERGE_REQUEST_TOTAL` | 10 | true | Maximum merge request when fetching data from gitlab, uses in query param `per_page` |
|
49
|
+
| `slack_users_file` | `ROOSTER_SLACK_USERS_FILE` | - | true | Comma separate file that contains mapping user of gitlab and slack using id |
|
50
|
+
| `slack_message_format_file` | `ROOSTER_SLACK_MESSAGE_FORMAT_FILE` | Check in this [file](lib/fastlane/plugin/rooster/helper/slack_file_client.rb) | true | Slack message format in json format contains `text`, `header`, `mr_item`, `footer`, and `empty_mr_text`. |
|
51
|
+
|
52
|
+
## Replace word in `slack_message_format`
|
53
|
+
|
54
|
+
In slack message format json has word replacement, check this table to see all possibility:
|
55
|
+
|
56
|
+
| Key | Availability | Description |
|
57
|
+
|-----|--------------|-------------------------------------------------|
|
58
|
+
| `MR_TOTAL` | header | Total merge request to be shown in slack |
|
59
|
+
| `MR_MILESTONE` | header, empty_mr_text | Current active milestone |
|
60
|
+
| `MR_TITLE` | mr_item | Title of merge request |
|
61
|
+
| `MR_TIME` | mr_item | Time relative to current date like `3 days ago` |
|
62
|
+
| `MR_ASSIGNEE_SINGLE` | mr_item | Merge request assignee just one user |
|
63
|
+
| `MR_ASSIGNEES` | mr_item | All assignee users in merge request |
|
64
|
+
| `MR_REVIEWERS` | mr_item | All reviewer users in merge request |
|
65
|
+
| `MR_LABELS` | mr_item | Merge request labels |
|
66
|
+
| `MR_LINK` | mr_item | Merge request link |
|
67
|
+
|
68
|
+
### Example usage
|
69
|
+
|
70
|
+
before
|
71
|
+
```json
|
72
|
+
{
|
73
|
+
"text": "Hello, its beautiful day!",
|
74
|
+
"header": "Hi there, Just wanted to let you know that we have MR_TOTAL merge requests that need your attention.",
|
75
|
+
"mr_item": "<MR_LINK|MR_TITLE> MR_ASSIGNEE_SINGLE",
|
76
|
+
"footer": "Thank you",
|
77
|
+
"empty_mr_text": "Congratulation there is no merge request anymore, keep the good works"
|
78
|
+
}
|
79
|
+
```
|
80
|
+
after
|
81
|
+
```json
|
82
|
+
{
|
83
|
+
"text": "Hello, its beautiful day!",
|
84
|
+
"header": "Hi there, Just wanted to let you know that we have 10 merge requests that need your attention.",
|
85
|
+
"mr_item": "<https://gitlab.com/nnn/merge_requests/1|feat: Add new feature mobile prepaid> @Bruce Wayne",
|
86
|
+
"footer": "Thank you",
|
87
|
+
"empty_mr_text": "Congratulation there is no merge request anymore, keep the good works"
|
88
|
+
}
|
89
|
+
```
|
24
90
|
|
25
91
|
## Run tests for this plugin
|
26
92
|
|
@@ -37,7 +103,7 @@ rubocop -a
|
|
37
103
|
|
38
104
|
## Issues and Feedback
|
39
105
|
|
40
|
-
For any other issues and feedback about this plugin, please submit
|
106
|
+
For any other issues and feedback about this plugin, please submit new [Github Issue](https://github.com/tbetmen/fastlane-plugin-rooster/issues).
|
41
107
|
|
42
108
|
## Troubleshooting
|
43
109
|
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'fastlane/action'
|
2
|
+
require_relative '../helper/gitlab_api_client'
|
3
|
+
require_relative '../helper/gitlab_merge_request_transformer'
|
4
|
+
require_relative '../helper/slack_file_client'
|
5
|
+
require_relative '../helper/slack_message_client'
|
6
|
+
|
7
|
+
module Fastlane
|
8
|
+
module Actions
|
9
|
+
class RoosterMergeRequestAction < Action
|
10
|
+
def self.run(params)
|
11
|
+
# get params or envs
|
12
|
+
users_file = params[:slack_users_file] || ENV['ROOSTER_SLACK_USERS_FILE']
|
13
|
+
gitlab_project_id = params[:gitlab_project_id] || ENV['ROOSTER_GITLAB_PROJECT_ID']
|
14
|
+
slack_webhook_url = params[:slack_webhook_url] || ENV['ROOSTER_SLACK_WEBHOOK_URL']
|
15
|
+
slack_message_format_file = params[:slack_message_format_file] || ENV['ROOSTER_SLACK_MESSAGE_FORMAT_FILE']
|
16
|
+
gitlab_mr_total = params[:gitlab_merge_request_total] || ENV['ROOSTER_GITLAB_MERGE_REQUEST_TOTAL']
|
17
|
+
gitlab_milestones_path = params[:gitlab_milestones_path] || ENV['ROOSTER_GITLAB_MILESTONES_PATH']
|
18
|
+
gitlab_token = params[:gitlab_token] || ENV['ROOSTER_GITLAB_ACCESS_TOKEN']
|
19
|
+
|
20
|
+
# load slack files
|
21
|
+
slack_file_client = Helper::SlackFileClient.new
|
22
|
+
slack_message_format = slack_file_client.message_format(slack_message_format_file)
|
23
|
+
slack_users = slack_file_client.users(users_file)
|
24
|
+
|
25
|
+
# get gitlab merge request
|
26
|
+
gitlab_api = Helper::GitlabApiClient.new(gitlab_token)
|
27
|
+
current_milestone = gitlab_api.current_milestone(gitlab_milestones_path)
|
28
|
+
mr_resp_json = gitlab_api.merge_requests(gitlab_project_id, gitlab_mr_total, current_milestone)
|
29
|
+
|
30
|
+
# transform merge request
|
31
|
+
gitlab_mr_transformer = Helper::GitlabMergeRequestTransformer.new(slack_users, mr_resp_json)
|
32
|
+
merge_request_formatted = gitlab_mr_transformer.transform
|
33
|
+
|
34
|
+
# post merge request into slack
|
35
|
+
slack_message_client = Helper::SlackMessageClient.new(
|
36
|
+
slack_users,
|
37
|
+
slack_message_format,
|
38
|
+
merge_request_formatted,
|
39
|
+
current_milestone
|
40
|
+
)
|
41
|
+
slack_message_client.post_message(
|
42
|
+
slack_webhook_url,
|
43
|
+
slack_message_client.payload
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
#####################################################
|
48
|
+
# @!group Documentation
|
49
|
+
#####################################################
|
50
|
+
|
51
|
+
def self.available_options
|
52
|
+
[
|
53
|
+
FastlaneCore::ConfigItem.new(
|
54
|
+
key: :gitlab_token,
|
55
|
+
env_name: 'ROOSTER_GITLAB_ACCESS_TOKEN',
|
56
|
+
description: 'Gitlab access token',
|
57
|
+
optional: false,
|
58
|
+
verify_block: proc do |value|
|
59
|
+
UI.user_error!('No gitlab access token given') unless value && !value.empty?
|
60
|
+
end
|
61
|
+
),
|
62
|
+
FastlaneCore::ConfigItem.new(
|
63
|
+
key: :gitlab_project_id,
|
64
|
+
env_name: 'ROOSTER_GITLAB_PROJECT_ID',
|
65
|
+
description: 'Gitlab project id',
|
66
|
+
optional: false,
|
67
|
+
verify_block: proc do |value|
|
68
|
+
UI.user_error!('No gitlab project id given') unless value && !value.empty?
|
69
|
+
end
|
70
|
+
),
|
71
|
+
FastlaneCore::ConfigItem.new(
|
72
|
+
key: :slack_webhook_url,
|
73
|
+
env_name: 'ROOSTER_SLACK_WEBHOOK_URL',
|
74
|
+
description: 'Slack webhook url',
|
75
|
+
optional: false,
|
76
|
+
verify_block: proc do |value|
|
77
|
+
UI.user_error!('No slack webhook url given') unless value && !value.empty?
|
78
|
+
end
|
79
|
+
),
|
80
|
+
FastlaneCore::ConfigItem.new(
|
81
|
+
key: :gitlab_milestones_path,
|
82
|
+
env_name: 'ROOSTER_GITLAB_MILESTONES_PATH',
|
83
|
+
description: 'Gitlab group or project milestones with given format either `groups/:group_id` or `projects/:project_id`',
|
84
|
+
optional: true
|
85
|
+
),
|
86
|
+
FastlaneCore::ConfigItem.new(
|
87
|
+
key: :gitlab_merge_request_total,
|
88
|
+
env_name: 'ROOSTER_GITLAB_MERGE_REQUEST_TOTAL',
|
89
|
+
default_value: 10,
|
90
|
+
type: Integer,
|
91
|
+
description: 'Maximum merge request when fetching data from gitlab, uses in query param `per_page`',
|
92
|
+
optional: true
|
93
|
+
),
|
94
|
+
FastlaneCore::ConfigItem.new(
|
95
|
+
key: :slack_users_file,
|
96
|
+
env_name: 'ROOSTER_SLACK_USERS_FILE',
|
97
|
+
description: 'Comma separate file that contains mapping user of gitlab and slack using id',
|
98
|
+
optional: true
|
99
|
+
),
|
100
|
+
FastlaneCore::ConfigItem.new(
|
101
|
+
key: :slack_message_format_file,
|
102
|
+
env_name: 'ROOSTER_SLACK_MESSAGE_FORMAT_FILE',
|
103
|
+
default_value: Helper::SlackFileClient::SLACK_MESSAGE_FORMAT_DEFAULT,
|
104
|
+
description: 'Slack message format in json format contains `text`, `header`, `mr_item`, `footer`, and `empty_mr_text`'\
|
105
|
+
'| In `header` you can use use `MR_TOTAL` and `MR_MILESTONE`'\
|
106
|
+
'| In `mr_item` you can use use `MR_TITLE`, `MR_TIME`, `MR_ASSIGNEE_SINGLE`, `MR_ASSIGNEES`, `MR_REVIEWERS` `MR_LABELS`, and `MR_LINK`'\
|
107
|
+
'| In `empty_mr_text` you can use `MR_MILESTONE`',
|
108
|
+
optional: true
|
109
|
+
)
|
110
|
+
]
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.example_code
|
114
|
+
[
|
115
|
+
'rooster_merge_request',
|
116
|
+
'rooster_merge_request(
|
117
|
+
gitlab_token: "xyx",
|
118
|
+
gitlab_project_id: "123456",
|
119
|
+
gitlab_milestones_path: "projects/12345678"
|
120
|
+
gitlab_merge_request_total: 20,
|
121
|
+
slack_users_file: "/User/documents/slack_users.csv",
|
122
|
+
slack_webhook_url: "https://hooks.slack.com/000/000",
|
123
|
+
slack_message_format_file: "/User/documents/slack_message_format.json",
|
124
|
+
)'
|
125
|
+
]
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.is_supported?(_)
|
129
|
+
true
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.author
|
133
|
+
'tbetmen (muhammadmmunir24@gmail.com)'
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.description
|
137
|
+
'Send gitlab merge requests reminder to slack.'
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.details
|
141
|
+
[
|
142
|
+
'This action is reminder system of gitlab merge request, ',
|
143
|
+
'fetching merge requests using Gitlab REST API and send to Slack using webhook url.'
|
144
|
+
].join('')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Fastlane
|
7
|
+
module Helper
|
8
|
+
module ApiRequest
|
9
|
+
def api_get_response_json(url, header)
|
10
|
+
uri = URI(url)
|
11
|
+
|
12
|
+
UI.message "Get Request from url: #{url}"
|
13
|
+
response = Net::HTTP.get_response(uri, header)
|
14
|
+
|
15
|
+
if response.is_a?(Net::HTTPSuccess)
|
16
|
+
UI.success "Successfully get request with code: #{response.code}"
|
17
|
+
JSON.parse(response.body)
|
18
|
+
else
|
19
|
+
UI.error "Failed to get request code: #{response.code} message: #{response.message}"
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def api_post(url, body, header)
|
25
|
+
uri = URI(url)
|
26
|
+
request = Net::HTTP::Post.new(uri, header)
|
27
|
+
request.body = JSON(body)
|
28
|
+
|
29
|
+
UI.message "Post Request to url: #{url}"
|
30
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |https|
|
31
|
+
https.request(request)
|
32
|
+
end
|
33
|
+
|
34
|
+
if response.is_a?(Net::HTTPSuccess)
|
35
|
+
UI.success "Successfully post request to with code: #{response.code}"
|
36
|
+
else
|
37
|
+
UI.error "Failed to post request code: #{response&.code} message: #{response&.message}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
2
|
+
require 'csv'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
module Helper
|
7
|
+
module FileLoader
|
8
|
+
def valid_extension(filepath, ext)
|
9
|
+
File.file?(filepath.to_s) && [ext.to_s].include?(File.extname(filepath.to_s))
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_json(filepath, default_value)
|
13
|
+
UI.message "Load json filepath #{filepath}"
|
14
|
+
|
15
|
+
if valid_extension(filepath, '.json')
|
16
|
+
UI.success 'Successfully loaded json file'
|
17
|
+
JSON.parse(File.read(filepath))
|
18
|
+
else
|
19
|
+
UI.success 'Successfully return default value because given file is missing or invalid json format'
|
20
|
+
default_value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_csv_to_hash(filepath, header_id, header_one, header_two, header_one_key, header_two_key)
|
25
|
+
UI.message "Load csv filepath #{filepath}"
|
26
|
+
|
27
|
+
unless valid_extension(filepath, '.csv')
|
28
|
+
UI.message 'The given file is missing or invalid csv format'
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
|
32
|
+
hash_value = {}
|
33
|
+
CSV.foreach(filepath, headers: true) do |user|
|
34
|
+
hash_value[user[header_id.to_s]] = {
|
35
|
+
header_one_key.to_s => user[header_one.to_s],
|
36
|
+
header_two_key.to_s => user[header_two.to_s]
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
UI.success 'Successfully loaded csv file into hash'
|
41
|
+
hash_value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'fastlane_core/ui/ui'
|
3
|
+
require_relative './api_request'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
module Helper
|
7
|
+
class GitlabApiClient
|
8
|
+
include Helper::ApiRequest
|
9
|
+
|
10
|
+
BASE_URL = 'https://gitlab.com/api/v4/'.freeze
|
11
|
+
|
12
|
+
def initialize(private_token)
|
13
|
+
@header = { 'PRIVATE-TOKEN': private_token }
|
14
|
+
end
|
15
|
+
|
16
|
+
#####################################################
|
17
|
+
# @!group MILESTONE
|
18
|
+
#####################################################
|
19
|
+
def current_milestone(milestones_path)
|
20
|
+
UI.message 'Fetching current milestone'
|
21
|
+
milestones_url = milestones_url(milestones_path)
|
22
|
+
|
23
|
+
if milestones_url.nil? || milestones_url.empty?
|
24
|
+
UI.success 'Return empty milestone because invalid milestone url'
|
25
|
+
''
|
26
|
+
else
|
27
|
+
milestones_json = api_get_response_json(milestones_url, @header)
|
28
|
+
value = milestone_title(milestones_json)
|
29
|
+
UI.success "Successfully fetched current milestone #{value}" unless value.empty?
|
30
|
+
value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# `path` available option ['groups/:group_id', 'projects/:project_id']
|
35
|
+
def milestones_url(path)
|
36
|
+
path.nil? || path.empty? ? '' : "#{BASE_URL}#{path}/milestones?state=active"
|
37
|
+
end
|
38
|
+
|
39
|
+
def milestone_title(milestone_resp_json)
|
40
|
+
return '' if milestone_resp_json.nil?
|
41
|
+
|
42
|
+
current_milestone = milestone_resp_json.find do |value|
|
43
|
+
due_date = DateTime.strptime(value['due_date'], '%Y-%m-%d')
|
44
|
+
DateTime.now.to_time.to_i <= due_date.to_time.to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
current_milestone.nil? ? '' : current_milestone['title']
|
48
|
+
end
|
49
|
+
|
50
|
+
#####################################################
|
51
|
+
# @!group MERGE REQUEST
|
52
|
+
#####################################################
|
53
|
+
def merge_requests(project_id, per_page = 10, current_milestone = '')
|
54
|
+
UI.message 'Fetching merge requests'
|
55
|
+
url = merge_request_url(project_id, per_page, current_milestone)
|
56
|
+
resp = api_get_response_json(url, @header)
|
57
|
+
|
58
|
+
if resp.nil?
|
59
|
+
UI.user_error! 'Failed to fetch gitlab merge request'
|
60
|
+
else
|
61
|
+
UI.success "Successfully fetched #{resp.length} merge requests"
|
62
|
+
resp
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def merge_request_url(project_id, per_page, current_milestone)
|
67
|
+
url = "#{BASE_URL}projects/#{project_id}/merge_requests?state=opened&order_by=created_at&sort=asc&per_page=#{per_page}"
|
68
|
+
current_milestone.nil? || current_milestone.empty? ? url : url + "&milestone=#{current_milestone}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module Helper
|
5
|
+
# Transform merge request json to new format by adding slack users id if `slack_users` parameter not nil or empty.
|
6
|
+
# And ignoring merge request when title has prefix `draft` and `wip`.
|
7
|
+
class GitlabMergeRequestTransformer
|
8
|
+
def initialize(slack_users, gitlab_mr_json)
|
9
|
+
@slack_users = slack_users
|
10
|
+
@gitlab_mr_json = gitlab_mr_json
|
11
|
+
end
|
12
|
+
|
13
|
+
def transform
|
14
|
+
UI.message 'Transform gitlab merge request into new format'
|
15
|
+
|
16
|
+
mr_filtered = remove_draft_merge_request(@gitlab_mr_json)
|
17
|
+
mr_transformed = compose_gitlab_merge_request(mr_filtered)
|
18
|
+
UI.success 'Successfully transformed into new format'
|
19
|
+
|
20
|
+
mr_transformed
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_slack_user_id(gitlab_user_id, gitlab_username)
|
24
|
+
if !@slack_users.nil? && @slack_users.key?(gitlab_user_id.to_s)
|
25
|
+
"<@#{@slack_users[gitlab_user_id.to_s]['slack_id']}>"
|
26
|
+
else
|
27
|
+
"@#{gitlab_username}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_slack_user_ids(gitlab_mr_json, key)
|
32
|
+
gitlab_mr_json[key.to_s]&.map do |v|
|
33
|
+
get_slack_user_id(v['id'], v['username'])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove_draft_merge_request(gitlab_mr_json)
|
38
|
+
gitlab_mr_json&.select do |value|
|
39
|
+
title = value['title']
|
40
|
+
!(title.downcase.start_with? 'draft') && !(title.downcase.start_with? 'wip')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def compose_gitlab_merge_request(gitlab_mr_json)
|
45
|
+
gitlab_mr_json&.map do |value|
|
46
|
+
{
|
47
|
+
title: value['title'],
|
48
|
+
created_at: value['created_at'],
|
49
|
+
link: value['web_url'],
|
50
|
+
assignee: get_slack_user_id(value.dig('assignee', 'id'), value.dig('assignee', 'username')),
|
51
|
+
assignees: get_slack_user_ids(value, 'assignees'),
|
52
|
+
reviewers: get_slack_user_ids(value, 'reviewers'),
|
53
|
+
labels: value['labels']&.map { |label| label },
|
54
|
+
milestone_title: value.dig('milestone', 'title')
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
2
|
+
require_relative './file_loader'
|
3
|
+
|
4
|
+
module Fastlane
|
5
|
+
module Helper
|
6
|
+
class SlackFileClient
|
7
|
+
include Helper::FileLoader
|
8
|
+
|
9
|
+
SLACK_MESSAGE_FORMAT_DEFAULT = {
|
10
|
+
'text' => 'Hello, its beautiful day!',
|
11
|
+
'header' => 'Hi there, Just wanted to let you know that we have MR_TOTAL merge requests that need your attention.',
|
12
|
+
'mr_item' => '<MR_LINK|MR_TITLE> MR_ASSIGNEE_SINGLE',
|
13
|
+
'footer' => 'Thank you',
|
14
|
+
'empty_mr_text' => 'Congratulation there is no merge request anymore, keep the good works'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def message_format(filepath)
|
18
|
+
UI.message 'Load slack message format'
|
19
|
+
load_json(filepath, SLACK_MESSAGE_FORMAT_DEFAULT)
|
20
|
+
end
|
21
|
+
|
22
|
+
def users(filepath)
|
23
|
+
UI.message 'Load slack users'
|
24
|
+
slack_users = load_csv_to_hash(
|
25
|
+
filepath,
|
26
|
+
'Gitlab User ID',
|
27
|
+
'Name',
|
28
|
+
'Slack ID',
|
29
|
+
'name',
|
30
|
+
'slack_id'
|
31
|
+
)
|
32
|
+
|
33
|
+
UI.success 'Slack users nil, skipped using tag user `@user` in when posting message reminder in slack' if slack_users.nil?
|
34
|
+
|
35
|
+
slack_users
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
2
|
+
require_relative './api_request'
|
3
|
+
require_relative './time_format'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
module Helper
|
7
|
+
class SlackMessageClient
|
8
|
+
include Helper::ApiRequest
|
9
|
+
include Helper::TimeFormat
|
10
|
+
|
11
|
+
def initialize(slack_users, slack_message_format, merge_requests, current_milestone)
|
12
|
+
@slack_users = slack_users
|
13
|
+
@slack_message_format = slack_message_format
|
14
|
+
@merge_requests = merge_requests
|
15
|
+
@current_milestone = current_milestone
|
16
|
+
end
|
17
|
+
|
18
|
+
def post_message(slack_webhook_url, payload)
|
19
|
+
UI.message 'Posting slack message'
|
20
|
+
api_post(
|
21
|
+
slack_webhook_url,
|
22
|
+
payload,
|
23
|
+
{ "Content-Type": 'application/json' }
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def markdown_section(text)
|
28
|
+
{
|
29
|
+
type: 'section',
|
30
|
+
text: {
|
31
|
+
type: 'mrkdwn',
|
32
|
+
text: text
|
33
|
+
}
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def format_users_string(users)
|
38
|
+
users.empty? ? '-' : users.join(' ')
|
39
|
+
end
|
40
|
+
|
41
|
+
def merge_request_numbering
|
42
|
+
@merge_requests.map.with_index do |mr, index|
|
43
|
+
time = format_relative_date_time(mr[:created_at])
|
44
|
+
assignees = format_users_string(mr[:assignees])
|
45
|
+
reviewers = format_users_string(mr[:reviewers])
|
46
|
+
text = @slack_message_format['mr_item'].to_s
|
47
|
+
.gsub('MR_TITLE', mr[:title].to_s)
|
48
|
+
.gsub('MR_TIME', time.to_s)
|
49
|
+
.gsub('MR_ASSIGNEE_SINGLE', mr[:assignee].to_s)
|
50
|
+
.gsub('MR_ASSIGNEES', assignees.to_s)
|
51
|
+
.gsub('MR_REVIEWERS', reviewers.to_s)
|
52
|
+
.gsub('MR_LABELS', mr[:labels].join(','))
|
53
|
+
.gsub('MR_LINK', mr[:link].to_s)
|
54
|
+
"#{index + 1}. #{text}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def merge_request_section
|
59
|
+
mr_numbering = merge_request_numbering
|
60
|
+
mr_numbering = mr_numbering.join("\n") if mr_numbering.length > 1
|
61
|
+
|
62
|
+
markdown_section(mr_numbering)
|
63
|
+
end
|
64
|
+
|
65
|
+
def payload
|
66
|
+
UI.message 'Composing slack payload'
|
67
|
+
merge_request_size = @merge_requests.nil? ? 0 : @merge_requests.length
|
68
|
+
|
69
|
+
unless merge_request_size.positive?
|
70
|
+
UI.success 'Successfully composed slack payload but has empty merge request'
|
71
|
+
return {
|
72
|
+
text: @slack_message_format['text'],
|
73
|
+
blocks: [
|
74
|
+
markdown_section(
|
75
|
+
@slack_message_format['empty_mr_text'].gsub('MR_MILESTONE', @current_milestone)
|
76
|
+
)
|
77
|
+
]
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
header_section = markdown_section(
|
82
|
+
@slack_message_format['header'].gsub('MR_TOTAL', merge_request_size.to_s)
|
83
|
+
.gsub('MR_MILESTONE', @current_milestone)
|
84
|
+
)
|
85
|
+
footer_section = markdown_section(@slack_message_format['footer'])
|
86
|
+
|
87
|
+
UI.success "Successfully composed slack payload with total #{merge_request_size} merge request"
|
88
|
+
{
|
89
|
+
text: @slack_message_format['text'],
|
90
|
+
blocks: [
|
91
|
+
header_section,
|
92
|
+
merge_request_section,
|
93
|
+
footer_section
|
94
|
+
]
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'fastlane_core/ui/ui'
|
3
|
+
|
4
|
+
module Fastlane
|
5
|
+
module Helper
|
6
|
+
# source: https://stackoverflow.com/a/63106647
|
7
|
+
module TimeFormat
|
8
|
+
TIME_UNIT_TO_SECS = {
|
9
|
+
SECONDS: 1,
|
10
|
+
MINUTES: 60,
|
11
|
+
HOURS: 3600,
|
12
|
+
DAYS: 24 * 3600,
|
13
|
+
WEEKS: 7 * 24 * 3600,
|
14
|
+
MONTHS: 30 * 24 * 3600,
|
15
|
+
YEARS: 365 * 24 * 3600
|
16
|
+
}.freeze
|
17
|
+
TIME_UNIT_LABELS = {
|
18
|
+
SECONDS: 'seconds',
|
19
|
+
MINUTES: 'minutes',
|
20
|
+
HOURS: 'hours',
|
21
|
+
DAYS: 'days',
|
22
|
+
WEEKS: 'weeks',
|
23
|
+
MONTHS: 'months',
|
24
|
+
YEARS: 'years'
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def format_relative_date_time(date_string, date_format = '%Y-%m-%dT%H:%M:%S.%L%Z')
|
28
|
+
created_at = DateTime.strptime(date_string, date_format)
|
29
|
+
now = DateTime.now
|
30
|
+
diff = now.to_time.to_i - created_at.to_time.to_i
|
31
|
+
|
32
|
+
if diff.negative?
|
33
|
+
UI.message '`date` cannot be in the future'
|
34
|
+
return 'in future'
|
35
|
+
end
|
36
|
+
|
37
|
+
time_unit = if diff < TIME_UNIT_TO_SECS[:MINUTES]
|
38
|
+
:SECONDS
|
39
|
+
elsif diff < TIME_UNIT_TO_SECS[:HOURS]
|
40
|
+
:MINUTES
|
41
|
+
elsif diff < TIME_UNIT_TO_SECS[:DAYS]
|
42
|
+
:HOURS
|
43
|
+
elsif diff < TIME_UNIT_TO_SECS[:WEEKS]
|
44
|
+
:DAYS
|
45
|
+
elsif diff < TIME_UNIT_TO_SECS[:MONTHS]
|
46
|
+
:WEEKS
|
47
|
+
elsif diff < TIME_UNIT_TO_SECS[:YEARS]
|
48
|
+
:MONTHS
|
49
|
+
else
|
50
|
+
:YEARS
|
51
|
+
end
|
52
|
+
|
53
|
+
time_relative = case time_unit
|
54
|
+
when :SECONDS, :MINUTES, :HOURS, :DAYS, :WEEKS
|
55
|
+
diff / TIME_UNIT_TO_SECS[time_unit]
|
56
|
+
when :MONTHS
|
57
|
+
0.step.find { |n| (created_at >> n) > now } - 1
|
58
|
+
else
|
59
|
+
0.step.find { |n| (created_at >> 12 * n) > now } - 1
|
60
|
+
end
|
61
|
+
|
62
|
+
"#{time_relative} #{TIME_UNIT_LABELS[time_unit]} ago"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fastlane-plugin-rooster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Muhammad M Munir
|
7
|
+
- Muhammad M. Munir
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-06-
|
11
|
+
date: 2023-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -159,8 +159,14 @@ files:
|
|
159
159
|
- LICENSE
|
160
160
|
- README.md
|
161
161
|
- lib/fastlane/plugin/rooster.rb
|
162
|
-
- lib/fastlane/plugin/rooster/actions/
|
163
|
-
- lib/fastlane/plugin/rooster/helper/
|
162
|
+
- lib/fastlane/plugin/rooster/actions/rooster_merge_request_action.rb
|
163
|
+
- lib/fastlane/plugin/rooster/helper/api_request.rb
|
164
|
+
- lib/fastlane/plugin/rooster/helper/file_loader.rb
|
165
|
+
- lib/fastlane/plugin/rooster/helper/gitlab_api_client.rb
|
166
|
+
- lib/fastlane/plugin/rooster/helper/gitlab_merge_request_transformer.rb
|
167
|
+
- lib/fastlane/plugin/rooster/helper/slack_file_client.rb
|
168
|
+
- lib/fastlane/plugin/rooster/helper/slack_message_client.rb
|
169
|
+
- lib/fastlane/plugin/rooster/helper/time_format.rb
|
164
170
|
- lib/fastlane/plugin/rooster/version.rb
|
165
171
|
homepage: https://github.com/tbetmen/fastlane-plugin-rooster
|
166
172
|
licenses:
|
@@ -1,420 +0,0 @@
|
|
1
|
-
require 'net/http'
|
2
|
-
require 'uri'
|
3
|
-
require 'json'
|
4
|
-
require 'csv'
|
5
|
-
require 'date'
|
6
|
-
|
7
|
-
require 'fastlane/action'
|
8
|
-
require_relative '../helper/rooster_helper'
|
9
|
-
|
10
|
-
module Fastlane
|
11
|
-
module Actions
|
12
|
-
class RoosterAction < Action
|
13
|
-
def self.run(params)
|
14
|
-
members_file = params[:members_file] || ENV['GITLAB_SLACK_MEMBERS_FILE']
|
15
|
-
gitlab_project_id = params[:gitlab_project_id] || ENV['GITLAB_PROJECT_ID']
|
16
|
-
slack_webhook_url = params[:slack_webhook_url] || ENV['SLACK_WEBHOOK_URL']
|
17
|
-
slack_message_format_file = params[:slack_message_format_file] || ENV['SLACK_MESSAGE_FORMAT_FILE']
|
18
|
-
gitlab_mr_total = params[:gitlab_merge_request_total] || ENV['GITLAB_MERGE_REQUEST_TOTAL']
|
19
|
-
gitlab_milestones_path = params[:gitlab_milestones_path] || ENV['GITLAB_MILESTONES_PATH']
|
20
|
-
|
21
|
-
@gitlab_token = params[:gitlab_token] || ENV['GITLAB_ACCESS_TOKEN']
|
22
|
-
@gitlab_api_base_url = 'https://gitlab.com/api/v4/'
|
23
|
-
@slack_message_format = get_slack_message_format(slack_message_format_file)
|
24
|
-
@members = get_members(members_file)
|
25
|
-
@current_milestone = get_current_milestone(gitlab_milestones_path)
|
26
|
-
|
27
|
-
mr_url = compose_merge_request_url(gitlab_project_id, gitlab_mr_total, @current_milestone)
|
28
|
-
mr_resp_json = get_response_json(mr_url)
|
29
|
-
merge_requests = compose_gitlab_merge_requests(mr_resp_json)
|
30
|
-
slack_payload = compose_slack_payload(merge_requests)
|
31
|
-
|
32
|
-
# UI.message slack_payload.to_json
|
33
|
-
post_request(
|
34
|
-
slack_webhook_url,
|
35
|
-
slack_payload,
|
36
|
-
{ "Content-Type": "application/json" }
|
37
|
-
)
|
38
|
-
end
|
39
|
-
|
40
|
-
#####################################################
|
41
|
-
# @!group API REQUEST
|
42
|
-
#####################################################
|
43
|
-
|
44
|
-
def self.get_response_json(url)
|
45
|
-
uri = URI(url)
|
46
|
-
header = { 'PRIVATE-TOKEN' => @gitlab_token }
|
47
|
-
response = Net::HTTP.get_response(uri, header)
|
48
|
-
|
49
|
-
if response.is_a?(Net::HTTPSuccess)
|
50
|
-
JSON.parse(response.body)
|
51
|
-
else
|
52
|
-
UI.error "Failed to get request from #{url} code #{response.code} message #{response.message}"
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def self.post_request(url, body, header)
|
57
|
-
uri = URI(url)
|
58
|
-
request = Net::HTTP::Post.new(uri, header)
|
59
|
-
request.body = JSON(body)
|
60
|
-
|
61
|
-
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |https|
|
62
|
-
https.request(request)
|
63
|
-
end
|
64
|
-
|
65
|
-
if response.is_a?(Net::HTTPSuccess)
|
66
|
-
UI.success "Successfully post request to #{url}"
|
67
|
-
else
|
68
|
-
UI.error "Failed to post request to #{url} code #{response.code} message #{response.message}"
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
#####################################################
|
73
|
-
# @!group MILESTONE
|
74
|
-
#####################################################
|
75
|
-
|
76
|
-
def self.compose_milestones_url(milestones_path)
|
77
|
-
return nil unless !milestones_path.nil?
|
78
|
-
|
79
|
-
"#{@gitlab_api_base_url}#{milestones_path}/milestones?state=active"
|
80
|
-
end
|
81
|
-
|
82
|
-
def self.extract_current_milestone_title(milestone_resp_json)
|
83
|
-
current_milestone = milestone_resp_json.find do |value|
|
84
|
-
due_date = DateTime.strptime(value.dig('due_date'), '%Y-%m-%d')
|
85
|
-
DateTime.now.to_time.to_i <= due_date.to_time.to_i
|
86
|
-
end
|
87
|
-
|
88
|
-
return "" unless !current_milestone.nil?
|
89
|
-
|
90
|
-
current_milestone.dig('title')
|
91
|
-
end
|
92
|
-
|
93
|
-
def self.get_current_milestone(milestones_path)
|
94
|
-
milestones_url = compose_milestones_url(milestones_path)
|
95
|
-
return "" unless !milestones_url.nil?
|
96
|
-
|
97
|
-
milestones_json = get_response_json(milestones_url)
|
98
|
-
extract_current_milestone_title(milestones_json)
|
99
|
-
end
|
100
|
-
|
101
|
-
#####################################################
|
102
|
-
# @!group SLACK MESSAGE FORMAT
|
103
|
-
#####################################################
|
104
|
-
|
105
|
-
def self.get_slack_message_format(slack_message_format_file)
|
106
|
-
unless !slack_message_format_file.is_a?(Hash)
|
107
|
-
return slack_message_format_file
|
108
|
-
end
|
109
|
-
|
110
|
-
JSON.parse(File.read(slack_message_format_file))
|
111
|
-
end
|
112
|
-
|
113
|
-
#####################################################
|
114
|
-
# @!group MEMBERS
|
115
|
-
#####################################################
|
116
|
-
|
117
|
-
def self.get_members(members_file)
|
118
|
-
return nil unless !members_file.nil? && !members_file.empty?
|
119
|
-
|
120
|
-
members = {}
|
121
|
-
CSV.foreach(members_file, headers: true) do |member|
|
122
|
-
members[member['Gitlab User ID']] = {
|
123
|
-
:name => member['Name'],
|
124
|
-
:slack_id => member['Slack ID']
|
125
|
-
}
|
126
|
-
end
|
127
|
-
|
128
|
-
members
|
129
|
-
end
|
130
|
-
|
131
|
-
#####################################################
|
132
|
-
# @!group MERGE REQUEST
|
133
|
-
#####################################################
|
134
|
-
|
135
|
-
def self.compose_merge_request_url(project_id, per_page, current_milestone)
|
136
|
-
"#{@gitlab_api_base_url}projects/#{project_id}/merge_requests?state=opened&order_by=created_at&sort=asc&per_page=#{per_page}&milestone=#{current_milestone}"
|
137
|
-
end
|
138
|
-
|
139
|
-
def self.get_slack_member_id(gitlab_member_id, gitlab_member_username)
|
140
|
-
if !@members.nil? && @members.key?(gitlab_member_id.to_s)
|
141
|
-
@members[gitlab_member_id.to_s][:slack_id]
|
142
|
-
else
|
143
|
-
"@#{gitlab_member_username}"
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
def self.get_slack_member_ids(gitlab_mr_json, key)
|
148
|
-
gitlab_mr_json.dig("#{key}")&.map do |v|
|
149
|
-
get_slack_member_id(v.dig('id'), v.dig('username'))
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def self.compose_gitlab_merge_requests(gitlab_mr_json)
|
154
|
-
gitlab_mr_json = gitlab_mr_json.select do |value|
|
155
|
-
title = value.dig('title')
|
156
|
-
!(title.downcase.start_with? 'draft') && !(title.downcase.start_with? 'wip')
|
157
|
-
end
|
158
|
-
|
159
|
-
gitlab_mr_json.map do |value|
|
160
|
-
{
|
161
|
-
:title => value.dig('title'),
|
162
|
-
:created_at => value.dig('created_at'),
|
163
|
-
:link => value.dig('web_url'),
|
164
|
-
:assignee => get_slack_member_id(value.dig('assignee', 'id'), value.dig('assignee', 'username')),
|
165
|
-
:assignees => get_slack_member_ids(value, 'assignees'),
|
166
|
-
:reviewers => get_slack_member_ids(value, 'reviewers'),
|
167
|
-
:labels => value.dig('labels')&.map { |label| label },
|
168
|
-
:milestone_title => value.dig('milestone', 'title')
|
169
|
-
}
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
#####################################################
|
174
|
-
# @!group TIME FORMAT
|
175
|
-
# source: https://stackoverflow.com/a/63106647
|
176
|
-
#####################################################
|
177
|
-
|
178
|
-
def self.format_relative_date_time(date_string)
|
179
|
-
time_unit_to_secs = {
|
180
|
-
SECONDS: 1,
|
181
|
-
MINUTES: 60,
|
182
|
-
HOURS: 3600,
|
183
|
-
DAYS: 24 * 3600,
|
184
|
-
WEEKS: 7 * 24 * 3600,
|
185
|
-
MONTHS: 30 * 24 * 3600,
|
186
|
-
YEARS: 365 * 24 * 3600
|
187
|
-
}
|
188
|
-
time_unit_labels = {
|
189
|
-
SECONDS: 'seconds',
|
190
|
-
MINUTES: 'minutes',
|
191
|
-
HOURS: 'hours',
|
192
|
-
DAYS: 'days',
|
193
|
-
WEEKS: 'weeks',
|
194
|
-
MONTHS: 'months',
|
195
|
-
YEARS: 'years'
|
196
|
-
}
|
197
|
-
|
198
|
-
created_at = DateTime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%L%Z')
|
199
|
-
diff = DateTime.now.to_time.to_i - created_at.to_time.to_i
|
200
|
-
|
201
|
-
UI.error("'date_time' cannot be in the future") if diff.negative?
|
202
|
-
|
203
|
-
time_unit = if diff < time_unit_to_secs[:MINUTES]
|
204
|
-
:SECONDS
|
205
|
-
elsif diff < time_unit_to_secs[:HOURS]
|
206
|
-
:MINUTES
|
207
|
-
elsif diff < time_unit_to_secs[:DAYS]
|
208
|
-
:HOURS
|
209
|
-
elsif diff < time_unit_to_secs[:WEEKS]
|
210
|
-
:DAYS
|
211
|
-
elsif diff < time_unit_to_secs[:MONTHS]
|
212
|
-
:WEEKS
|
213
|
-
elsif diff < time_unit_to_secs[:YEARS]
|
214
|
-
:MONTHS
|
215
|
-
else
|
216
|
-
:YEARS
|
217
|
-
end
|
218
|
-
|
219
|
-
time_relative = case time_unit
|
220
|
-
when :SECONDS, :MINUTES, :HOURS, :DAYS, :WEEKS
|
221
|
-
diff / time_unit_to_secs[time_unit]
|
222
|
-
when :MONTHS
|
223
|
-
0.step.find { |n| (created_at >> n) > now } - 1
|
224
|
-
else
|
225
|
-
0.step.find { |n| (created_at >> 12 * n) > now } - 1
|
226
|
-
end
|
227
|
-
|
228
|
-
"#{time_relative} #{time_unit_labels[time_unit]} ago"
|
229
|
-
end
|
230
|
-
|
231
|
-
#####################################################
|
232
|
-
# @!group SLACK
|
233
|
-
#####################################################
|
234
|
-
|
235
|
-
def self.format_users_string(users)
|
236
|
-
return '-' if users.empty?
|
237
|
-
|
238
|
-
users.join(' ')
|
239
|
-
end
|
240
|
-
|
241
|
-
def self.compose_merge_request_list_message(merge_requests)
|
242
|
-
merge_requests_formatted = merge_requests.map.with_index do |mr, index|
|
243
|
-
time = format_relative_date_time(mr[:created_at])
|
244
|
-
assignees = format_users_string(mr[:assignees])
|
245
|
-
reviewers = format_users_string(mr[:reviewers])
|
246
|
-
text = @slack_message_format['mr_item'].to_s
|
247
|
-
.gsub('MR_TITLE', mr[:title].to_s)
|
248
|
-
.gsub('MR_TIME', time.to_s)
|
249
|
-
.gsub('MR_ASSIGNEE_SINGLE', mr[:assignee].to_s)
|
250
|
-
.gsub('MR_ASSIGNEES', assignees.to_s)
|
251
|
-
.gsub('MR_REVIEWERS', reviewers.to_s)
|
252
|
-
.gsub('MR_LABELS', mr[:labels].join(','))
|
253
|
-
.gsub('MR_LINK', mr[:link].to_s)
|
254
|
-
"#{index+1}. #{text}"
|
255
|
-
end
|
256
|
-
|
257
|
-
if merge_requests_formatted.length > 1
|
258
|
-
merge_requests_formatted = merge_requests_formatted.join("\n")
|
259
|
-
end
|
260
|
-
|
261
|
-
{
|
262
|
-
"type": 'section',
|
263
|
-
"text": {
|
264
|
-
"type": 'mrkdwn',
|
265
|
-
"text": merge_requests_formatted
|
266
|
-
}
|
267
|
-
}
|
268
|
-
end
|
269
|
-
|
270
|
-
def self.compose_slack_payload(merge_requests)
|
271
|
-
unless merge_requests.length > 0
|
272
|
-
return {
|
273
|
-
:text => @slack_message_format['text'],
|
274
|
-
:blocks => [
|
275
|
-
{
|
276
|
-
"type": 'section',
|
277
|
-
"text": {
|
278
|
-
"type": 'mrkdwn',
|
279
|
-
"text": @slack_message_format['empty_mr_text'].gsub('MR_MILESTONE', @current_milestone)
|
280
|
-
}
|
281
|
-
}
|
282
|
-
]
|
283
|
-
}
|
284
|
-
end
|
285
|
-
|
286
|
-
header_text = @slack_message_format['header'].gsub('MR_TOTAL', merge_requests.length.to_s)
|
287
|
-
.gsub('MR_MILESTONE', @current_milestone)
|
288
|
-
header_section = {
|
289
|
-
:type => 'section',
|
290
|
-
:text => {
|
291
|
-
:type => 'mrkdwn',
|
292
|
-
:text => header_text
|
293
|
-
}
|
294
|
-
}
|
295
|
-
mr_section = compose_merge_request_list_message(merge_requests)
|
296
|
-
footer_section = {
|
297
|
-
:type => 'section',
|
298
|
-
:text => {
|
299
|
-
:type => 'mrkdwn',
|
300
|
-
:text => @slack_message_format['footer']
|
301
|
-
}
|
302
|
-
}
|
303
|
-
|
304
|
-
{
|
305
|
-
:text => @slack_message_format['text'],
|
306
|
-
:blocks => [header_section, mr_section, footer_section]
|
307
|
-
}
|
308
|
-
end
|
309
|
-
|
310
|
-
#####################################################
|
311
|
-
# @!group Documentation
|
312
|
-
#####################################################
|
313
|
-
|
314
|
-
def self.available_options # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
315
|
-
[
|
316
|
-
FastlaneCore::ConfigItem.new(
|
317
|
-
key: :gitlab_token,
|
318
|
-
env_name: 'GITLAB_ACCESS_TOKEN',
|
319
|
-
description: 'Gitlab access token',
|
320
|
-
optional: false,
|
321
|
-
verify_block: proc do |value|
|
322
|
-
UI.user_error!('No gitlab access token given') unless value and !value.empty?
|
323
|
-
end
|
324
|
-
),
|
325
|
-
FastlaneCore::ConfigItem.new(
|
326
|
-
key: :gitlab_project_id,
|
327
|
-
env_name: 'GITLAB_PROJECT_ID',
|
328
|
-
description: 'Gitlab project id',
|
329
|
-
optional: false,
|
330
|
-
verify_block: proc do |value|
|
331
|
-
UI.user_error!('No gitlab mr url given') unless value and !value.empty?
|
332
|
-
end
|
333
|
-
),
|
334
|
-
FastlaneCore::ConfigItem.new(
|
335
|
-
key: :gitlab_milestones_path,
|
336
|
-
env_name: 'GITLAB_MILESTONES_PATH',
|
337
|
-
description: 'Gitlab group or project milestones with given format either `groups/:group_id` or `projects/:project_id`',
|
338
|
-
optional: true,
|
339
|
-
verify_block: proc do |value|
|
340
|
-
UI.user_error!('No gitlab milestones path given') unless value and !value.empty?
|
341
|
-
end
|
342
|
-
),
|
343
|
-
FastlaneCore::ConfigItem.new(
|
344
|
-
key: :gitlab_merge_request_total,
|
345
|
-
env_name: 'GITLAB_MERGE_REQUEST_TOTAL',
|
346
|
-
default_value: '10',
|
347
|
-
description: 'Total merge request to be shown in reminder, default value is `10`',
|
348
|
-
optional: true,
|
349
|
-
verify_block: proc do |value|
|
350
|
-
UI.user_error!('No gitlab merge request total given') unless value and !value.empty?
|
351
|
-
end
|
352
|
-
),
|
353
|
-
FastlaneCore::ConfigItem.new(
|
354
|
-
key: :members_file,
|
355
|
-
env_name: 'GITLAB_SLACK_MEMBERS_FILE',
|
356
|
-
description: 'Gitlab members filepath in csv format',
|
357
|
-
optional: true,
|
358
|
-
verify_block: proc do |value|
|
359
|
-
UI.user_error!('No members file given') unless value and !value.empty?
|
360
|
-
end
|
361
|
-
),
|
362
|
-
FastlaneCore::ConfigItem.new(
|
363
|
-
key: :slack_webhook_url,
|
364
|
-
env_name: 'SLACK_WEBHOOK_URL',
|
365
|
-
description: 'Slack webhook url',
|
366
|
-
optional: false,
|
367
|
-
verify_block: proc do |value|
|
368
|
-
UI.user_error!('No slack webhook url given') unless value and !value.empty?
|
369
|
-
end
|
370
|
-
),
|
371
|
-
FastlaneCore::ConfigItem.new(
|
372
|
-
key: :slack_message_format_file,
|
373
|
-
env_name: 'SLACK_MESSAGE_FORMAT_FILE',
|
374
|
-
default_value: {
|
375
|
-
'text' => 'Hello, beautiful day!',
|
376
|
-
'header' =>'Hi there, Just wanted to let you know that we have MR_TOTAL merge requests that need your attention.',
|
377
|
-
'mr_item' => '<MR_LINK|MR_TITLE> MR_ASSIGNEE_SINGLE',
|
378
|
-
'footer' => 'Thank you',
|
379
|
-
'empty_mr_text' => 'Congratulation there is no merge request anymore, keep the good works'
|
380
|
-
},
|
381
|
-
description: "Slack message format in json format contains `text`, `header`, `mr_item`, `footer`, and `empty_mr_text`"\
|
382
|
-
"| In `header` you can use use `MR_TOTAL` and `MR_MILESTONE`"\
|
383
|
-
"| In `mr_item` you can use use `MR_TITLE`, `MR_TIME`, `MR_ASSIGNEE_SINGLE`, `MR_ASSIGNEES`, `MR_REVIEWERS` `MR_LABELS`, and `MR_LINK`"\
|
384
|
-
"| In `empty_mr_text` you can use `MR_MILESTONE`",
|
385
|
-
optional: true
|
386
|
-
)
|
387
|
-
]
|
388
|
-
end
|
389
|
-
|
390
|
-
def self.example_code
|
391
|
-
[
|
392
|
-
'gitlab_mr_reminder()',
|
393
|
-
'gitlab_mr_reminder(
|
394
|
-
gitlab_members_file: "/User/documents/fe_members.csv",
|
395
|
-
gitlab_token: "XXXX",
|
396
|
-
gitlab_project_id: "https://gitlab.com/...",
|
397
|
-
slack_webhook_url: "ttps://hooks.slack.com/...",
|
398
|
-
slack_message_format_file: "/User/documents/slack_format.csv", # optional
|
399
|
-
)'
|
400
|
-
]
|
401
|
-
end
|
402
|
-
|
403
|
-
def self.is_supported?(platform)
|
404
|
-
true
|
405
|
-
end
|
406
|
-
|
407
|
-
def self.author
|
408
|
-
'tbetmen'
|
409
|
-
end
|
410
|
-
|
411
|
-
def self.description
|
412
|
-
'Send gitlab merge requests reminder to slack.'
|
413
|
-
end
|
414
|
-
|
415
|
-
def self.details
|
416
|
-
"This action is reminder system of gitlab merge request, fetching merge requests using Gitlab REST API and send to Slack using webhook url."
|
417
|
-
end
|
418
|
-
end
|
419
|
-
end
|
420
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
require 'fastlane_core/ui/ui'
|
2
|
-
|
3
|
-
module Fastlane
|
4
|
-
UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
|
5
|
-
|
6
|
-
module Helper
|
7
|
-
class RoosterHelper
|
8
|
-
# class methods that you define here become available in your action
|
9
|
-
# as `Helper::RoosterHelper.your_method`
|
10
|
-
#
|
11
|
-
def self.show_message
|
12
|
-
UI.message("Hello from the rooster plugin helper!")
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|