modulesync 1.3.0 → 2.1.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.
@@ -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,49 +95,52 @@ 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
- 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)
114
- unless options[:offline]
115
- Git.pull(options[:git_base], git_repo, options[:branch], options[:project_root], module_options || {})
116
- 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]
117
113
 
118
- 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)
119
115
  settings = Settings.new(defaults[GLOBAL_DEFAULTS_KEY] || {},
120
116
  defaults,
121
117
  module_configs[GLOBAL_DEFAULTS_KEY] || {},
122
118
  module_configs,
123
- :puppet_module => module_name,
119
+ :puppet_module => puppet_module.repository_name,
124
120
  :git_base => options[:git_base],
125
- :namespace => namespace)
121
+ :namespace => puppet_module.repository_namespace)
122
+
126
123
  settings.unmanaged_files(module_files).each do |filename|
127
- $stdout.puts "Not managing #{filename} in #{module_name}"
124
+ $stdout.puts "Not managing '#{filename}' in '#{puppet_module.given_name}'"
128
125
  end
129
126
 
130
127
  files_to_manage = settings.managed_files(module_files)
131
- files_to_manage.each { |filename| manage_file(filename, settings, options) }
128
+ files_to_manage.each { |filename| manage_file(puppet_module, filename, settings, options) }
132
129
 
133
130
  if options[:noop]
134
- 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)
135
135
  elsif !options[:offline]
136
- pushed = Git.update(git_repo, files_to_manage, options)
137
- pushed && options[:pr] && pr(module_options).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)
138
144
  end
139
145
  end
140
146
 
@@ -147,9 +153,10 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
147
153
  self.class.config_path(file, options)
148
154
  end
149
155
 
150
- def self.update(options)
151
- options = config_defaults.merge(options)
156
+ def self.update(cli_options)
157
+ @options = config_defaults.merge(cli_options)
152
158
  defaults = Util.parse_config(config_path(CONF_FILE, options))
159
+
153
160
  if options[:pr]
154
161
  unless options[:branch]
155
162
  $stderr.puts 'A branch must be specified with --branch to use --pr!'
@@ -163,26 +170,28 @@ module ModuleSync # rubocop:disable Metrics/ModuleLength
163
170
  local_files = find_template_files(local_template_dir)
164
171
  module_files = relative_names(local_files, local_template_dir)
165
172
 
166
- managed_modules = self.managed_modules(config_path(options[:managed_modules_conf], options),
167
- options[:filter],
168
- options[:negative_filter])
169
-
170
173
  errors = false
171
174
  # managed_modules is either an array or a hash
172
- managed_modules.each do |puppet_module, module_options|
175
+ managed_modules.each do |puppet_module|
173
176
  begin
174
- manage_module(puppet_module, module_files, module_options, defaults, options)
175
- rescue # rubocop:disable Lint/RescueWithoutErrorClass
176
- $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
177
185
  raise unless options[:skip_broken]
178
186
  errors = true
179
- $stdout.puts "Skipping #{puppet_module} as update process failed"
187
+ $stdout.puts "Skipping '#{puppet_module.given_name}' as update process failed"
180
188
  end
181
189
  end
182
190
  exit 1 if errors && options[:fail_on_warnings]
183
191
  end
184
192
 
185
- def self.pr(module_options)
193
+ def self.pr(puppet_module)
194
+ module_options = puppet_module.options
186
195
  github_conf = module_options[:github]
187
196
  gitlab_conf = module_options[:gitlab]
188
197
 
@@ -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',
@@ -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,26 +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}"
19
20
  target_branch = options[:pr_target_branch] || 'master'
20
21
 
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])
22
+ if options[:noop]
28
23
  $stdout.puts \
29
- "Submitted PR '#{options[:pr_title]}' to #{repo_path} - merges #{options[:branch]} into #{target_branch}"
30
- else
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?
31
34
  # Skip creating the PR if it exists already.
32
- $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
33
37
  end
34
38
 
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
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}"
38
48
 
39
49
  # We only assign labels to the PR if we've discovered a list > 1. The labels MUST
40
50
  # already exist. We DO NOT create missing labels.
@@ -15,27 +15,39 @@ module ModuleSync
15
15
 
16
16
  def manage(namespace, module_name, options)
17
17
  repo_path = File.join(namespace, module_name)
18
-
19
- head = "#{namespace}:#{options[:branch]}"
18
+ branch = options[:remote_branch] || options[:branch]
19
+ head = "#{namespace}:#{branch}"
20
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
+
21
29
  merge_requests = @api.merge_requests(repo_path,
22
30
  :state => 'opened',
23
31
  :source_branch => head,
24
32
  :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
33
+ unless merge_requests.empty?
36
34
  # Skip creating the MR if it exists already.
37
- $stdout.puts "Skipped! #{merge_requests.length} MRs found for branch #{options[:branch]}"
35
+ $stdout.puts "Skipped! #{merge_requests.length} MRs found for branch #{branch}"
36
+ return
38
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(', ')}"
39
51
  end
40
52
  end
41
53
  end