branchable_cdn_assets 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab78d952715ccd097239e67bccdd4e0e1a6c7635
4
- data.tar.gz: f7a994bd5aee7df2ada620b1641e769946abfb8e
3
+ metadata.gz: 5d4b5ae8228cc5c14fb3ed1ed06abf2efd1056b4
4
+ data.tar.gz: 1a3676d5d335a8a329d2196a98ea518bbeb79348
5
5
  SHA512:
6
- metadata.gz: 1f80a5a192ced43ab482fa31e870e7baf429e6e5f33a6e71be4419c0dc4b5de7bcb26105d6a5b47c04056b1324b4ca08ad7ddef6204868ef6ebef39283d30c68
7
- data.tar.gz: 8765796e5a26a58cd1519e0a707889d26d9ea26dea1448cfc53f46e1cc7e394f089a6e106487fd9210da50a3af9d0447f7e698dcfabcb83e7bf5eac9b5ff1a3d
6
+ metadata.gz: 9dca1c4d503f5f9f3c442118cdf17685f02e74d1eedfb7000fff59d107624240edd3fed79545b300a865c4f64a687bbfbf9d93e46e8ae78450ee5d361bdfd050
7
+ data.tar.gz: 7c94bfdadfc58cc6d8b3c9648c2dce11dec0aaa1eb28d0a68afc19d219b3af5aade65dc9a2ace355c504fe03cb38a4b9a9e0faf8cfe9d361ec06249a3c5a4a99
@@ -2,12 +2,13 @@ require_relative 'config/environment_attribute_reader'
2
2
 
3
3
  module BranchableCDNAssets
4
4
  class Config
5
- extend EnvironmentAttributeReader
5
+ include EnvironmentAttributeReader
6
+ extend EnvironmentAttributeReader::ClassMethods
6
7
 
7
8
  attr_reader :raw_data, :branch,
8
9
  :production_branch, :cloudfront, :cdn_dir,
9
- :env, :environments
10
- env_attr_reader :host, :root, :url
10
+ :dir_permissions, :file_permissions, :rsync_flags
11
+ env_attr_reader :host, :root, :url, :manage_cloudfront
11
12
 
12
13
  def initialize data, branch=Asgit.current_branch
13
14
  @raw_data = normalize_data data
@@ -16,13 +17,48 @@ module BranchableCDNAssets
16
17
  @production_branch = raw_data.fetch :production_branch, 'master'
17
18
  @cloudfront = raw_data.fetch :cloudfront, {}
18
19
  @cdn_dir = raw_data.fetch :dir, 'cdn'
20
+ @dir_permissions = env_attr(:dir_permissions) || 755
21
+ @file_permissions = env_attr(:file_permissions) || 644
22
+ @rsync_flags = env_attr(:rsync_flags) || '-aviz'
23
+ end
24
+
25
+ def env
26
+ @env ||= env_for_branch
27
+ end
28
+
29
+ def environments
30
+ @environments ||= add_env_path_modifications_to_env_data raw_data.fetch(:environments, {})
31
+ end
32
+
33
+ def remotes
34
+ return @_remotes if @_remotes
19
35
 
20
- @env = env_for_branch
21
- @environments = add_env_path_modifications_to_env_data raw_data.fetch(:environments, {})
36
+ @_remotes = environments[env.to_sym].fetch(:remotes, [default_remote]).map do |r|
37
+ remote = Remote.new
38
+ default_remote.merge( normalize_data(r) ).each do |k, v|
39
+ remote.public_send "#{k}=", v
40
+ end
41
+ remote
42
+ end
22
43
  end
23
44
 
24
45
  private
25
46
 
47
+ class Remote < Struct.new(:host, :root, :url,
48
+ :dir_permissions, :file_permissions, :rsync_flags)
49
+ end
50
+
51
+ def default_remote
52
+ {
53
+ host: host,
54
+ root: root,
55
+ url: url,
56
+ dir_permissions: dir_permissions,
57
+ file_permissions: file_permissions,
58
+ rsync_flags: rsync_flags
59
+ }
60
+ end
61
+
26
62
  def add_env_path_modifications_to_env_data environments
