modulesync 0.9.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,7 +9,7 @@ require 'modulesync/settings'
9
9
  require 'modulesync/util'
10
10
  require 'monkey_patches'
11
11
 
12
- module ModuleSync
12
+ module ModuleSync # rubocop:disable Metrics/ModuleLength
13
13
  include Constants
14
14
 
15
15
  def self.config_defaults
@@ -22,31 +22,39 @@ module ModuleSync
22
22
  end
23
23
 
24
24
  def self.local_file(config_path, file)
25
- "#{config_path}/#{MODULE_FILES_DIR}/#{file}"
25
+ File.join(config_path, MODULE_FILES_DIR, file)
26
26
  end
27
27
 
28
- def self.module_file(project_root, puppet_module, file)
29
- "#{project_root}/#{puppet_module}/#{file}"
28
+ def self.module_file(project_root, namespace, puppet_module, *parts)
29
+ File.join(project_root, namespace, puppet_module, *parts)
30
30
  end
31
31
 
32
- def self.local_files(path)
33
- if File.exist?(path)
34
- # only select *.erb files, and strip the extension. This way all the code will only have to handle bare paths, except when reading the actual ERB text
35
- local_files = Find.find(path).find_all { |p| p =~ /.erb$/ && !File.directory?(p) }.collect { |p| p.chomp('.erb') }.to_a
32
+ # List all template files.
33
+ #
34
+ # Only select *.erb files, and strip the extension. This way all the code will only have to handle bare paths,
35
+ # except when reading the actual ERB text
36
+ def self.find_template_files(local_template_dir)
37
+ if File.exist?(local_template_dir)
38
+ Find.find(local_template_dir).find_all { |p| p =~ /.erb$/ && !File.directory?(p) }
39
+ .collect { |p| p.chomp('.erb') }
40
+ .to_a
36
41
  else
37
- puts "#{path} does not exist. Check that you are working in your module configs directory or that you have passed in the correct directory with -c."
42
+ $stdout.puts "#{local_template_dir} does not exist." \
43
+ ' Check that you are working in your module configs directory or' \
44
+ ' that you have passed in the correct directory with -c.'
38
45
  exit
39
46
  end
40
47
  end
41
48
 
