modulesync 0.10.0 → 2.0.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.
@@ -43,7 +43,8 @@ module ModuleSync
43
43
  :default => CLI.defaults[:git_base] || 'git@github.com:'
44
44
  class_option :namespace,
45
45
  :aliases => '-n',
46
- :desc => 'Remote github namespace (user or organization) to clone from and push to. Defaults to puppetlabs',
46
+ :desc => 'Remote github namespace (user or organization) to clone from and push to.' \
47
+ ' Defaults to puppetlabs',
47
48
  :default => CLI.defaults[:namespace] || 'puppetlabs'
48
49
  class_option :filter,
49
50
  :aliases => '-f',
@@ -53,7 +54,8 @@ module ModuleSync
53
54
  :desc => 'A regular expression to skip repositories.'
54
55
  class_option :branch,
55
56
  :aliases => '-b',
56
- :desc => 'Branch name to make the changes in. Defaults to the default branch of the upstream repository, but falls back to "master".',
57
+ :desc => 'Branch name to make the changes in.' \
58
+ ' Defaults to the default branch of the upstream repository, but falls back to "master".',
57
59
  :default => CLI.defaults[:branch]
58
60
 
59
61
  desc 'update', 'Update the modules in managed_modules.yml'
@@ -63,7 +65,10 @@ module ModuleSync
63
65
  :default => CLI.defaults[:message]
64
66
  option :configs,
65
67
  :aliases => '-c',
66
- :desc => 'The local directory or remote repository to define the list of managed modules, the file templates, and the default values for template variables.'
68
+ :desc => 'The local directory or remote repository to define the list of managed modules,' \
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'
67
72
  option :remote_branch,
68
73
  :aliases => '-r',
69
74
  :desc => 'Remote branch name to push the changes to. Defaults to the branch name.',
@@ -87,14 +92,18 @@ module ModuleSync
87
92
  :default => false
88
93
  option :pr,
89
94
  :type => :boolean,
90
- :desc => 'Submit GitHub PR',
95
+ :desc => 'Submit pull/merge request',
91
96
  :default => false
92
97
  option :pr_title,
93
- :desc => 'Title of GitHub PR',
98
+ :desc => 'Title of pull/merge request',
94
99
  :default => CLI.defaults[:pr_title] || 'Update to module template files'
95
100
  option :pr_labels,
96
- :desc => 'Labels to add to the GitHub PR',
101
+ :type => :array,
102
+ :desc => 'Labels to add to the pull/merge request',
97
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'
98
107
  option :offline,
99
108
  :type => :boolean,
100
109
  :desc => 'Do not run any Git commands. Allows the user to manage Git outside of ModuleSync.',
@@ -114,18 +123,21 @@ module ModuleSync
114
123
  option :tag_pattern,
115
124
  :desc => 'The pattern to use when tagging releases.'
116
125
  option :pre_commit_script,
117
- :desc => 'A script to be run before commiting',
126
+ :desc => 'A script to be run before committing',
118
127
  :default => CLI.defaults[:pre_commit_script]
119
128
  option :fail_on_warnings,
120
129
  :type => :boolean,
121
130
  :aliases => '-F',
122
- :desc => 'Produce a failure exit code when there are warnings (only has effect when --skip_broken is enabled)',
131
+ :desc => 'Produce a failure exit code when there are warnings' \
132
+ ' (only has effect when --skip_broken is enabled)',
123
133
  :default => false
124
134
 
125
135
  def update
126
136
  config = { :command => 'update' }.merge(options)
127
137
  config = Util.symbolize_keys(config)
128
- raise Thor::Error, 'No value provided for required option "--message"' unless config[:noop] || config[:message] || config[:offline]
138
+ raise Thor::Error, 'No value provided for required option "--message"' unless config[:noop] \
139
+ || config[:message] \
140
+ || config[:offline]
129
141
  config[:git_opts] = { 'amend' => config[:amend], 'force' => config[:force] }
130
142
  ModuleSync.update(config)
131
143
  end
@@ -46,12 +46,13 @@ module ModuleSync
46
46
  end
47
47
 
48
48
  def self.pull(git_base, name, branch, project_root, opts)