27
63
  Hash[*environments.map do |env,data|
28
64
  data = data.dup
@@ -31,14 +67,14 @@ module BranchableCDNAssets
31
67
  data[:url] = File.join( data[:url], branch )
32
68
  end
33
69
 
34
- [env,data]
70
+ [env, data]
35
71
  end.flatten]
36
72
  end
37
73
 
38
74
  def env_for_branch
39
75
  if branch == production_branch
40
76
  return :production
41
- elsif raw_data.fetch(:environments).has_key?( branch.to_sym )
77
+ elsif raw_data.fetch(:environments, {}).has_key?( branch.to_sym )
42
78
  return branch.to_sym
43
79
  else
44
80
  return raw_data.fetch(:default_env, 'staging').to_sym
@@ -2,12 +2,18 @@ module BranchableCDNAssets
2
2
  class Config
3
3
 
4
4
  module EnvironmentAttributeReader
5
- def env_attr_reader *keys
6
- keys.each do |key|
7
- define_method key do
8
- environments[env.to_sym].fetch(key) {
9
- raise ArgumentError, "No key '#{key.to_s}' exists for '#{env}'"
10
- }
5
+
6
+ def env_attr key
7
+ environments.fetch(env.to_sym, {})
8
+ .fetch(key, nil)
9
+ end
10
+
11
+ module ClassMethods
12
+ def env_attr_reader *keys
13
+ keys.each do |key|
14
+ define_method key do
15
+ env_attr(key) || raise( ArgumentError, "No key '#{key.to_s}' exists for '#{env}'" )
16
+ end
11
17
  end
12
18
  end
13
19
  end
@@ -1,4 +1,5 @@
1
1
  require_relative 'file_manager/checks'
2
+ require_relative 'file_manager/permissions'
2
3
 
3
4
  module BranchableCDNAssets
4
5
  class FileManager
@@ -26,35 +27,44 @@ module BranchableCDNAssets
26
27
  # @return [Array]
27
28
  def list where=:all
28
29
  case where
29
- when :local then
30
- return list_local_files
31
- when :remote then
32
- return list_remote_files
33
- when :both then
34
- return list_local_files & list_remote_files
35
- else
36
- return list_local_files + list_remote_files
30
+ when :local then return list_local_files
31
+ when :remote then return list_remote_files
32
+ when :both then return list_local_files & list_remote_files
33
+ else return list_local_files + list_remote_files
37
34
  end
38
35
  end
39
36
 
37
+
38
+ # make cdn dir on the remotes with given permissions
39
+ def setup
40
+ setup_remotes
41
+
42
+ return nil
43
+ end
44
+
45
+
40
46
  # destructive pull, removes references to remote
41
47
  # files once they're pulled
42
48
  # @return [Array] the pulled files
43
49
  def pull!
44
- pull_list = pull
50
+ pull_list = pull_all
45
51
  @manifest.remove_files( pull_list )
46
52
  @manifest.update_source_file!
53
+
47
54
  return pull_list
48
55
  end
49
56
 
57
+
50
58
  # destructive push, removes the local versions of
51
59
  # the pushed files and adds them to the manifest
52
60
  # @return [Array] the pushed files
53
61
  def push!
54
- setup_remote
62
+ setup_remotes
55
63
 
56
64
  push_list = push
57
- invalidate( push_list ) if config.env.to_sym == :production
65
+ if config.env.to_sym == :production && config.manage_cloudfront
66
+ invalidate( push_list )
67
+ end
58
68
 
59
69
  @manifest.merge_files( push_list )
60
70
  @manifest.update_source_file!
@@ -62,6 +72,7 @@ module BranchableCDNAssets
62
72
  return push_list
63
73
  end
64
74
 
75
+
65
76
  # remove local files that exist on the remote
66
77
  # @return [Array]
67
78
  def prune!
@@ -71,9 +82,12 @@ module BranchableCDNAssets
71
82
  list(:both).each do |f|
72
83
  Shell.run_local "rm #{File.join( root, f )}"
73
84
  end
85
+
74
86
  remove_empty_directories
87
+ return nil
75
88
  end
76
89
 
90
+
77
91
  # move the current cdn files to the production cdn
