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.
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
@@ -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
+ """
@@ -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
- GITHUB_TOKEN = ENV.fetch('GITHUB_TOKEN', '')
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, file)
36
- File.join(project_root, namespace, puppet_module, file)
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(module_file(options[:project_root], namespace, module_name, filename))
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 = Renderer.render(erb, configs)
99
- Renderer.sync(template, module_file(options[:project_root], namespace, 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)
100
100
  rescue # rubocop:disable Lint/RescueWithoutErrorClass
101
- STDERR.puts "Error while rendering #{filename}"
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
- if options[:pr] && !GITHUB_TOKEN
109
- STDERR.puts 'Environment variable GITHUB_TOKEN must be set to use --pr!'
110
- raise unless options[:skip_broken]
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
- return nil unless pushed && options[:pr]
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(File.join(options[:configs], CONF_FILE))
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
- local_template_dir = File.join(options[:configs], MODULE_FILES_DIR)
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(File.join(options[:configs], options[:managed_modules_conf]),
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
- manage_module(puppet_module, module_files, module_options, defaults, options)
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
- STDERR.puts "Error while updating #{puppet_module}"
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
@@ -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 GitHub PR',
95
+ :desc => 'Submit pull/merge request',
94
96
  :default => false
95
97
  option :pr_title,
96
- :desc => 'Title of GitHub PR',
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
- :desc => 'Labels to add to the GitHub PR',
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