modulesync 1.2.0 → 2.1.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.
@@ -0,0 +1,26 @@
1
+ Feature: Run `msync update` without a good context
2
+
3
+ Scenario: Run `msync update` without any module
4
+ Given a directory named "moduleroot"
5
+ When I run `msync update --message "In a bad context"`
6
+ Then the exit status should be 1
7
+ And the stderr should contain:
8
+ """
9
+ No modules found
10
+ """
11
+
12
+ Scenario: Run `msync update` without the "moduleroot" directory
13
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
14
+ When I run `msync update --message "In a bad context"`
15
+ Then the exit status should be 1
16
+ And the stderr should contain "moduleroot"
17
+
18
+ Scenario: Run `msync update` without commit message
19
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
20
+ And a directory named "moduleroot"
21
+ When I run `msync update`
22
+ Then the exit status should be 1
23
+ And the stderr should contain:
24
+ """
25
+ No value provided for required option "--message"
26
+ """
@@ -0,0 +1,87 @@
1
+ Feature: Bump a new version after an update
2
+ Scenario: Bump the module version, update changelog and tag it after an update that produces changes
3
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
4
+ And the puppet module "puppet-test" from "fakenamespace" has a file named "CHANGELOG.md" with:
5
+ """
6
+ ## 1965-04-14 - Release 0.4.2
7
+ """
8
+ And a file named "config_defaults.yml" with:
9
+ """
10
+ ---
11
+ new-file:
12
+ content: aruba
13
+ """
14
+ And a directory named "moduleroot"
15
+ And a file named "moduleroot/new-file.erb" with:
16
+ """
17
+ <%= @configs['content'] %>
18
+ """
19
+ When I run `msync update --message "Add new-file" --bump --changelog --tag`
20
+ Then the exit status should be 0
21
+ And the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba"
22
+ And the stdout should contain:
23
+ """
24
+ Bumped to version 0.4.3
25
+ """
26
+ And the stdout should contain:
27
+ """
28
+ Tagging with 0.4.3
29
+ """
30
+ And the file named "modules/fakenamespace/puppet-test/CHANGELOG.md" should contain "0.4.3"
31
+ And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba"
32
+ And the puppet module "puppet-test" from "fakenamespace" should have a tag named "0.4.3"
33
+
34
+ Scenario: Bump the module version after an update that produces changes
35
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
36
+ And a file named "config_defaults.yml" with:
37
+ """
38
+ ---
39
+ new-file:
40
+ content: aruba
41
+ """
42
+ And a directory named "moduleroot"
43
+ And a file named "moduleroot/new-file.erb" with:
44
+ """
45
+ <%= @configs['content'] %>
46
+ """
47
+ When I run `msync update --message "Add new-file" --bump`
48
+ Then the exit status should be 0
49
+ And the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba"
50
+ And the stdout should contain:
51
+ """
52
+ Bumped to version 0.4.3
53
+ """
54
+ And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba"
55
+ And the puppet module "puppet-test" from "fakenamespace" should not have a tag named "0.4.3"
56
+
57
+ Scenario: Bump the module version with changelog update when no CHANGELOG.md is available
58
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
59
+ And a file named "config_defaults.yml" with:
60
+ """
61
+ ---
62
+ new-file:
63
+ content: aruba
64
+ """
65
+ And a directory named "moduleroot"
66
+ And a file named "moduleroot/new-file.erb" with:
67
+ """
68
+ <%= @configs['content'] %>
69
+ """
70
+ When I run `msync update --message "Add new-file" --bump --changelog`
71
+ Then the exit status should be 0
72
+ And the file named "modules/fakenamespace/puppet-test/new-file" should contain "aruba"
73
+ And the stdout should contain:
74
+ """
75
+ Bumped to version 0.4.3
76
+ No CHANGELOG.md file found, not updating.
77
+ """
78
+ And the file named "modules/fakenamespace/puppet-test/CHANGELOG.md" should not exist
79
+ And the puppet module "puppet-test" from "fakenamespace" should have 2 commits made by "Aruba"
80
+
81
+ Scenario: Dont bump the module version after an update that produces no changes
82
+ Given a basic setup with a puppet module "puppet-test" from "fakenamespace"
83
+ And a directory named "moduleroot"
84
+ When I run `msync update --message "Add new-file" --bump --tag`
85
+ Then the exit status should be 0
86
+ And the puppet module "puppet-test" from "fakenamespace" should have no commits made by "Aruba"
87
+ And the puppet module "puppet-test" from "fakenamespace" should not have a tag named "0.4.3"
data/lib/modulesync.rb CHANGED
@@ -1,15 +1,19 @@
1
1
  require 'fileutils'