78
92
  # guarded to only run from prod
79
93
  def move_to_production branch
@@ -86,25 +100,127 @@ module BranchableCDNAssets
86
100
  push!
87
101
  end
88
102
 
103
+
89
104
  def remove_empty_directories
90
105
  empty_directories.each do |dir|
91
106
  Dir.rmdir( dir )
92
107
  end
93
108
  end
94
109
 
110
+
95
111
  def find file
96
- return :local if list(:local).include?(file)
97
- return File.join( config.url, file ) if list(:remote).include?(file)
112
+ return :local if list(:local).include?(file)
113
+ return File.join(config.url, file) if list(:remote).include?(file)
98
114
 
99
115
  unless config.env == :production
100
116
  production_config = Config.new( config.raw_data, config.production_branch )
101
117
  production_manifest = Manifest.new( File.join( root, "#{config.production_branch}.manifest" ) )
118
+
102
119
  return File.join( production_config.url, file ) if production_manifest.files.include?(file)
103
120
  end
104
121
  end
105
122
 
123
+
124
+ # x files were not on every remote
125
+ # remote-x (x files):
126
+ # - file/path
127
+ # - file/path
128
+ #
129
+ # remote-y (x files):
130
+ # - path/to/file
131
+ # - path
132
+ def doctor
133
+ db = uniq_files_per_remote
134
+ count = config.remotes.flat_map { |r| db[r.host].length }.inject(0, :+)
135
+
136
+ if count > 0
137
+ puts "#{count} files were not on every remote"
138
+ config.remotes.each do |remote|
139
+ puts "#{remote.host} (#{db[remote.host].length} files)"
140
+ if db[remote.host].length > 0
141
+ db[remote.host].each { |f| puts " + #{f}" }
142
+ end
143
+ end
144
+ else
145
+ puts "all good, content is the same on all remotes"
146
+ end
147
+
148
+ return nil
149
+ end
150
+
151
+ def heal
152
+ db = uniq_files_per_remote
153
+ all_uniq = []
154
+ config.remotes.each do |remote|
155
+ all_uniq.push *db[remote.host]
156
+ list_file = create_list_file db[remote.host].join("\n")
157
+ pull_list = map_rsync_output_to_list rsync_files_from_remote(list_file, remote)
158
+ list_file.unlink
159
+
160
+ puts "#{pull_list.length} files pulled from #{remote.host}"
161
+ end
162
+
163
+ config.remotes.each do |remote|
164
+ list_file = create_list_file ( all_uniq - db[remote.host] ).join("\n")
165
+ push_list = map_rsync_output_to_list rsync_files_to_remote( list_file, remote )
166
+ list_file.unlink
167
+
168
+ puts "#{push_list.length} files pushed to #{remote.host}"
169
+ end
170
+
171
+ nil
172
+ end
173
+
106
174
  private
107
175
 
