modulesync 1.0.0 → 2.0.1
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/.gitignore +9 -3
- data/.rubocop.yml +3 -0
- data/.rubocop_todo.yml +1 -1
- data/.travis.yml +11 -7
- data/CHANGELOG.md +57 -0
- data/Gemfile +5 -1
- data/HISTORY.md +227 -0
- data/README.md +58 -16
- data/Rakefile +25 -0
- data/features/update.feature +55 -0
- data/lib/modulesync.rb +83 -48
- data/lib/modulesync/cli.rb +9 -3
- data/lib/modulesync/pr/github.rb +57 -0
- data/lib/modulesync/pr/gitlab.rb +54 -0
- data/lib/modulesync/renderer.rb +4 -3
- data/lib/modulesync/util.rb +4 -1
- data/modulesync.gemspec +5 -4
- data/spec/unit/modulesync/pr/github_spec.rb +49 -0
- data/spec/unit/modulesync/pr/gitlab_spec.rb +81 -0
- data/spec/unit/modulesync_spec.rb +8 -0
- metadata +37 -10
data/Rakefile
CHANGED
@@ -19,3 +19,28 @@ Cucumber::Rake::Task.new do |t|
|
|
19
19
|
end
|
20
20
|
|
21
21
|
task :test => %i[clean spec cucumber rubocop]
|
22
|
+
task :default => %i[test]
|
23
|
+
|
24
|
+
begin
|
25
|
+
require 'github_changelog_generator/task'
|
26
|
+
GitHubChangelogGenerator::RakeTask.new :changelog do |config|
|
27
|
+
config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file."
|
28
|
+
config.exclude_labels = %w[duplicate question invalid wontfix wont-fix modulesync skip-changelog]
|
29
|
+
config.user = 'voxpupuli'
|
30
|
+
config.project = 'modulesync'
|
31
|
+
config.future_release = Gem::Specification.load("#{config.project}.gemspec").version
|
32
|
+
end
|
33
|
+
|
34
|
+
# Workaround for https://github.com/github-changelog-generator/github-changelog-generator/issues/715
|
35
|
+
require 'rbconfig'
|
36
|
+
if RbConfig::CONFIG['host_os'] =~ /linux/
|
37
|
+
task :changelog do
|
38
|
+
puts 'Fixing line endings...'
|
39
|
+
changelog_file = File.join(__dir__, 'CHANGELOG.md')
|
40
|
+
changelog_txt = File.read(changelog_file)
|
41
|
+
new_contents = changelog_txt.gsub(/\r\n/, "\n")
|
42
|
+
File.open(changelog_file, 'w') { |file| file.puts new_contents }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
rescue LoadError
|
46
|
+
end
|
data/features/update.feature
CHANGED
@@ -905,6 +905,28 @@ Feature: update
|
|
905
905
|
Then the output should not contain "error"
|
906
906
|
Then the output should not contain "rejected"
|
907
907
|
|
908
|
+
Scenario: Creating a GitHub PR with an update
|
909
|
+
Given a mocked git configuration
|
910
|
+
And a remote module repository
|
911
|
+
And a directory named "moduleroot"
|
912
|
+
And I set the environment variables to:
|
913
|
+
| variable | value |
|
914
|
+
| GITHUB_TOKEN | foobar |
|
915
|
+
When I run `msync update --noop --branch managed_update --pr`
|
916
|
+
Then the output should contain "Would submit PR "
|
917
|
+
And the exit status should be 0
|
918
|
+
|
919
|
+
Scenario: Creating a GitLab MR with an update
|
920
|
+
Given a mocked git configuration
|
921
|
+
And a remote module repository
|
922
|
+
And a directory named "moduleroot"
|
923
|
+
And I set the environment variables to:
|
924
|
+
| variable | value |
|
925
|
+
| GITLAB_TOKEN | foobar |
|
926
|
+
When I run `msync update --noop --branch managed_update --pr`
|
927
|
+
Then the output should contain "Would submit MR "
|
928
|
+
And the exit status should be 0
|
929
|
+
|
908
930
|
Scenario: Repository with a default branch other than master
|
909
931
|
Given a mocked git configuration
|
910
932
|
And a remote module repository with "develop" as the default branch
|
@@ -922,3 +944,36 @@ Feature: update
|
|
922
944
|
When I run `msync update -m "Update Gemfile"`
|
923
945
|
Then the exit status should be 0
|
924
946
|
Then the output should contain "Using repository's default branch: develop"
|
947
|
+
|
948
|
+
Scenario: Adding a new file from a template using meta data
|
949
|
+
And a file named "config_defaults.yml" with:
|
950
|
+
"""
|
951
|
+
---
|
952
|
+
"""
|
953
|
+
Given a file named "managed_modules.yml" with:
|
954
|
+
"""
|
955
|
+
---
|
956
|
+
- puppet-test
|
957
|
+
"""
|
958
|
+
And a file named "modulesync.yml" with:
|
959
|
+
"""
|
960
|
+
---
|
961
|
+
namespace: maestrodev
|
962
|
+
git_base: https://github.com/
|
963
|
+
"""
|
964
|
+
And a directory named "moduleroot"
|
965
|
+
And a file named "moduleroot/test.erb" with:
|
966
|
+
"""
|
967
|
+
module: <%= @metadata[:module_name] %>
|
968
|
+
target: <%= @metadata[:target_file] %>
|
969
|
+
workdir: <%= @metadata[:workdir] %>
|
970
|
+
"""
|
971
|
+
When I run `msync update --noop`
|
972
|
+
Then the exit status should be 0
|
973
|
+
Given I run `cat modules/maestrodev/puppet-test/test`
|
974
|
+
Then the output should contain:
|
975
|
+
"""
|
976
|
+
module: puppet-test
|
977
|
+
target: modules/maestrodev/puppet-test/test
|
978
|
+
workdir: modules/maestrodev/puppet-test
|
979
|
+
"""
|
data/lib/modulesync.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'fileutils'
|
2
|
-
require 'octokit'
|
3
2
|
require 'pathname'
|
4
3
|
require 'modulesync/cli'
|
5
4
|
require 'modulesync/constants'
|
@@ -10,13 +9,7 @@ require 'modulesync/settings'
|
|
10
9
|
require 'modulesync/util'
|
11
10
|
require 'monkey_patches'
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
Octokit.configure do |c|
|
16
|
-
c.api_endpoint = ENV.fetch('GITHUB_BASE_URL', 'https://api.github.com')
|
17
|
-
end
|
18
|
-
|
19
|
-
module ModuleSync
|
12
|
+
module ModuleSync # rubocop:disable Metrics/ModuleLength
|
20
13
|
include Constants
|
21
14
|
|
22
15
|
def self.config_defaults
|
@@ -32,8 +25,8 @@ module ModuleSync
|
|
32
25
|
File.join(config_path, MODULE_FILES_DIR, file)
|
33
26
|
end
|
34
27
|
|
35
|
-
def self.module_file(project_root, namespace, puppet_module,
|
36
|
-
File.join(project_root, namespace, puppet_module,
|
28
|
+
def self.module_file(project_root, namespace, puppet_module, *parts)
|
29
|
+
File.join(project_root, namespace, puppet_module, *parts)
|
37
30
|
end
|
38
31
|
|
39
32
|
# List all template files.
|
@@ -46,7 +39,7 @@ module ModuleSync
|
|
46
39
|
.collect { |p| p.chomp('.erb') }
|
47
40
|
.to_a
|
48
41
|
else
|
49
|
-
puts "#{local_template_dir} does not exist." \
|
42
|
+
$stdout.puts "#{local_template_dir} does not exist." \
|
50
43
|
' Check that you are working in your module configs directory or' \
|
51
44
|
' that you have passed in the correct directory with -c.'
|
52
45
|
exit
|
@@ -60,7 +53,7 @@ module ModuleSync
|
|
60
53
|
def self.managed_modules(config_file, filter, negative_filter)
|
61
54
|
managed_modules = Util.parse_config(config_file)
|
62
55
|
if managed_modules.empty?
|
63
|
-
puts "No modules found in #{config_file}." \
|
56
|
+
$stdout.puts "No modules found in #{config_file}." \
|
64
57
|
' Check that you specified the right :configs directory and :managed_modules_conf file.'
|
65
58
|
exit
|
66
59
|
end
|
@@ -89,28 +82,34 @@ module ModuleSync
|
|
89
82
|
namespace = settings.additional_settings[:namespace]
|
90
83
|
module_name = settings.additional_settings[:puppet_module]
|
91
84
|
configs = settings.build_file_configs(filename)
|
85
|
+
target_file = module_file(options[:project_root], namespace, module_name, filename)
|
92
86
|
if configs['delete']
|
93
|
-
Renderer.remove(
|
87
|
+
Renderer.remove(target_file)
|
94
88
|
else
|
95
89
|
templatename = local_file(options[:configs], filename)
|
96
90
|
begin
|
97
91
|
erb = Renderer.build(templatename)
|
98
|
-
template
|
99
|
-
|
92
|
+
# Meta data passed to the template as @metadata[:name]
|
93
|
+
metadata = {
|
94
|
+
:module_name => module_name,
|
95
|
+
:workdir => module_file(options[:project_root], namespace, module_name),
|
96
|
+
:target_file => target_file,
|
97
|
+
}
|
98
|
+
template = Renderer.render(erb, configs, metadata)
|
99
|
+
Renderer.sync(template, target_file)
|
100
100
|
rescue # rubocop:disable Lint/RescueWithoutErrorClass
|
101
|
-
|
101
|
+
$stderr.puts "Error while rendering #{filename}"
|
102
102
|
raise
|
103
103
|
end
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
107
107
|
def self.manage_module(puppet_module, module_files, module_options, defaults, options)
|
108
|
-
|
109
|
-
|
110
|
-
|
108
|
+
default_namespace = options[:namespace]
|
109
|
+
if module_options.is_a?(Hash) && module_options.key?(:namespace)
|
110
|
+
default_namespace = module_options[:namespace]
|
111
111
|
end
|
112
|
-
|
113
|
-
namespace, module_name = module_name(puppet_module, options[:namespace])
|
112
|
+
namespace, module_name = module_name(puppet_module, default_namespace)
|
114
113
|
git_repo = File.join(namespace, module_name)
|
115
114
|
unless options[:offline]
|
116
115
|
Git.pull(options[:git_base], git_repo, options[:branch], options[:project_root], module_options || {})
|
@@ -125,7 +124,7 @@ module ModuleSync
|
|
125
124
|
:git_base => options[:git_base],
|
126
125
|
:namespace => namespace)
|
127
126
|
settings.unmanaged_files(module_files).each do |filename|
|
128
|
-
puts "Not managing #{filename} in #{module_name}"
|
127
|
+
$stdout.puts "Not managing #{filename} in #{module_name}"
|
129
128
|
end
|
130
129
|
|
131
130
|
files_to_manage = settings.managed_files(module_files)
|
@@ -133,40 +132,39 @@ module ModuleSync
|
|
133
132
|
|
134
133
|
if options[:noop]
|
135
134
|
Git.update_noop(git_repo, options)
|
135
|
+
options[:pr] && pr(module_options).manage(namespace, module_name, options)
|
136
136
|
elsif !options[:offline]
|
137
|
-
# Git.update() returns a boolean: true if files were pushed, false if not.
|
138
137
|
pushed = Git.update(git_repo, files_to_manage, options)
|
139
|
-
|
140
|
-
|
141
|
-
# We only do GitHub PR work if the GITHUB_TOKEN variable is set in the environment.
|
142
|
-
repo_path = File.join(namespace, module_name)
|
143
|
-
puts "Submitting PR '#{options[:pr_title]}' on GitHub to #{repo_path} - merges #{options[:branch]} into master"
|
144
|
-
github = Octokit::Client.new(:access_token => GITHUB_TOKEN)
|
145
|
-
pr = github.create_pull_request(repo_path, 'master', options[:branch], options[:pr_title], options[:message])
|
146
|
-
puts "PR created at #{pr['html_url']}"
|
147
|
-
|
148
|
-
# PR labels can either be a list in the YAML file or they can pass in a comma
|
149
|
-
# separated list via the command line argument.
|
150
|
-
pr_labels = Util.parse_list(options[:pr_labels])
|
151
|
-
|
152
|
-
# We only assign labels to the PR if we've discovered a list > 1. The labels MUST
|
153
|
-
# already exist. We DO NOT create missing labels.
|
154
|
-
unless pr_labels.empty?
|
155
|
-
puts "Attaching the following labels to PR #{pr['number']}: #{pr_labels.join(', ')}"
|
156
|
-
github.add_labels_to_an_issue(repo_path, pr['number'], pr_labels)
|
157
|
-
end
|
138
|
+
pushed && options[:pr] && pr(module_options).manage(namespace, module_name, options)
|
158
139
|
end
|
159
140
|
end
|
160
141
|
|
142
|
+
def self.config_path(file, options)
|
143
|
+
return file if Pathname.new(file).absolute?
|
144
|
+
File.join(options[:configs], file)
|
145
|
+
end
|
146
|
+
|
147
|
+
def config_path(file, options)
|
148
|
+
self.class.config_path(file, options)
|
149
|
+
end
|
150
|
+
|
161
151
|
def self.update(options)
|
162
152
|
options = config_defaults.merge(options)
|
163
|
-
defaults = Util.parse_config(
|
153
|
+
defaults = Util.parse_config(config_path(CONF_FILE, options))
|
154
|
+
if options[:pr]
|
155
|
+
unless options[:branch]
|
156
|
+
$stderr.puts 'A branch must be specified with --branch to use --pr!'
|
157
|
+
raise
|
158
|
+
end
|
164
159
|
|
165
|
-
|
160
|
+
@pr = create_pr_manager if options[:pr]
|
161
|
+
end
|
162
|
+
|
163
|
+
local_template_dir = config_path(MODULE_FILES_DIR, options)
|
166
164
|
local_files = find_template_files(local_template_dir)
|
167
165
|
module_files = relative_names(local_files, local_template_dir)
|
168
166
|
|
169
|
-
managed_modules = self.managed_modules(
|
167
|
+
managed_modules = self.managed_modules(config_path(options[:managed_modules_conf], options),
|
170
168
|
options[:filter],
|
171
169
|
options[:negative_filter])
|
172
170
|
|
@@ -174,14 +172,51 @@ module ModuleSync
|
|
174
172
|
# managed_modules is either an array or a hash
|
175
173
|
managed_modules.each do |puppet_module, module_options|
|
176
174
|
begin
|
177
|
-
|
175
|
+
mod_options = module_options.nil? ? nil : Util.symbolize_keys(module_options)
|
176
|
+
manage_module(puppet_module, module_files, mod_options, defaults, options)
|
178
177
|
rescue # rubocop:disable Lint/RescueWithoutErrorClass
|
179
|
-
|
178
|
+
$stderr.puts "Error while updating #{puppet_module}"
|
180
179
|
raise unless options[:skip_broken]
|
181
180
|
errors = true
|
182
|
-
puts "Skipping #{puppet_module} as update process failed"
|
181
|
+
$stdout.puts "Skipping #{puppet_module} as update process failed"
|
183
182
|
end
|
184
183
|
end
|
185
184
|
exit 1 if errors && options[:fail_on_warnings]
|
186
185
|
end
|
186
|
+
|
187
|
+
def self.pr(module_options)
|
188
|
+
module_options ||= {}
|
189
|
+
github_conf = module_options[:github]
|
190
|
+
gitlab_conf = module_options[:gitlab]
|
191
|
+
|
192
|
+
if !github_conf.nil?
|
193
|
+
base_url = github_conf[:base_url] || ENV.fetch('GITHUB_BASE_URL', 'https://api.github.com')
|
194
|
+
require 'modulesync/pr/github'
|
195
|
+
ModuleSync::PR::GitHub.new(github_conf[:token], base_url)
|
196
|
+
elsif !gitlab_conf.nil?
|
197
|
+
base_url = gitlab_conf[:base_url] || ENV.fetch('GITLAB_BASE_URL', 'https://gitlab.com/api/v4')
|
198
|
+
require 'modulesync/pr/gitlab'
|
199
|
+
ModuleSync::PR::GitLab.new(gitlab_conf[:token], base_url)
|
200
|
+
elsif @pr.nil?
|
201
|
+
$stderr.puts 'No GitHub or GitLab token specified for --pr!'
|
202
|
+
raise
|
203
|
+
else
|
204
|
+
@pr
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.create_pr_manager
|
209
|
+
github_token = ENV.fetch('GITHUB_TOKEN', '')
|
210
|
+
gitlab_token = ENV.fetch('GITLAB_TOKEN', '')
|
211
|
+
|
212
|
+
if !github_token.empty?
|
213
|
+
require 'modulesync/pr/github'
|
214
|
+
ModuleSync::PR::GitHub.new(github_token, ENV.fetch('GITHUB_BASE_URL', 'https://api.github.com'))
|
215
|
+
elsif !gitlab_token.empty?
|
216
|
+
require 'modulesync/pr/gitlab'
|
217
|
+
ModuleSync::PR::GitLab.new(gitlab_token, ENV.fetch('GITLAB_BASE_URL', 'https://gitlab.com/api/v4'))
|
218
|
+
else
|
219
|
+
warn '--pr specified without environment variables GITHUB_TOKEN or GITLAB_TOKEN'
|
220
|
+
end
|
221
|
+
end
|
187
222
|
end
|
data/lib/modulesync/cli.rb
CHANGED
@@ -67,6 +67,8 @@ module ModuleSync
|
|
67
67
|
:aliases => '-c',
|
68
68
|
:desc => 'The local directory or remote repository to define the list of managed modules,' \
|
69
69
|
' the file templates, and the default values for template variables.'
|
70
|
+
option :managed_modules_conf,
|
71
|
+
:desc => 'The file name to define the list of managed modules'
|
70
72
|
option :remote_branch,
|
71
73
|
:aliases => '-r',
|
72
74
|
:desc => 'Remote branch name to push the changes to. Defaults to the branch name.',
|
@@ -90,14 +92,18 @@ module ModuleSync
|
|
90
92
|
:default => false
|
91
93
|
option :pr,
|
92
94
|
:type => :boolean,
|
93
|
-
:desc => 'Submit
|
95
|
+
:desc => 'Submit pull/merge request',
|
94
96
|
:default => false
|
95
97
|
option :pr_title,
|
96
|
-
:desc => 'Title of
|
98
|
+
:desc => 'Title of pull/merge request',
|
97
99
|
:default => CLI.defaults[:pr_title] || 'Update to module template files'
|
98
100
|
option :pr_labels,
|
99
|
-
:
|
101
|
+
:type => :array,
|
102
|
+
:desc => 'Labels to add to the pull/merge request',
|
100
103
|
:default => CLI.defaults[:pr_labels] || []
|
104
|
+
option :pr_target_branch,
|
105
|
+
:desc => 'Target branch for the pull/merge request',
|
106
|
+
:default => CLI.defaults[:pr_target_branch] || 'master'
|
101
107
|
option :offline,
|
102
108
|
:type => :boolean,
|
103
109
|
:desc => 'Do not run any Git commands. Allows the user to manage Git outside of ModuleSync.',
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'octokit'
|
2
|
+
require 'modulesync/util'
|
3
|
+
|
4
|
+
module ModuleSync
|
5
|
+
module PR
|
6
|
+
# GitHub creates and manages pull requests on github.com or GitHub
|
7
|
+
# Enterprise installations.
|
8
|
+
class GitHub
|
9
|
+
def initialize(token, endpoint)
|
10
|
+
Octokit.configure do |c|
|
11
|
+
c.api_endpoint = endpoint
|
12
|
+
end
|
13
|
+
@api = Octokit::Client.new(:access_token => token)
|
14
|
+
end
|
15
|
+
|
16
|
+
def manage(namespace, module_name, options)
|
17
|
+
repo_path = File.join(namespace, module_name)
|
18
|
+
branch = options[:remote_branch] || options[:branch]
|
19
|
+
head = "#{namespace}:#{branch}"
|
20
|
+
target_branch = options[:pr_target_branch] || 'master'
|
21
|
+
|
22
|
+
if options[:noop]
|
23
|
+
$stdout.puts \
|
24
|
+
"Using no-op. Would submit PR '#{options[:pr_title]}' to #{repo_path} " \
|
25
|
+
"- merges #{branch} into #{target_branch}"
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
pull_requests = @api.pull_requests(repo_path,
|
30
|
+
:state => 'open',
|
31
|
+
:base => target_branch,
|
32
|
+
:head => head)
|
33
|
+
unless pull_requests.empty?
|
34
|
+
# Skip creating the PR if it exists already.
|
35
|
+
$stdout.puts "Skipped! #{pull_requests.length} PRs found for branch #{branch}"
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
pr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
|
40
|
+
pr = @api.create_pull_request(repo_path,
|
41
|
+
target_branch,
|
42
|
+
branch,
|
43
|
+
options[:pr_title],
|
44
|
+
options[:message])
|
45
|
+
$stdout.puts \
|
46
|
+
"Submitted PR '#{options[:pr_title]}' to #{repo_path} " \
|
47
|
+
"- merges #{branch} into #{target_branch}"
|
48
|
+
|
49
|
+
# We only assign labels to the PR if we've discovered a list > 1. The labels MUST
|
50
|
+
# already exist. We DO NOT create missing labels.
|
51
|
+
return if pr_labels.empty?
|
52
|
+
$stdout.puts "Attaching the following labels to PR #{pr['number']}: #{pr_labels.join(', ')}"
|
53
|
+
@api.add_labels_to_an_issue(repo_path, pr['number'], pr_labels)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'gitlab'
|
2
|
+
require 'modulesync/util'
|
3
|
+
|
4
|
+
module ModuleSync
|
5
|
+
module PR
|
6
|
+
# GitLab creates and manages merge requests on gitlab.com or private GitLab
|
7
|
+
# installations.
|
8
|
+
class GitLab
|
9
|
+
def initialize(token, endpoint)
|
10
|
+
@api = Gitlab::Client.new(
|
11
|
+
:endpoint => endpoint,
|
12
|
+
:private_token => token
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def manage(namespace, module_name, options)
|
17
|
+
repo_path = File.join(namespace, module_name)
|
18
|
+
branch = options[:remote_branch] || options[:branch]
|
19
|
+
head = "#{namespace}:#{branch}"
|
20
|
+
target_branch = options[:pr_target_branch] || 'master'
|
21
|
+
|
22
|
+
if options[:noop]
|
23
|
+
$stdout.puts \
|
24
|
+
"Using no-op. Would submit MR '#{options[:pr_title]}' to #{repo_path} " \
|
25
|
+
"- merges #{branch} into #{target_branch}"
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
merge_requests = @api.merge_requests(repo_path,
|
30
|
+
:state => 'opened',
|
31
|
+
:source_branch => head,
|
32
|
+
:target_branch => target_branch)
|
33
|
+
unless merge_requests.empty?
|
34
|
+
# Skip creating the MR if it exists already.
|
35
|
+
$stdout.puts "Skipped! #{merge_requests.length} MRs found for branch #{branch}"
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
mr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
|
40
|
+
mr = @api.create_merge_request(repo_path,
|
41
|
+
options[:pr_title],
|
42
|
+
:source_branch => branch,
|
43
|
+
:target_branch => target_branch,
|
44
|
+
:labels => mr_labels)
|
45
|
+
$stdout.puts \
|
46
|
+
"Submitted MR '#{options[:pr_title]}' to #{repo_path} " \
|
47
|
+
"- merges #{branch} into #{target_branch}"
|
48
|
+
|
49
|
+
return if mr_labels.empty?
|
50
|
+
$stdout.puts "Attached the following labels to MR #{mr.iid}: #{mr_labels.join(', ')}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|