modulesync 0.9.0 → 1.3.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.
@@ -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)