42
- def self.module_files(local_files, path)
43
- local_files.map { |file| file.sub(/#{path}/, '') }
49
+ def self.relative_names(file_list, path)
50
+ file_list.map { |file| file.sub(/#{path}/, '') }
44
51
  end
45
52
 
46
53
  def self.managed_modules(config_file, filter, negative_filter)
47
54
  managed_modules = Util.parse_config(config_file)
48
55
  if managed_modules.empty?
49
- puts "No modules found in #{config_file}. Check that you specified the right :configs directory and :managed_modules_conf file."
56
+ $stdout.puts "No modules found in #{config_file}." \
57
+ ' Check that you specified the right :configs directory and :managed_modules_conf file.'
50
58
  exit
51
59
  end
52
60
  managed_modules.select! { |m| m =~ Regexp.new(filter) } unless filter.nil?
@@ -71,62 +79,93 @@ module ModuleSync
71
79
  end
72
80
 
73
81
  def self.manage_file(filename, settings, options)
82
+ namespace = settings.additional_settings[:namespace]
74
83
  module_name = settings.additional_settings[:puppet_module]
75
84
  configs = settings.build_file_configs(filename)
85
+ target_file = module_file(options[:project_root], namespace, module_name, filename)
76
86
  if configs['delete']
77
- Renderer.remove(module_file(options[:project_root], module_name, filename))
87
+ Renderer.remove(target_file)
78
88
  else
79
89
  templatename = local_file(options[:configs], filename)
80
90
  begin
81
91
  erb = Renderer.build(templatename)
82
- template = Renderer.render(erb, configs)
83
- Renderer.sync(template, module_file(options[:project_root], module_name, filename))
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)
84
100
  rescue # rubocop:disable Lint/RescueWithoutErrorClass
85
- STDERR.puts "Error while rendering #{filename}"
101
+ $stderr.puts "Error while rendering #{filename}"
86
102
  raise
87
103
  end
88
104
  end
89
105
  end
90
106
 
91
107
  def self.manage_module(puppet_module, module_files, module_options, defaults, options)
92
- puts "Syncing #{puppet_module}"
93
- namespace, module_name = module_name(puppet_module, options[:namespace])
108
+ default_namespace = options[:namespace]
109
+ if module_options.is_a?(Hash) && module_options.key?(:namespace)
110
+ default_namespace = module_options[:namespace]
111
+ end
112
+ namespace, module_name = module_name(puppet_module, default_namespace)
113
+ git_repo = File.join(namespace, module_name)
94
114
  unless options[:offline]
95
- git_base = options[:git_base]
96
- git_uri = "#{git_base}#{namespace}"
97
- Git.pull(git_uri, module_name, options[:branch], options[:project_root], module_options || {})
115
+ Git.pull(options[:git_base], git_repo, options[:branch], options[:project_root], module_options || {})
98
116
  end
99
- module_configs = Util.parse_config("#{options[:project_root]}/#{module_name}/#{MODULE_CONF_FILE}")
117
+
118
+ module_configs = Util.parse_config(module_file(options[:project_root], namespace, module_name, MODULE_CONF_FILE))
100
119
  settings = Settings.new(defaults[GLOBAL_DEFAULTS_KEY] || {},
101
120
  defaults,
102
121
  module_configs[GLOBAL_DEFAULTS_KEY] || {},
103
122
  module_configs,
104
123
  :puppet_module => module_name,
105
- :git_base => git_base,
124
+ :git_base => options[:git_base],
106
125
  :namespace => namespace)
107
126
  settings.unmanaged_files(module_files).each do |filename|
108
- puts "Not managing #{filename} in #{module_name}"
127
+ $stdout.puts "Not managing #{filename} in #{module_name}"
109
128
  end
110
129
 
111
130
  files_to_manage = settings.managed_files(module_files)
112
131
  files_to_manage.each { |filename| manage_file(filename, settings, options) }
113
132
 
114
133
  if options[:noop]
115
- Git.update_noop(module_name, options)
134
+ Git.update_noop(git_repo, options)
116
135
  elsif !options[:offline]
117
- Git.update(module_name, files_to_manage, options)
136
+ pushed = Git.update(git_repo, files_to_manage, options)
137
+ pushed && options[:pr] && pr(module_options).manage(namespace, module_name, options)
118
138
  end
119
139
  end
120
140
 
141
+ def self.config_path(file, options)
142
+ return file if Pathname.new(file).absolute?
143
+ File.join(options[:configs], file)
144
+ end
145
+
146
+ def config_path(file, options)
147
+ self.class.config_path(file, options)
148
+ end
149
+
121
150
  def self.update(options)
122
151
  options = config_defaults.merge(options)
123
- defaults = Util.parse_config("#{options[:configs]}/#{CONF_FILE}")
152
+ defaults = Util.parse_config(config_path(CONF_FILE, options))
153
+ if options[:pr]
154
+ unless options[:branch]
155
+ $stderr.puts 'A branch must be specified with --branch to use --pr!'
156
+ raise
157
+ end
158
+
159
+ @pr = create_pr_manager if options[:pr]
160
+ end
124
161
 
125
- path = "#{options[:configs]}/#{MODULE_FILES_DIR}"
126
- local_files = self.local_files(path)
127
- module_files = self.module_files(local_files, path)
162
+ local_template_dir = config_path(MODULE_FILES_DIR, options)
163
+ local_files = find_template_files(local_template_dir)
164
+ module_files = relative_names(local_files, local_template_dir)
128
165
 
129
- managed_modules = self.managed_modules("#{options[:configs]}/#{options[:managed_modules_conf]}", options[:filter], options[:negative_filter])
166
+ managed_modules = self.managed_modules(config_path(options[:managed_modules_conf], options),
167
+ options[:filter],
168
+ options[:negative_filter])
130
169
 
131
170
  errors = false
132
171
  # managed_modules is either an array or a hash
@@ -134,12 +173,47 @@ module ModuleSync
134
173
  begin
135
174
  manage_module(puppet_module, module_files, module_options, defaults, options)
136
175
  rescue # rubocop:disable Lint/RescueWithoutErrorClass
137
- STDERR.puts "Error while updating #{puppet_module}"
176
+ $stderr.puts "Error while updating #{puppet_module}"
138
177
  raise unless options[:skip_broken]
139
178
  errors = true
140
- puts "Skipping #{puppet_module} as update process failed"
179
+ $stdout.puts "Skipping #{puppet_module} as update process failed"
141
180
  end
142
181
  end
143
182
  exit 1 if errors && options[:fail_on_warnings]
144
183
  end
184
+
185
+ def self.pr(module_options)
186
+ github_conf = module_options[:github]
187
+ gitlab_conf = module_options[:gitlab]
188
+
189
+ if !github_conf.nil?
190
+ base_url = github_conf[:base_url] || ENV.fetch('GITHUB_BASE_URL', 'https://api.github.com')
191
+ require 'modulesync/pr/github'
192
+ ModuleSync::PR::GitHub.new(github_conf[:token], base_url)
193
+ elsif !gitlab_conf.nil?
194
+ base_url = gitlab_conf[:base_url] || ENV.fetch('GITLAB_BASE_URL', 'https://gitlab.com/api/v4')
195
+ require 'modulesync/pr/gitlab'
196
+ ModuleSync::PR::GitLab.new(gitlab_conf[:token], base_url)
197
+ elsif @pr.nil?
198
+ $stderr.puts 'No GitHub or GitLab token specified for --pr!'
199
+ raise
200
+ else
201
+ @pr
202
+ end
203
+ end
204
+
205
+ def self.create_pr_manager
206
+ github_token = ENV.fetch('GITHUB_TOKEN', '')
207
+ gitlab_token = ENV.fetch('GITLAB_TOKEN', '')
208
+
209
+ if !github_token.empty?
210
+ require 'modulesync/pr/github'
211
+ ModuleSync::PR::GitHub.new(github_token, ENV.fetch('GITHUB_BASE_URL', 'https://api.github.com'))
212
+ elsif !gitlab_token.empty?
213
+ require 'modulesync/pr/gitlab'
214
+ ModuleSync::PR::GitLab.new(gitlab_token, ENV.fetch('GITLAB_BASE_URL', 'https://gitlab.com/api/v4'))
215
+ else
216
+ warn '--pr specified without environment variables GITHUB_TOKEN or GITLAB_TOKEN'
217
+ end
218
+ end
145
219
  end
@@ -43,7 +43,8 @@ module ModuleSync
43
43
  :default => CLI.defaults[:git_base] || 'git@github.com:'
44
44
  class_option :namespace,
45
45
  :aliases => '-n',
46
- :desc => 'Remote github namespace (user or organization) to clone from and push to. Defaults to puppetlabs',
46
+ :desc => 'Remote github namespace (user or organization) to clone from and push to.' \
47
+ ' Defaults to puppetlabs',
47
48
  :default => CLI.defaults[:namespace] || 'puppetlabs'
48
49
  class_option :filter,
49
50
  :aliases => '-f',
@@ -53,7 +54,8 @@ module ModuleSync
53
54
  :desc => 'A regular expression to skip repositories.'
54
55
  class_option :branch,
55
56
  :aliases => '-b',
56
- :desc => 'Branch name to make the changes in. Defaults to the default branch of the upstream repository, but falls back to "master".',
57
+ :desc => 'Branch name to make the changes in.' \
58
+ ' Defaults to the default branch of the upstream repository, but falls back to "master".',
57
59
  :default => CLI.defaults[:branch]
58
60
 
59
61
  desc 'update', 'Update the modules in managed_modules.yml'
@@ -63,7 +65,10 @@ module ModuleSync
63
65
  :default => CLI.defaults[:message]
64
66
  option :configs,
65
67
  :aliases => '-c',
66
- :desc => 'The local directory or remote repository to define the list of managed modules, the file templates, and the default values for template variables.'
68
+ :desc => 'The local directory or remote repository to define the list of managed modules,' \
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'
67
72
  option :remote_branch,
68
73
  :aliases => '-r',
69
74
  :desc => 'Remote branch name to push the changes to. Defaults to the branch name.',
@@ -85,6 +90,20 @@ module ModuleSync
85
90
  :type => :boolean,
86
91
  :desc => 'No-op mode',
87
92
  :default => false
93
+ option :pr,
94
+ :type => :boolean,
95
+ :desc => 'Submit pull/merge request',
96
+ :default => false
97
+ option :pr_title,
98
+ :desc => 'Title of pull/merge request',
99
+ :default => CLI.defaults[:pr_title] || 'Update to module template files'
100
+ option :pr_labels,
101
+ :type => :array,
102
+ :desc => 'Labels to add to the pull/merge request',
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'
88
107
  option :offline,
89
108
  :type => :boolean,
90
109
  :desc => 'Do not run any Git commands. Allows the user to manage Git outside of ModuleSync.',
@@ -104,18 +123,21 @@ module ModuleSync
104
123
  option :tag_pattern,
105
124
  :desc => 'The pattern to use when tagging releases.'
106
125
  option :pre_commit_script,
107
- :desc => 'A script to be run before commiting',
126
+ :desc => 'A script to be run before committing',
108
127
  :default => CLI.defaults[:pre_commit_script]
109
128
  option :fail_on_warnings,
110
129
  :type => :boolean,
111
130
  :aliases => '-F',
112
- :desc => 'Produce a failure exit code when there are warnings (only has effect when --skip_broken is enabled)',
131
+ :desc => 'Produce a failure exit code when there are warnings' \
132
+ ' (only has effect when --skip_broken is enabled)',
113
133
  :default => false
114
134
 
115
135
  def update
116
136
  config = { :command => 'update' }.merge(options)
117
137
  config = Util.symbolize_keys(config)
118
- raise Thor::Error, 'No value provided for required option "--message"' unless config[:noop] || config[:message] || config[:offline]
138
+ raise Thor::Error, 'No value provided for required option "--message"' unless config[:noop] \
139
+ || config[:message] \
140
+ || config[:offline]
119
141
  config[:git_opts] = { 'amend' => config[:amend], 'force' => config[:force] }
120
142
  ModuleSync.update(config)
121
143
  end
@@ -46,12 +46,13 @@ module ModuleSync
46
46
  end
47
47
 
48
48
  def self.pull(git_base, name, branch, project_root, opts)
49
+ puts "Syncing #{name}"
49
50
  Dir.mkdir(project_root) unless Dir.exist?(project_root)
50
51
 
51
52
  # Repo needs to be cloned in the cwd
52
53
  if !Dir.exist?("#{project_root}/#{name}") || !Dir.exist?("#{project_root}/#{name}/.git")
53
54
  puts 'Cloning repository fresh'
54
- remote = opts[:remote] || (git_base.start_with?('file://') ? "#{git_base}/#{name}" : "#{git_base}/#{name}.git")
55
+ remote = opts[:remote] || (git_base.start_with?('file://') ? "#{git_base}#{name}" : "#{git_base}#{name}.git")
55
56
  local = "#{project_root}/#{name}"
56
57
  puts "Cloning from #{remote}"
57
58
  repo = ::Git.clone(remote, local)
@@ -151,11 +152,14 @@ module ModuleSync
151
152
  rescue ::Git::GitExecuteError => git_error
152
153
  if git_error.message =~ /working (directory|tree) clean/
153
154
  puts "There were no files to update in #{name}. Not committing."
155
+ return false
154
156
  else
155
157
  puts git_error
156
158
  raise
157
159
  end
158
160
  end
161
+
162
+ true
159
163
  end
160
164
 
161
165
  # Needed because of a bug in the git gem that lists ignored files as
@@ -0,0 +1,47 @@
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
+ head = "#{namespace}:#{options[:branch]}"
19
+ target_branch = options[:pr_target_branch] || 'master'
20
+
21
+ pull_requests = @api.pull_requests(repo_path, :state => 'open', :base => target_branch, :head => head)
22
+ if pull_requests.empty?
23
+ pr = @api.create_pull_request(repo_path,
24
+ target_branch,
25
+ options[:branch],
26
+ options[:pr_title],
27
+ options[:message])
28
+ $stdout.puts \
29
+ "Submitted PR '#{options[:pr_title]}' to #{repo_path} - merges #{options[:branch]} into #{target_branch}"
30
+ else
31
+ # Skip creating the PR if it exists already.
32
+ $stdout.puts "Skipped! #{pull_requests.length} PRs found for branch #{options[:branch]}"
33
+ end
34
+
35
+ # PR labels can either be a list in the YAML file or they can pass in a comma
36
+ # separated list via the command line argument.
37
+ pr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
38
+
39
+ # We only assign labels to the PR if we've discovered a list > 1. The labels MUST
40
+ # already exist. We DO NOT create missing labels.
41
+ return if pr_labels.empty?
42
+ $stdout.puts "Attaching the following labels to PR #{pr['number']}: #{pr_labels.join(', ')}"
43
+ @api.add_labels_to_an_issue(repo_path, pr['number'], pr_labels)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
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
+
19
+ head = "#{namespace}:#{options[:branch]}"
20
+ target_branch = options[:pr_target_branch] || 'master'
21
+ merge_requests = @api.merge_requests(repo_path,
22
+ :state => 'opened',
23
+ :source_branch => head,
24
+ :target_branch => target_branch)
25
+ if merge_requests.empty?
26
+ mr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
27
+ mr = @api.create_merge_request(repo_path, options[:pr_title],
28
+ :source_branch => options[:branch],
29
+ :target_branch => target_branch,
30
+ :labels => mr_labels)
31
+ $stdout.puts \
32
+ "Submitted MR '#{options[:pr_title]}' to #{repo_path} - merges #{options[:branch]} into #{target_branch}"
33
+ return if mr_labels.empty?
34
+ $stdout.puts "Attached the following labels to MR #{mr.iid}: #{mr_labels.join(', ')}"
35
+ else
36
+ # Skip creating the MR if it exists already.
37
+ $stdout.puts "Skipped! #{merge_requests.length} MRs found for branch #{options[:branch]}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -4,8 +4,9 @@ require 'find'
4
4
  module ModuleSync
5
5
  module Renderer
6
6
  class ForgeModuleFile
7
- def initialize(configs = {})
7
+ def initialize(configs = {}, metadata = {})
8
8
  @configs = configs
9
+ @metadata = metadata
9
10
  end
10
11
  end
11
12
 
@@ -26,8 +27,8 @@ module ModuleSync
26
27
  File.delete(file) if File.exist?(file)
27
28
  end
28
29
 
29
- def self.render(_template, configs = {})
30
- ForgeModuleFile.new(configs).render
30
+ def self.render(_template, configs = {}, metadata = {})
31
+ ForgeModuleFile.new(configs, metadata).render
31
32
  end
32
33
 
33
34
  def self.sync(template, target_name)
@@ -19,10 +19,9 @@ module ModuleSync
19
19
 
20
20
  def build_file_configs(target_name)
21
21
  file_def = lookup_config(defaults, target_name)
22
- file_md = lookup_config(module_defaults, target_name)
23
22
  file_mc = lookup_config(module_configs, target_name)
24
23
 
25
- global_defaults.merge(file_def).merge(file_md).merge(file_mc).merge(additional_settings)
24
+ global_defaults.merge(file_def).merge(module_defaults).merge(file_mc).merge(additional_settings)
26
25
  end
27
26
 
28
27
  def managed?(target_name)