modulesync 1.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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