49
+ puts "Syncing #{name}"
49
50
  Dir.mkdir(project_root) unless Dir.exist?(project_root)
50
51
 
51
52
  # Repo needs to be cloned in the cwd
52
53
  if !Dir.exist?("#{project_root}/#{name}") || !Dir.exist?("#{project_root}/#{name}/.git")
53
54
  puts 'Cloning repository fresh'
54
- remote = opts[:remote] || (git_base.start_with?('file://') ? "#{git_base}/#{name}" : "#{git_base}/#{name}.git")
55
+ remote = opts[:remote] || (git_base.start_with?('file://') ? "#{git_base}#{name}" : "#{git_base}#{name}.git")
55
56
  local = "#{project_root}/#{name}"
56
57
  puts "Cloning from #{remote}"
57
58
  repo = ::Git.clone(remote, local)
@@ -0,0 +1,56 @@
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
+ head = "#{namespace}:#{options[:branch]}"
19
+ target_branch = options[:pr_target_branch] || 'master'
20
+
21
+ if options[:noop]
22
+ $stdout.puts \
23
+ "Using no-op. Would submit PR '#{options[:pr_title]}' to #{repo_path} " \
24
+ "- merges #{options[:branch]} into #{target_branch}"
25
+ return
26
+ end
27
+
28
+ pull_requests = @api.pull_requests(repo_path,
29
+ :state => 'open',
30
+ :base => target_branch,
31
+ :head => head)
32
+ unless pull_requests.empty?
33
+ # Skip creating the PR if it exists already.
34
+ $stdout.puts "Skipped! #{pull_requests.length} PRs found for branch #{options[:branch]}"
35
+ return
36
+ end
37
+
38
+ pr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
39
+ pr = @api.create_pull_request(repo_path,
40
+ target_branch,
41
+ options[:branch],
42
+ options[:pr_title],
43
+ options[:message])
44
+ $stdout.puts \
45
+ "Submitted PR '#{options[:pr_title]}' to #{repo_path} " \
46
+ "- merges #{options[:branch]} into #{target_branch}"
47
+
48
+ # We only assign labels to the PR if we've discovered a list > 1. The labels MUST
49
+ # already exist. We DO NOT create missing labels.
50
+ return if pr_labels.empty?
51
+ $stdout.puts "Attaching the following labels to PR #{pr['number']}: #{pr_labels.join(', ')}"
52
+ @api.add_labels_to_an_issue(repo_path, pr['number'], pr_labels)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,53 @@
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
+ head = "#{namespace}:#{options[:branch]}"
19
+ target_branch = options[:pr_target_branch] || 'master'
20
+
21
+ if options[:noop]
22
+ $stdout.puts \
23
+ "Using no-op. Would submit MR '#{options[:pr_title]}' to #{repo_path} " \
24
+ "- merges #{options[:branch]} into #{target_branch}"
25
+ return
26
+ end
27
+
28
+ merge_requests = @api.merge_requests(repo_path,
29
+ :state => 'opened',
30
+ :source_branch => head,
31
+ :target_branch => target_branch)
32
+ unless merge_requests.empty?
33
+ # Skip creating the MR if it exists already.
34
+ $stdout.puts "Skipped! #{merge_requests.length} MRs found for branch #{options[:branch]}"
35
+ return
36
+ end
37
+
38
+ mr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
39
+ mr = @api.create_merge_request(repo_path,
40
+ options[:pr_title],
41
+ :source_branch => options[:branch],
42
+ :target_branch => target_branch,
43
+ :labels => mr_labels)
44
+ $stdout.puts \
45
+ "Submitted MR '#{options[:pr_title]}' to #{repo_path} " \
46
+ "- merges #{options[:branch]} into #{target_branch}"
47
+
48
+ return if mr_labels.empty?
49
+ $stdout.puts "Attached the following labels to MR #{mr.iid}: #{mr_labels.join(', ')}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -4,8 +4,9 @@ require 'find'
4
4
  module ModuleSync
5
5
  module Renderer
6
6
  class ForgeModuleFile
7
- def initialize(configs = {})
7
+ def initialize(configs = {}, metadata = {})
8
8
  @configs = configs
9
+ @metadata = metadata
9
10
  end
