modulesync 1.3.0 → 2.1.1

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