2
2
  require 'pathname'
3
+
3
4
  require 'modulesync/cli'
4
5
  require 'modulesync/constants'
5
- require 'modulesync/git'
6
6
  require 'modulesync/hook'
7
+ require 'modulesync/puppet_module'
7
8
  require 'modulesync/renderer'
8
9
  require 'modulesync/settings'
9
10
  require 'modulesync/util'
11
+
10
12
  require 'monkey_patches'
11
13
 
12
14
  module ModuleSync # rubocop:disable Metrics/ModuleLength
15
+ class Error < StandardError; end
16
+
13
17
  include Constants
14
18
 
15
19
  def self.config_defaults
@@ -21,12 +25,12 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
21
25
  }
22
26
  end
23
27
 
24
- def self.local_file(config_path, file)
25
- File.join(config_path, MODULE_FILES_DIR, file)
28
+ def self.options
29
+ @options
26
30
  end
27
31
 
28
- def self.module_file(project_root, namespace, puppet_module, *parts)
29
- File.join(project_root, namespace, puppet_module, *parts)
32
+ def self.local_file(config_path, file)
33
+ File.join(config_path, MODULE_FILES_DIR, file)
30
34
  end
31
35
 
32
36
  # List all template files.
@@ -39,10 +43,10 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
39
43
  .collect { |p| p.chomp('.erb') }
40
44
  .to_a
41
45
  else
42
- $stdout.puts "#{local_template_dir} does not exist." \
46
+ $stderr.puts "#{local_template_dir} does not exist." \
43
47
  ' Check that you are working in your module configs directory or' \
44
48
  ' that you have passed in the correct directory with -c.'
45
- exit
49
+ exit 1
46
50
  end
47
51
  end
48
52
 