10
11
  end
11
12
 
@@ -26,8 +27,8 @@ module ModuleSync
26
27
  File.delete(file) if File.exist?(file)
27
28
  end
28
29
 
29
- def self.render(_template, configs = {})
30
- ForgeModuleFile.new(configs).render
30
+ def self.render(_template, configs = {}, metadata = {})
31
+ ForgeModuleFile.new(configs, metadata).render
31
32
  end
32
33
 
33
34
  def self.sync(template, target_name)
@@ -19,10 +19,9 @@ module ModuleSync
19
19
 
20
20
  def build_file_configs(target_name)
21
21
  file_def = lookup_config(defaults, target_name)
22
- file_md = lookup_config(module_defaults, target_name)
23
22
  file_mc = lookup_config(module_configs, target_name)
24
23
 
25
- global_defaults.merge(file_def).merge(file_md).merge(file_mc).merge(additional_settings)
24
+ global_defaults.merge(file_def).merge(module_defaults).merge(file_mc).merge(additional_settings)
26
25
  end
27
26
 
28
27
  def managed?(target_name)
@@ -3,7 +3,10 @@ require 'yaml'
3
3
  module ModuleSync
4
4
  module Util
5
5
  def self.symbolize_keys(hash)
6
- hash.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo }
6
+ hash.inject({}) do |memo, (k, v)|
7
+ memo[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
8
+ memo
9
+ end
7
10
  end
8
11
 
9
12
  def self.parse_config(config_file)
@@ -3,27 +3,28 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = 'modulesync'
6
- spec.version = '0.10.0'
6
+ spec.version = '2.0.0'
7
7
  spec.authors = ['Vox Pupuli']
8
8
  spec.email = ['voxpupuli@groups.io']
9
9
  spec.summary = 'Puppet Module Synchronizer'
10
10
  spec.description = 'Utility to synchronize common files across puppet modules in Github.'
11
11
  spec.homepage = 'http://github.com/voxpupuli/modulesync'
12
12
  spec.license = 'Apache-2.0'
13
- spec.required_ruby_version = '>= 2.0.0'
13
+ spec.required_ruby_version = '>= 2.5.0'
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0")
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ['lib']
19
19
 
20
- spec.add_development_dependency 'aruba'
20
+ spec.add_development_dependency 'aruba', '~> 0.14'
21
21
  spec.add_development_dependency 'bundler'
22
22
  spec.add_development_dependency 'rake'
23
23
  spec.add_development_dependency 'rspec'
24
24
  spec.add_development_dependency 'rubocop', '~> 0.50.0'
25
25
 
26
26
  spec.add_runtime_dependency 'git', '~>1.3'
27
+ spec.add_runtime_dependency 'gitlab', '~>4.0'
27
28
  spec.add_runtime_dependency 'octokit', '~>4.0'
28
29
  spec.add_runtime_dependency 'puppet-blacksmith', '~>3.0'
29
30
  spec.add_runtime_dependency 'thor'
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+ require 'modulesync/pr/github'
3
+
4
+ describe ModuleSync::PR::GitHub do
5
+ context '::manage' do
6
+ before(:each) do
7
+ @git_repo = 'test/modulesync'
8
+ @namespace, @repo_name = @git_repo.split('/')
9
+ @options = {
10
+ :pr => true,
11
+ :pr_title => 'Test PR is submitted',
12
+ :branch => 'test',
13
+ :message => 'Hello world',
14
+ :pr_auto_merge => false,
15
+ }
16
+
17
+ @client = double()
18
+ allow(Octokit::Client).to receive(:new).and_return(@client)
19
+ @it = ModuleSync::PR::GitHub.new('test', 'https://api.github.com')
20
+ end
21
+
22
+ it 'submits PR when --pr is set' do
23
+ allow(@client).to receive(:pull_requests).with(@git_repo, :state => 'open', :base => 'master', :head => "#{@namespace}:#{@options[:branch]}").and_return([])
24
+ expect(@client).to receive(:create_pull_request).with(@git_repo, 'master', @options[:branch], @options[:pr_title], @options[:message]).and_return({"html_url" => "http://example.com/pulls/22"})
25
+ expect { @it.manage(@namespace, @repo_name, @options) }.to output(/Submitted PR/).to_stdout
26
+ end
27
+
28
+ it 'skips submitting PR if one has already been issued' do
29
+ pr = {
30
+ "title" => "Test title",
31
+ "html_url" => "https://example.com/pulls/44",
32
+ "number" => "44"
33
+ }
34
+
35
+ expect(@client).to receive(:pull_requests).with(@git_repo, :state => 'open', :base => 'master', :head => "#{@namespace}:#{@options[:branch]}").and_return([pr])
36
+ expect { @it.manage(@namespace, @repo_name, @options) }.to output(/Skipped! 1 PRs found for branch test/).to_stdout
37
+ end
38
+
39
+ it 'adds labels to PR when --pr-labels is set' do
40
+ @options[:pr_labels] = "HELLO,WORLD"
41
+
42
+ allow(@client).to receive(:create_pull_request).and_return({"html_url" => "http://example.com/pulls/22", "number" => "44"})
43
+ allow(@client).to receive(:pull_requests).with(@git_repo, :state => 'open', :base => 'master', :head => "#{@namespace}:#{@options[:branch]}").and_return([])
44
+
45
+ expect(@client).to receive(:add_labels_to_an_issue).with(@git_repo, "44", ["HELLO", "WORLD"])
46
+ expect { @it.manage(@namespace, @repo_name, @options) }.to output(/Attaching the following labels to PR 44: HELLO, WORLD/).to_stdout
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+ require 'modulesync/pr/gitlab'
3
+
4
+ describe ModuleSync::PR::GitLab do
5
+ context '::manage' do
6
+ before(:each) do
7
+ @git_repo = 'test/modulesync'
8
+ @namespace, @repo_name = @git_repo.split('/')
9
+ @options = {
10
+ :pr => true,
11
+ :pr_title => 'Test PR is submitted',
12
+ :branch => 'test',
13
+ :message => 'Hello world',
14
+ :pr_auto_merge => false,
15
+ }
16
+
17
+ @client = double()
18
+ allow(Gitlab::Client).to receive(:new).and_return(@client)
19
+ @it = ModuleSync::PR::GitLab.new('test', 'https://gitlab.com/api/v4')
20
+ end
21
+
22
+ it 'submits MR when --pr is set' do
23
+ allow(@client).to receive(:merge_requests)
24
+ .with(@git_repo,
25
+ :state => 'opened',
26
+ :source_branch => "#{@namespace}:#{@options[:branch]}",
27
+ :target_branch => 'master',
28
+ ).and_return([])
29
+
30
+ expect(@client).to receive(:create_merge_request)
31
+ .with(@git_repo,
32
+ @options[:pr_title],
33
+ :labels => [],
34
+ :source_branch => @options[:branch],
35
+ :target_branch => 'master',
36
+ ).and_return({"html_url" => "http://example.com/pulls/22"})
37
+
38
+ expect { @it.manage(@namespace, @repo_name, @options) }.to output(/Submitted MR/).to_stdout
39
+ end
40
+
41
+ it 'skips submitting MR if one has already been issued' do
42
+ mr = {
43
+ "title" => "Test title",
44
+ "html_url" => "https://example.com/pulls/44",
45
+ "iid" => "44"
46
+ }
47
+
48
+ expect(@client).to receive(:merge_requests)
49
+ .with(@git_repo,
50
+ :state => 'opened',
51
+ :source_branch => "#{@namespace}:#{@options[:branch]}",
52
+ :target_branch => 'master',
53
+ ).and_return([mr])
54
+
55
+ expect { @it.manage(@namespace, @repo_name, @options) }.to output(/Skipped! 1 MRs found for branch test/).to_stdout
56
+ end
57
+
58
+ it 'adds labels to MR when --pr-labels is set' do
59
+ @options[:pr_labels] = "HELLO,WORLD"
60
+ mr = double()
61
+ allow(mr).to receive(:iid).and_return("42")
62
+
63
+ expect(@client).to receive(:create_merge_request)
64
+ .with(@git_repo,
65
+ @options[:pr_title],
66
+ :labels => ["HELLO", "WORLD"],
67
+ :source_branch => @options[:branch],
68
+ :target_branch => 'master',
69
+ ).and_return(mr)
70
+
71
+ allow(@client).to receive(:merge_requests)
72
+ .with(@git_repo,
73
+ :state => 'opened',
74
+ :source_branch => "#{@namespace}:#{@options[:branch]}",
75
+ :target_branch => 'master',
76
+ ).and_return([])
77
+
78
+ expect { @it.manage(@namespace, @repo_name, @options) }.to output(/Attached the following labels to MR 42: HELLO, WORLD/).to_stdout
79
+ end
80
+ end
81
+ end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe ModuleSync do
4
4
  context '::update' do
5
5
  it 'loads the managed modules from the specified :managed_modules_conf' do
6
- allow(ModuleSync).to receive(:local_files).and_return([])
6
+ allow(ModuleSync).to receive(:find_template_files).and_return([])
7
7
  allow(ModuleSync::Util).to receive(:parse_config).with('./config_defaults.yml').and_return({})
8
8
  expect(ModuleSync).to receive(:managed_modules).with('./test_file.yml', nil, nil).and_return([])
9
9
 
@@ -11,4 +11,12 @@ describe ModuleSync do
11
11
  ModuleSync.update(options)
12
12
  end
13
13
  end
14
+
15
+ context '::pr' do
16
+ describe "Raise Error" do
17
+ it 'raises an error when neither GITHUB_TOKEN nor GITLAB_TOKEN are set for PRs' do
18
+ expect { ModuleSync.pr({}) }.to raise_error(RuntimeError).and output(/No GitHub or GitLab token specified for --pr/).to_stderr
19
+ end
20
+ end
21
+ end
14
22
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: modulesync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vox Pupuli
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-27 00:00:00.000000000 Z
11
+ date: 2020-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aruba
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '0.14'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '0.14'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '1.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: gitlab
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '4.0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '4.0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: octokit
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -151,6 +165,7 @@ files:
151
165
  - ".travis.yml"
152
166
  - CHANGELOG.md
153
167
  - Gemfile
168
+ - HISTORY.md
154
169
  - LICENSE
155
170
  - README.md
156
171
  - Rakefile
@@ -166,12 +181,16 @@ files:
166
181
  - lib/modulesync/constants.rb
167
182
  - lib/modulesync/git.rb
168
183
  - lib/modulesync/hook.rb
184
+ - lib/modulesync/pr/github.rb
185
+ - lib/modulesync/pr/gitlab.rb
169
186
  - lib/modulesync/renderer.rb
170
187
  - lib/modulesync/settings.rb
171
188
  - lib/modulesync/util.rb
172
189
  - lib/monkey_patches.rb
173
190
  - modulesync.gemspec
174
191
  - spec/spec_helper.rb
192
+ - spec/unit/modulesync/pr/github_spec.rb
193
+ - spec/unit/modulesync/pr/gitlab_spec.rb
175
194
  - spec/unit/modulesync/settings_spec.rb
176
195
  - spec/unit/modulesync_spec.rb
177
196
  homepage: http://github.com/voxpupuli/modulesync
@@ -186,14 +205,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
186
205
  requirements:
187
206
  - - ">="
188
207
  - !ruby/object:Gem::Version
189
- version: 2.0.0
208
+ version: 2.5.0
190
209
  required_rubygems_version: !ruby/object:Gem::Requirement
191
210
  requirements:
192
211
  - - ">="
193
212
  - !ruby/object:Gem::Version
194
213
  version: '0'
195
214
  requirements: []
196
- rubygems_version: 3.0.1
215
+ rubygems_version: 3.1.2
197
216
  signing_key:
198
217
  specification_version: 4
199
218
  summary: Puppet Module Synchronizer
@@ -204,5 +223,7 @@ test_files:
204
223
  - features/support/env.rb
205
224
  - features/update.feature
206
225
  - spec/spec_helper.rb
226
+ - spec/unit/modulesync/pr/github_spec.rb
227
+ - spec/unit/modulesync/pr/gitlab_spec.rb
207
228
  - spec/unit/modulesync/settings_spec.rb
208
229
  - spec/unit/modulesync_spec.rb