176
+ def pull_all
177
+ db = uniq_files_per_remote
178
+ pulled = []
179
+ config.remotes.each_with_index do |remote, idx|
180
+ if idx == 0
181
+ file_list = db[remote.host] + db["all"]
182
+ else
183
+ file_list = db[remote.host]
184
+ end
185
+
186
+ list_file = create_list_file file_list.join("\n")
187
+ pull_list = map_rsync_output_to_list rsync_files_from_remote( list_file, remote )
188
+ list_file.unlink
189
+
190
+ pulled.push *pull_list
191
+ puts "#{pull_list.length} files pulled from #{remote.host}"
192
+ end
193
+
194
+ return pulled
195
+ end
196
+
197
+ def files_per_remote
198
+ config.remotes.each_with_object({}) do |remote, collection|
199
+ resp = Shell.run_remote "find #{remote.root} -type f", hostname: remote.host
200
+ if resp.success?
201
+ collection[remote.host] = resp.stdout.split("\n")
202
+ .map { |f| f.sub /^#{Regexp.escape(remote.root)}\/?/, '' }
203
+ else
204
+ warn "problem pulling file list for #{remote.host}\n#{resp.stderr}"
205
+ end
206
+ end
207
+ end
208
+
209
+ def uniq_files_per_remote
210
+ files_by_remote = files_per_remote
211
+ files_on_all = []
212
+ files_by_remote.each_with_index do |(remote, files), idx|
213
+ if idx == 0
214
+ files_on_all = files
215
+ else
216
+ files_on_all = files_on_all & files
217
+ end
218
+ end
219
+
220
+ Hash[ files_by_remote.map { |r, files| [r, (files - files_on_all)] }
221
+ .push ["all", files_on_all] ]
222
+ end
223
+
108
224
  def invalidate files
109
225
  to_invalidate = ( files & list(:local) ).map do |f|
110
226
  File.join( config.cloudfront[:path_prefix], f )
@@ -115,9 +231,15 @@ module BranchableCDNAssets
115
231
  end
116
232
 
117
233
  # create the root dir on remote
118
- def setup_remote
119
- resp = Shell.run_remote "mkdir -p #{config.root}", hostname: config.host
120
- raise resp.stderr unless resp.success?
234
+ def setup_remotes
235
+ config.remotes.each do |remote|
236
+ resp = Shell.run_remote "mkdir -p -m #{remote.dir_permissions} #{remote.root}", hostname: remote.host
237
+ unless resp.success?
238
+ raise "problem setting up #{remote.host}\n#{resp.stderr}"
239
+ else
240
+ puts "set up #{remote.root} on #{remote.host}"
241
+ end
242
+ end
121
243
  end
122
244
 
123
245
  def empty_directories
@@ -140,20 +262,30 @@ module BranchableCDNAssets
140
262
  def push
141
263
  list_file = create_list_file( list(:local).join("\n") )
142
264
  ensure_local_file_permissions
143
- push_list = map_rsync_output_to_list rsync_files_to_remote(list_file)
144
- list_file.unlink
145
265
 
266
+ push_list = config.remotes.flat_map do |remote|
267
+ map_rsync_output_to_list( rsync_files_to_remote(list_file, remote) )
268
+ end.uniq
269
+
270
+ list_file.unlink
146
271
  push_list = resolve_conflicts(push_list)
147
272
  puts "#{push_list.length} files pushed to #{branch}"
148
273
  return push_list
149
274
  end
150
275
 
151
- def rsync_files_to_remote list_file
152
- resp = Shell.run_local "rsync -aviz --files-from=#{list_file.path} " +
153
- "-e ssh #{root}/ #{config.host}:#{config.root}"
276
+ def rsync_files_to_remote list_file, remote
277
+ dir_perms = Permissions.new( remote.dir_permissions ).to_chmod
278
+ file_perms = Permissions.new( remote.file_permissions ).to_chmod
279
+ file_permissions = "--chmod=" +
280
+ "Du=#{dir_perms.user},Dg=#{dir_perms.group},Do=#{dir_perms.others}," +
281
+ "Fu=#{file_perms.user},Fg=#{file_perms.group},Fo=#{file_perms.others}"
154
282
 
155
- raise "rsync failed\n#{resp.stderr}" unless resp.success?
156
- return resp.stdout
283
+
284
+ resp = Shell.run_local "rsync #{remote.rsync_flags} #{file_permissions} --files-from=#{list_file.path} " +
285
+ "-e ssh #{root}/ #{remote.host}:#{remote.root}"
286
+
287
+ raise "rsync failed for #{remote.host}\n#{resp.stderr}" unless resp.success?
288
+ resp.stdout
157
289
  end
158
290
 
159
291
  def ensure_local_file_permissions
@@ -174,9 +306,9 @@ module BranchableCDNAssets
174
306
  return pull_list
175
307
  end
176
308
 
177
- def rsync_files_from_remote list_file
178
- resp = Shell.run_local "rsync -aviz --files-from=#{list_file.path}" +
179
- " -e ssh #{config.host}:#{config.root} #{root}/"
309
+ def rsync_files_from_remote list_file, remote
310
+ resp = Shell.run_local "rsync #{config.rsync_flags} --files-from=#{list_file.path}" +
311
+ " -e ssh #{remote.host}:#{remote.root} #{root}/"
180
312
 
181
313
  raise "rsync failed\n#{resp.stderr}" unless resp.success?
182
314
  return resp.stdout
@@ -5,17 +5,21 @@ module BranchableCDNAssets
5
5
  def manifest_list_clean_for_master
6
6
  manifests = Dir[ File.join( root, "*.manifest" ) ]
7
7
  if Asgit.current_branch == "master" && manifests.length > 1
8
- raise "there should only be a production manifest on the master branch\n" +
9
- "consider cleaning up with rake cdn:move_to_production[branch_name]"
8
+ raise "the master branch should only have a production manifest\n" +
9
+ "move branched assets to production with the `move_to_production` task"
10
10
  end
11
11
  end
12
12
 
13
13
  def local_file_conflict
14
14
  intersection = list(:both)
15
15
  if !intersection.empty?
16
- puts "#{intersection.length} files are duplicated locally and on the remote"
17
- unless Shell.get_input("do you want to continue? (y|n)") == "y"
16
+ puts "#{intersection.length} files detected locally and in the manifest"
17
+
18
+ if Shell.get_input("would you like to list the conflicting files? (y|n) ") == "y"
18
19
  puts "the conflicting files: \n#{intersection.join("\n")}"
20
+ end
21
+
22
+ unless Shell.get_input("do you want to continue? (y|n) ") == "y"
19
23
  abort
20
24
  end
21
25
  end
@@ -23,15 +27,18 @@ module BranchableCDNAssets
23
27
 
24
28
  def ready_for_production
25
29
  if Asgit.current_branch != "master"
26
- puts "you shouldn't move to production except from master"
30
+ puts "you shouldn't move to production except from master branch"
27
31
  abort
28
32
  end
33
+
29
34
  if !Asgit.remote_up_to_date?
30
- puts "make sure you've pulled all remote changes"
35
+ puts "make sure you're repo is in sync with the remote"
31
36
  abort
32
37
  end
38
+
33
39
  if !list(:local).empty?
34
- puts "push all your local files and commit first"
40
+ puts "assets detected in the local cdn directory\n" +
41
+ "remove them by running the `prune` task"
35
42
  abort
36
43
  end
37
44
  end
@@ -0,0 +1,34 @@
1
+ module BranchableCDNAssets
2
+ class FileManager
3
+
4
+ class Permissions
5
+
6
+ attr_reader :user, :group, :others
7
+
8
+ def initialize int
9
+ @user, @group, @others = int.to_s.chars.to_a
10
+ end
11
+
12
+ def to_chmod
13
+ Struct.new(:user, :group, :others)
14
+ .new( chmod_map(user), chmod_map(group), chmod_map(others) )
15
+ end
16
+
17
+ private
18
+
19
+ def chmod_map num
20
+ {
21
+ "7" => "rwx",
22
+ "6" => "rw",
23
+ "5" => "rx",
24
+ "4" => "r",
25
+ "3" => "wx",
26
+ "2" => "w",
27
+ "1" => "x",
28
+ "0" => "-"
29
+ }.fetch num
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -7,7 +7,7 @@ module BranchableCDNAssets
7
7
 
8
8
  def initialize source_file
9
9
  @source_file = source_file
10
- @file_set = File.exists?(source_file) ? read_source_file : Set.new
10
+ @file_set = File.exists?(source_file) ? read_source_file : Set.new
11
11
  end
12
12
 
13
13
  # list files set as an Array
@@ -15,17 +15,20 @@ module BranchableCDNAssets
15
15
  attr_reader :file_manager, :rake_namespace
16
16
 
17
17
  def initialize namespace, config
18
- @file_manager = FileManager.new config
18
+ @file_manager = FileManager.new config
19
19
  @rake_namespace = namespace
20
20
  end
21
21
 
22
22
  def tasks
23
23
  {
24
- list: 'list of local files',
24
+ list: 'list of local files',
25
25
  pull!: 'move the current branch\'s remote files to local',
26
26
  push!: 'move local files to the current branch\'s remote',
27
27
  prune!: 'remove local files with the same name as remote files',
28
- move_to_production: 'move named branch files to production cdn'
28
+ move_to_production: 'move named branch files to production cdn',
29
+ doctor: 'list file differences between remotes',
30
+ heal: 'pulls then pushes necessary files to make all remotes lists match',
31
+ setup: 'setup dir on remotes'
29
32
  }
30
33
  end
31
34
 
@@ -59,7 +62,7 @@ module BranchableCDNAssets
59
62
  def register_task task_name, task_desc
60
63
  in_namespace do
61
64
  desc task_desc
62
- task task_name.to_s.sub('!','') do
65
+ task task_name.to_s.sub('!', '') do
63
66
  puts file_manager.public_send( :with_check, task_name )
64
67
  end
65
68
  end
@@ -1,3 +1,3 @@
1
1
  module BranchableCDNAssets
2
- VERSION = "0.5.3"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -9,6 +9,13 @@ describe BranchableCDNAssets::FileManager do
9
9
  cdn_dir: 'cdn',
10
10
  host: 'host',
11
11
  root: 'root',
12
+ manage_cloudfront: true,
13
+ dir_permissions: 755,
14
+ file_permissions: 644,
15
+ rsync_flags: '-aviz',
16
+ remotes: [
17
+ BranchableCDNAssets::Config::Remote.new( 'host', 'root', 'url', 755, 644, '-aviz' )
18
+ ],
12
19
  cloudfront: {
13
20
  path_prefix: '/prefix'
14
21
  }
@@ -140,10 +147,16 @@ describe BranchableCDNAssets::FileManager do
140
147
  end
141
148
 
142
149
  it "returns list of pulled files" do
150
+ allow( BranchableCDNAssets::Shell ).to receive(:run_remote)
151
+ .with("find root -type f", hostname: "host")
152
+ .and_return(@success_response)
143
153
  expect( described_class.new(@config).pull! ).to match_array @file_list
144
154
  end
145
155
 
146
156
  it "removes files & updates manifest" do
157
+ allow( BranchableCDNAssets::Shell ).to receive(:run_remote)
158
+ .with("find root -type f", hostname: "host")
159
+ .and_return(@success_response)
147
160
  expect( @manifest ).to receive(:remove_files).with(@file_list)
148
161
  expect( @manifest ).to receive(:update_source_file!)
149
162
  described_class.new(@config).pull!
@@ -172,11 +185,15 @@ describe BranchableCDNAssets::FileManager do
172
185
 
173
186
  context "when setup_remote is successful" do
174
187
  before :each do
175
- allow( BranchableCDNAssets::Shell ).to receive(:run_remote).with('mkdir -p root', hostname: 'host').and_return(@success_response)
188
+ allow( BranchableCDNAssets::Shell ).to receive(:run_remote)
189
+ .with('mkdir -p -m 755 root', hostname: 'host')
190
+ .and_return(@success_response)
176
191
  end
177
192
 
178
193
  it "sets up remote" do
179
- expect( BranchableCDNAssets::Shell ).to receive(:run_remote).with('mkdir -p root', hostname: 'host').and_return(@success_response)
194
+ expect( BranchableCDNAssets::Shell ).to receive(:run_remote)
195
+ .with('mkdir -p -m 755 root', hostname: 'host')
196
+ .and_return(@success_response)
180
197
  described_class.new(@config).push!
181
198
  end
182
199
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: branchable_cdn_assets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Sloan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-27 00:00:00.000000000 Z
11
+ date: 2015-02-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -93,6 +93,7 @@ files:
93
93
  - lib/branchable_cdn_assets/config/environment_attribute_reader.rb
94
94
  - lib/branchable_cdn_assets/file_manager.rb
95
95
  - lib/branchable_cdn_assets/file_manager/checks.rb
96
+ - lib/branchable_cdn_assets/file_manager/permissions.rb
96
97
  - lib/branchable_cdn_assets/manifest.rb
97
98
  - lib/branchable_cdn_assets/rake_tasks.rb
98
99
  - lib/branchable_cdn_assets/shell.rb
@@ -128,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
129
  version: '0'
129
130
  requirements: []
130
131
  rubyforge_project:
131
- rubygems_version: 2.2.2
132
+ rubygems_version: 2.4.5
132
133
  signing_key:
133
134
  specification_version: 4
134
135
  summary: Helpers for syncing and finding assets accross multiple remotes