@@ -50,21 +54,20 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
50
54
  file_list.map { |file| file.sub(/#{path}/, '') }
51
55
  end
52
56
 
53
- def self.managed_modules(config_file, filter, negative_filter)
57
+ def self.managed_modules
58
+ config_file = config_path(options[:managed_modules_conf], options)
59
+ filter = options[:filter]
60
+ negative_filter = options[:negative_filter]
61
+
54
62
  managed_modules = Util.parse_config(config_file)
55
63
  if managed_modules.empty?
56
- $stdout.puts "No modules found in #{config_file}." \
64
+ $stderr.puts "No modules found in #{config_file}." \
57
65
  ' Check that you specified the right :configs directory and :managed_modules_conf file.'
58
- exit
66
+ exit 1
59
67
  end
60
68
  managed_modules.select! { |m| m =~ Regexp.new(filter) } unless filter.nil?
61
69
  managed_modules.reject! { |m| m =~ Regexp.new(negative_filter) } unless negative_filter.nil?
62
- managed_modules
63
- end
64
-
65
- def self.module_name(module_name, default_namespace)
66
- return [default_namespace, module_name] unless module_name.include?('/')
67
- ns, mod = module_name.split('/')
70
+ managed_modules.map { |given_name, options| PuppetModule.new(given_name, options) }
68
71
  end
69
72
 
70
73
  def self.hook(options)
@@ -78,11 +81,11 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
78
81
  end
79
82
  end
80
83
 
81
- def self.manage_file(filename, settings, options)
84
+ def self.manage_file(puppet_module, filename, settings, options)
82
85
  namespace = settings.additional_settings[:namespace]
83
86
  module_name = settings.additional_settings[:puppet_module]
84
87
  configs = settings.build_file_configs(filename)
85
- target_file = module_file(options[:project_root], namespace, module_name, filename)
88
+ target_file = puppet_module.path(filename)
86
89
  if configs['delete']
87
90
  Renderer.remove(target_file)
88
91
  else
@@ -92,51 +95,68 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
92
95
  # Meta data passed to the template as @metadata[:name]
93
96
  metadata = {
94
97
  :module_name => module_name,
95
- :workdir => module_file(options[:project_root], namespace, module_name),
98
+ :workdir => puppet_module.working_directory,
96
99
  :target_file => target_file,
97
100
  }
98
101
  template = Renderer.render(erb, configs, metadata)
99
102
  Renderer.sync(template, target_file)
100
- rescue # rubocop:disable Lint/RescueWithoutErrorClass
101
- $stderr.puts "Error while rendering #{filename}"
103
+ rescue StandardError => e
104
+ $stderr.puts "#{puppet_module.given_name}: Error while rendering file: '#{filename}'"
102
105
  raise
103
106
  end
104
107
  end
105
108
  end
106
109
 
107
- def self.manage_module(puppet_module, module_files, module_options, defaults, options)
108
- namespace, module_name = module_name(puppet_module, options[:namespace])
109
- git_repo = File.join(namespace, module_name)
110
- unless options[:offline]
111
- Git.pull(options[:git_base], git_repo, options[:branch], options[:project_root], module_options || {})
112
- end
110
+ def self.manage_module(puppet_module, module_files, defaults)
111
+ puts "Syncing '#{puppet_module.given_name}'"
112
+ puppet_module.repository.prepare_workspace(options[:branch]) unless options[:offline]
113
113
 
114
- module_configs = Util.parse_config(module_file(options[:project_root], namespace, module_name, MODULE_CONF_FILE))
114
+ module_configs = Util.parse_config puppet_module.path(MODULE_CONF_FILE)
115
115
  settings = Settings.new(defaults[GLOBAL_DEFAULTS_KEY] || {},
116
116
  defaults,
117
117
  module_configs[GLOBAL_DEFAULTS_KEY] || {},
118
118
  module_configs,
119
- :puppet_module => module_name,
119
+ :puppet_module => puppet_module.repository_name,
120
120
  :git_base => options[:git_base],
121
- :namespace => namespace)
121
+ :namespace => puppet_module.repository_namespace)
122
+
122
123
  settings.unmanaged_files(module_files).each do |filename|
123
- $stdout.puts "Not managing #{filename} in #{module_name}"
124
+ $stdout.puts "Not managing '#{filename}' in '#{puppet_module.given_name}'"
124
125
  end
125
126
 
126
127
  files_to_manage = settings.managed_files(module_files)
127
- files_to_manage.each { |filename| manage_file(filename, settings, options) }
128
+ files_to_manage.each { |filename| manage_file(puppet_module, filename, settings, options) }
128
129
 
129
130
  if options[:noop]
130
- Git.update_noop(git_repo, options)
131
+ puts "Using no-op. Files in '#{puppet_module.given_name}' may be changed but will not be committed."
132
+ puppet_module.repository.show_changes(options)
133
+ options[:pr] && \
134
+ pr(puppet_module).manage(puppet_module.repository_namespace, puppet_module.repository_name, options)
131
135
  elsif !options[:offline]
132
- pushed = Git.update(git_repo, files_to_manage, options)
133
- pushed && options[:pr] && @pr.manage(namespace, module_name, options)
136
+ pushed = puppet_module.repository.submit_changes(files_to_manage, options)
137
+ # Only bump/tag if pushing didn't fail (i.e. there were changes)
138
+ if pushed && options[:bump]
139
+ new = puppet_module.bump(options[:message], options[:changelog])
140
+ puppet_module.repository.tag(new, options[:tag_pattern]) if options[:tag]
141
+ end
142
+ pushed && options[:pr] && \
143
+ pr(puppet_module).manage(puppet_module.repository_namespace, puppet_module.repository_name, options)
134
144
  end
135
145
  end
136
146
 
137
- def self.update(options)
138
- options = config_defaults.merge(options)
139
- defaults = Util.parse_config(File.join(options[:configs], CONF_FILE))
147
+ def self.config_path(file, options)
148
+ return file if Pathname.new(file).absolute?
149
+ File.join(options[:configs], file)
150
+ end
151
+
152
+ def config_path(file, options)
153
+ self.class.config_path(file, options)
154
+ end
155
+
156
+ def self.update(cli_options)
157
+ @options = config_defaults.merge(cli_options)
158
+ defaults = Util.parse_config(config_path(CONF_FILE, options))
159
+
140
160
  if options[:pr]
141
161
  unless options[:branch]
142
162
  $stderr.puts 'A branch must be specified with --branch to use --pr!'
@@ -146,29 +166,51 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
146
166
  @pr = create_pr_manager if options[:pr]
147
167
  end
148
168
 
149
- local_template_dir = File.join(options[:configs], MODULE_FILES_DIR)
169
+ local_template_dir = config_path(MODULE_FILES_DIR, options)
150
170
  local_files = find_template_files(local_template_dir)
151
171
  module_files = relative_names(local_files, local_template_dir)
152
172
 
153
- managed_modules = self.managed_modules(File.join(options[:configs], options[:managed_modules_conf]),
154
- options[:filter],
155
- options[:negative_filter])
156
-
157
173
  errors = false
158
174
  # managed_modules is either an array or a hash
159
- managed_modules.each do |puppet_module, module_options|
175
+ managed_modules.each do |puppet_module|
160
176
  begin
161
- manage_module(puppet_module, module_files, module_options, defaults, options)
162
- rescue # rubocop:disable Lint/RescueWithoutErrorClass
163
- $stderr.puts "Error while updating #{puppet_module}"
177
+ manage_module(puppet_module, module_files, defaults)
178
+ rescue ModuleSync::Error, Git::GitExecuteError => e
179
+ message = e.message || "Error during '#{options[:command]}'"
180
+ $stderr.puts "#{puppet_module.given_name}: #{message}"
181
+ exit 1 unless options[:skip_broken]
182
+ errors = true
183
+ $stdout.puts "Skipping '#{puppet_module.given_name}' as update process failed"
184
+ rescue StandardError => e
164
185
  raise unless options[:skip_broken]
165
186
  errors = true
166
- $stdout.puts "Skipping #{puppet_module} as update process failed"
187
+ $stdout.puts "Skipping '#{puppet_module.given_name}' as update process failed"
167
188
  end
168
189
  end
169
190
  exit 1 if errors && options[:fail_on_warnings]
170
191
  end
171
192
 
193
+ def self.pr(puppet_module)
194
+ module_options = puppet_module.options
195
+ github_conf = module_options[:github]
196
+ gitlab_conf = module_options[:gitlab]
197
+
198
+ if !github_conf.nil?
199
+ base_url = github_conf[:base_url] || ENV.fetch('GITHUB_BASE_URL', 'https://api.github.com')
200
+ require 'modulesync/pr/github'
201
+ ModuleSync::PR::GitHub.new(github_conf[:token], base_url)
202
+ elsif !gitlab_conf.nil?
203
+ base_url = gitlab_conf[:base_url] || ENV.fetch('GITLAB_BASE_URL', 'https://gitlab.com/api/v4')
204
+ require 'modulesync/pr/gitlab'
205
+ ModuleSync::PR::GitLab.new(gitlab_conf[:token], base_url)
206
+ elsif @pr.nil?
207
+ $stderr.puts 'No GitHub or GitLab token specified for --pr!'
208
+ raise
209
+ else
210
+ @pr
211
+ end
212
+ end
213
+
172
214
  def self.create_pr_manager
173
215
  github_token = ENV.fetch('GITHUB_TOKEN', '')
174
216
  gitlab_token = ENV.fetch('GITLAB_TOKEN', '')
@@ -177,11 +219,10 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
177
219
  require 'modulesync/pr/github'
178
220
  ModuleSync::PR::GitHub.new(github_token, ENV.fetch('GITHUB_BASE_URL', 'https://api.github.com'))
179
221
  elsif !gitlab_token.empty?
180
- require 'modulesync/pr/github'
222
+ require 'modulesync/pr/gitlab'
181
223
  ModuleSync::PR::GitLab.new(gitlab_token, ENV.fetch('GITLAB_BASE_URL', 'https://gitlab.com/api/v4'))
182
224
  else
183
- $stderr.puts 'Environment variables GITHUB_TOKEN or GITLAB_TOKEN must be set to use --pr!'
184
- raise
225
+ warn '--pr specified without environment variables GITHUB_TOKEN or GITLAB_TOKEN'
185
226
  end
186
227
  end
187
228
  end
@@ -1,10 +1,12 @@
1
1
  require 'thor'
2
+
2
3
  require 'modulesync'
4
+ require 'modulesync/cli/thor'
3
5
  require 'modulesync/constants'
4
6
  require 'modulesync/util'
5
7
 
6
8
  module ModuleSync
7
- class CLI
9
+ module CLI
8
10
  def self.defaults
9
11
  @defaults ||= Util.symbolize_keys(Util.parse_config(Constants::MODULESYNC_CONF_FILE))
10
12
  end
@@ -36,15 +38,14 @@ module ModuleSync
36
38
  class Base < Thor
37
39
  class_option :project_root,
38
40
  :aliases => '-c',
39
- :desc => 'Path used by git to clone modules into. Defaults to "modules"',
41
+ :desc => 'Path used by git to clone modules into.',
40
42
  :default => CLI.defaults[:project_root] || 'modules'
41
43
  class_option :git_base,
42
44
  :desc => 'Specify the base part of a git URL to pull from',
43
45
  :default => CLI.defaults[:git_base] || 'git@github.com:'
44
46
  class_option :namespace,
45
47
  :aliases => '-n',
46
- :desc => 'Remote github namespace (user or organization) to clone from and push to.' \
47
- ' Defaults to puppetlabs',
48
+ :desc => 'Remote github namespace (user or organization) to clone from and push to.',
48
49
  :default => CLI.defaults[:namespace] || 'puppetlabs'
49
50
  class_option :filter,
50
51
  :aliases => '-f',
@@ -67,6 +68,8 @@ module ModuleSync
67
68
  :aliases => '-c',
68
69
  :desc => 'The local directory or remote repository to define the list of managed modules,' \
69
70
  ' the file templates, and the default values for template variables.'
71
+ option :managed_modules_conf,
72
+ :desc => 'The file name to define the list of managed modules'
70
73
  option :remote_branch,
71
74
  :aliases => '-r',
72
75
  :desc => 'Remote branch name to push the changes to. Defaults to the branch name.',
@@ -99,6 +102,9 @@ module ModuleSync
99
102
  :type => :array,
100
103
  :desc => 'Labels to add to the pull/merge request',
101
104
  :default => CLI.defaults[:pr_labels] || []
105
+ option :pr_target_branch,
106
+ :desc => 'Target branch for the pull/merge request',
107
+ :default => CLI.defaults[:pr_target_branch] || 'master'
102
108
  option :offline,
103
109
  :type => :boolean,
104
110
  :desc => 'Do not run any Git commands. Allows the user to manage Git outside of ModuleSync.',
@@ -0,0 +1,24 @@
1
+ require 'thor'
2
+ require 'modulesync/cli'
3
+
4
+ module ModuleSync
5
+ module CLI
6
+ # Workaround some, still unfixed, Thor behaviors
7
+ #
8
+ # This class extends ::Thor class to
9
+ # - exit with status code sets to `1` on Thor failure (e.g. missing required option)
10
+ # - exit with status code sets to `1` when user calls `msync` (or a subcommand) without required arguments
11
+ class Thor < ::Thor
12
+ desc '_invalid_command_call', 'Invalid command', hide: true
13
+ def _invalid_command_call
14
+ self.class.new.help
15
+ exit 1
16
+ end
17
+ default_task :_invalid_command_call
18
+
19
+ def self.exit_on_failure?
20
+ true
21
+ end
22
+ end
23
+ end
24
+ end
@@ -15,20 +15,36 @@ module ModuleSync
15
15
 
16
16
  def manage(namespace, module_name, options)
17
17
  repo_path = File.join(namespace, module_name)
18
- head = "#{namespace}:#{options[:branch]}"
18
+ branch = options[:remote_branch] || options[:branch]
19
+ head = "#{namespace}:#{branch}"
20
+ target_branch = options[:pr_target_branch] || 'master'
19
21
 
20
- pull_requests = @api.pull_requests(repo_path, :state => 'open', :base => 'master', :head => head)
21
- if pull_requests.empty?
22
- pr = @api.create_pull_request(repo_path, 'master', options[:branch], options[:pr_title], options[:message])
23
- $stdout.puts "Submitted PR '#{options[:pr_title]}' to #{repo_path} - merges #{options[:branch]} into master"
24
- else
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?
25
34
  # Skip creating the PR if it exists already.
26
- $stdout.puts "Skipped! #{pull_requests.length} PRs found for branch #{options[:branch]}"
35
+ $stdout.puts "Skipped! #{pull_requests.length} PRs found for branch #{branch}"
36
+ return
27
37
  end
28
38
 
29
- # PR labels can either be a list in the YAML file or they can pass in a comma
30
- # separated list via the command line argument.
31
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}"
32
48
 
33
49
  # We only assign labels to the PR if we've discovered a list > 1. The labels MUST
34
50
  # already exist. We DO NOT create missing labels.