middleman-s3_sync 4.6.5 → 4.8.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +29 -0
- data/.github/workflows/release.yml +53 -0
- data/Changelog.md +54 -0
- data/README.md +108 -2
- data/lib/middleman/s3_sync/caching_policy.rb +6 -0
- data/lib/middleman/s3_sync/options.rb +12 -1
- data/lib/middleman/s3_sync/resource.rb +30 -11
- data/lib/middleman/s3_sync/version.rb +1 -1
- data/lib/middleman/s3_sync.rb +116 -0
- data/lib/middleman-s3_sync/commands.rb +3 -1
- data/lib/middleman-s3_sync/extension.rb +3 -0
- data/middleman-s3_sync.gemspec +21 -18
- data/spec/aws_sdk_parameters_spec.rb +96 -0
- data/spec/caching_policy_spec.rb +27 -2
- data/spec/resource_spec.rb +206 -0
- data/spec/s3_sync_integration_spec.rb +293 -4
- data/spec/spec_helper.rb +0 -1
- metadata +74 -74
|
@@ -15,11 +15,21 @@ describe 'S3Sync CloudFront Integration' do
|
|
|
15
15
|
dry_run: false,
|
|
16
16
|
verbose: false,
|
|
17
17
|
delete: true,
|
|
18
|
-
bucket: 'test-bucket'
|
|
18
|
+
bucket: 'test-bucket',
|
|
19
|
+
after_s3_sync: nil
|
|
19
20
|
)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
before do
|
|
24
|
+
# Reset cached app to avoid double leakage between tests
|
|
25
|
+
Middleman::S3Sync.instance_variable_set(:@app, nil)
|
|
26
|
+
|
|
27
|
+
# Mock sitemap for ensure_resource_list_updated! call
|
|
28
|
+
sitemap = double('sitemap')
|
|
29
|
+
allow(sitemap).to receive(:ensure_resource_list_updated!)
|
|
30
|
+
allow(app).to receive(:respond_to?).with(:sitemap).and_return(true)
|
|
31
|
+
allow(app).to receive(:sitemap).and_return(sitemap)
|
|
32
|
+
|
|
23
33
|
allow(::Middleman::Application).to receive(:new).and_return(app)
|
|
24
34
|
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(s3_sync_options)
|
|
25
35
|
allow(Middleman::S3Sync).to receive(:say_status)
|
|
@@ -70,7 +80,8 @@ describe 'S3Sync CloudFront Integration' do
|
|
|
70
80
|
cloudfront_invalidate: true,
|
|
71
81
|
cloudfront_distribution_id: 'E1234567890123',
|
|
72
82
|
cloudfront_invalidate_all: true,
|
|
73
|
-
bucket: 'test-bucket'
|
|
83
|
+
bucket: 'test-bucket',
|
|
84
|
+
after_s3_sync: nil
|
|
74
85
|
)
|
|
75
86
|
end
|
|
76
87
|
|
|
@@ -89,7 +100,8 @@ describe 'S3Sync CloudFront Integration' do
|
|
|
89
100
|
let(:s3_sync_options) do
|
|
90
101
|
double(
|
|
91
102
|
cloudfront_invalidate: false,
|
|
92
|
-
bucket: 'test-bucket'
|
|
103
|
+
bucket: 'test-bucket',
|
|
104
|
+
after_s3_sync: nil
|
|
93
105
|
)
|
|
94
106
|
end
|
|
95
107
|
|
|
@@ -101,6 +113,28 @@ describe 'S3Sync CloudFront Integration' do
|
|
|
101
113
|
end
|
|
102
114
|
end
|
|
103
115
|
|
|
116
|
+
describe 'sitemap population' do
|
|
117
|
+
it 'calls ensure_resource_list_updated! before processing resources' do
|
|
118
|
+
sitemap = double('sitemap')
|
|
119
|
+
expect(sitemap).to receive(:ensure_resource_list_updated!)
|
|
120
|
+
allow(sitemap).to receive(:respond_to?).with(:ensure_resource_list_updated!).and_return(true)
|
|
121
|
+
allow(app).to receive(:sitemap).and_return(sitemap)
|
|
122
|
+
|
|
123
|
+
Middleman::S3Sync.sync
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'handles apps without sitemap gracefully' do
|
|
127
|
+
Middleman::S3Sync.instance_variable_set(:@app, nil)
|
|
128
|
+
|
|
129
|
+
app_without_sitemap = double('app_without_sitemap')
|
|
130
|
+
allow(app_without_sitemap).to receive(:respond_to?).with(:sitemap).and_return(false)
|
|
131
|
+
allow(::Middleman::Application).to receive(:new).and_return(app_without_sitemap)
|
|
132
|
+
|
|
133
|
+
# Should not raise an error
|
|
134
|
+
expect { Middleman::S3Sync.sync }.not_to raise_error
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
104
138
|
describe 'path tracking during resource operations' do
|
|
105
139
|
before do
|
|
106
140
|
# Reset invalidation paths before each path tracking test
|
|
@@ -131,6 +165,128 @@ describe 'S3Sync CloudFront Integration' do
|
|
|
131
165
|
end
|
|
132
166
|
end
|
|
133
167
|
|
|
168
|
+
describe 'orphan file discovery (scan_build_dir)' do
|
|
169
|
+
let(:build_dir) { Dir.mktmpdir }
|
|
170
|
+
|
|
171
|
+
after do
|
|
172
|
+
FileUtils.remove_entry(build_dir) if File.directory?(build_dir)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
before do
|
|
176
|
+
# Reset state
|
|
177
|
+
Middleman::S3Sync.instance_variable_set(:@s3_sync_resources, {})
|
|
178
|
+
Middleman::S3Sync.instance_variable_set(:@bucket_files, {})
|
|
179
|
+
|
|
180
|
+
allow(Middleman::S3Sync).to receive(:say_status)
|
|
181
|
+
allow(Middleman::S3Sync).to receive(:build_dir).and_return(build_dir)
|
|
182
|
+
allow(Middleman::S3Sync).to receive(:remote_resource_for_path).and_return(nil)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
context 'when scan_build_dir is enabled' do
|
|
186
|
+
let(:s3_sync_options) do
|
|
187
|
+
double(
|
|
188
|
+
scan_build_dir: true,
|
|
189
|
+
build_dir: build_dir,
|
|
190
|
+
bucket: 'test-bucket',
|
|
191
|
+
prefix: nil,
|
|
192
|
+
delete: false,
|
|
193
|
+
verbose: false,
|
|
194
|
+
ignore_paths: [],
|
|
195
|
+
prefer_gzip: false,
|
|
196
|
+
force: false,
|
|
197
|
+
acl: 'public-read',
|
|
198
|
+
after_s3_sync: nil
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'discovers files not in sitemap' do
|
|
203
|
+
# Create orphan files in build directory
|
|
204
|
+
FileUtils.mkdir_p(File.join(build_dir, 'images'))
|
|
205
|
+
File.write(File.join(build_dir, 'orphan.txt'), 'orphan content')
|
|
206
|
+
File.write(File.join(build_dir, 'images', 'optimized.webp'), 'image data')
|
|
207
|
+
|
|
208
|
+
# Mock Resource creation to avoid S3 calls
|
|
209
|
+
allow(Middleman::S3Sync::Resource).to receive(:new) do |resource, remote, path: nil|
|
|
210
|
+
mock_resource = double('resource', status: :new, path: path)
|
|
211
|
+
allow(mock_resource).to receive(:tap).and_yield(mock_resource).and_return(mock_resource)
|
|
212
|
+
mock_resource
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
Middleman::S3Sync.send(:discover_orphan_files)
|
|
216
|
+
|
|
217
|
+
resources = Middleman::S3Sync.send(:s3_sync_resources)
|
|
218
|
+
expect(resources.keys).to include('orphan.txt')
|
|
219
|
+
expect(resources.keys).to include('images/optimized.webp')
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'skips files already in sitemap' do
|
|
223
|
+
File.write(File.join(build_dir, 'existing.html'), 'content')
|
|
224
|
+
|
|
225
|
+
# Pre-populate sitemap resource
|
|
226
|
+
Middleman::S3Sync.send(:s3_sync_resources)['existing.html'] = double('resource')
|
|
227
|
+
|
|
228
|
+
# Count how many times Resource.new is called
|
|
229
|
+
call_count = 0
|
|
230
|
+
allow(Middleman::S3Sync::Resource).to receive(:new) do |resource, remote, path: nil|
|
|
231
|
+
call_count += 1
|
|
232
|
+
mock_resource = double('resource', status: :new, path: path)
|
|
233
|
+
allow(mock_resource).to receive(:tap).and_yield(mock_resource).and_return(mock_resource)
|
|
234
|
+
mock_resource
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
Middleman::S3Sync.send(:discover_orphan_files)
|
|
238
|
+
|
|
239
|
+
# Should not have created a new resource for existing.html
|
|
240
|
+
expect(call_count).to eq(0)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'skips directories' do
|
|
244
|
+
FileUtils.mkdir_p(File.join(build_dir, 'subdir', 'nested'))
|
|
245
|
+
File.write(File.join(build_dir, 'subdir', 'file.txt'), 'content')
|
|
246
|
+
|
|
247
|
+
created_paths = []
|
|
248
|
+
allow(Middleman::S3Sync::Resource).to receive(:new) do |resource, remote, path: nil|
|
|
249
|
+
created_paths << path
|
|
250
|
+
mock_resource = double('resource', status: :new, path: path)
|
|
251
|
+
allow(mock_resource).to receive(:tap).and_yield(mock_resource).and_return(mock_resource)
|
|
252
|
+
mock_resource
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
Middleman::S3Sync.send(:discover_orphan_files)
|
|
256
|
+
|
|
257
|
+
expect(created_paths).to include('subdir/file.txt')
|
|
258
|
+
expect(created_paths).not_to include('subdir')
|
|
259
|
+
expect(created_paths).not_to include('subdir/nested')
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
context 'when scan_build_dir is disabled' do
|
|
264
|
+
let(:s3_sync_options) do
|
|
265
|
+
double(
|
|
266
|
+
scan_build_dir: false,
|
|
267
|
+
build_dir: build_dir,
|
|
268
|
+
bucket: 'test-bucket',
|
|
269
|
+
after_s3_sync: nil
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
it 'does not scan for orphan files' do
|
|
274
|
+
File.write(File.join(build_dir, 'orphan.txt'), 'content')
|
|
275
|
+
|
|
276
|
+
expect(Middleman::S3Sync).not_to receive(:discover_orphan_files)
|
|
277
|
+
|
|
278
|
+
# Call work_to_be_done? but mock the heavy parts
|
|
279
|
+
allow(Middleman::S3Sync).to receive(:mm_resources).and_return([])
|
|
280
|
+
allow(Middleman::S3Sync).to receive(:remote_only_paths).and_return([])
|
|
281
|
+
allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
|
|
282
|
+
allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
|
|
283
|
+
allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
|
|
284
|
+
|
|
285
|
+
Middleman::S3Sync.send(:work_to_be_done?)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
134
290
|
describe 'batch delete operations' do
|
|
135
291
|
let(:bucket) { double('bucket') }
|
|
136
292
|
let(:resource1) { double('resource1', path: 'file1.html', remote_path: 'file1.html') }
|
|
@@ -177,7 +333,8 @@ describe 'S3Sync CloudFront Integration' do
|
|
|
177
333
|
dry_run_options = double(
|
|
178
334
|
dry_run: true,
|
|
179
335
|
delete: true,
|
|
180
|
-
bucket: 'test-bucket'
|
|
336
|
+
bucket: 'test-bucket',
|
|
337
|
+
after_s3_sync: nil
|
|
181
338
|
)
|
|
182
339
|
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(dry_run_options)
|
|
183
340
|
allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([resource1])
|
|
@@ -195,4 +352,136 @@ describe 'S3Sync CloudFront Integration' do
|
|
|
195
352
|
Middleman::S3Sync.send(:delete_resources)
|
|
196
353
|
end
|
|
197
354
|
end
|
|
355
|
+
|
|
356
|
+
describe 'after_s3_sync callback' do
|
|
357
|
+
before do
|
|
358
|
+
Middleman::S3Sync.instance_variable_set(:@app, nil)
|
|
359
|
+
|
|
360
|
+
sitemap = double('sitemap')
|
|
361
|
+
allow(sitemap).to receive(:ensure_resource_list_updated!)
|
|
362
|
+
allow(app).to receive(:respond_to?).with(:sitemap).and_return(true)
|
|
363
|
+
allow(app).to receive(:sitemap).and_return(sitemap)
|
|
364
|
+
|
|
365
|
+
allow(::Middleman::Application).to receive(:new).and_return(app)
|
|
366
|
+
allow(Middleman::S3Sync).to receive(:say_status)
|
|
367
|
+
allow(Middleman::S3Sync).to receive(:work_to_be_done?).and_return(true)
|
|
368
|
+
allow(Middleman::S3Sync).to receive(:update_bucket_versioning)
|
|
369
|
+
allow(Middleman::S3Sync).to receive(:update_bucket_website)
|
|
370
|
+
allow(Middleman::S3Sync).to receive(:ignore_resources)
|
|
371
|
+
allow(Middleman::S3Sync).to receive(:create_resources)
|
|
372
|
+
allow(Middleman::S3Sync).to receive(:update_resources)
|
|
373
|
+
allow(Middleman::S3Sync).to receive(:delete_resources)
|
|
374
|
+
allow(Middleman::S3Sync::CloudFront).to receive(:invalidate)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
context 'when after_s3_sync callback is provided' do
|
|
378
|
+
it 'executes the callback after sync completes' do
|
|
379
|
+
callback_executed = false
|
|
380
|
+
callback_options = double(
|
|
381
|
+
cloudfront_invalidate: false,
|
|
382
|
+
bucket: 'test-bucket',
|
|
383
|
+
after_s3_sync: -> { callback_executed = true }
|
|
384
|
+
)
|
|
385
|
+
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(callback_options)
|
|
386
|
+
allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
|
|
387
|
+
allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
|
|
388
|
+
allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
|
|
389
|
+
|
|
390
|
+
Middleman::S3Sync.sync
|
|
391
|
+
|
|
392
|
+
expect(callback_executed).to be true
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
it 'passes sync results to the callback' do
|
|
396
|
+
received_results = nil
|
|
397
|
+
callback_options = double(
|
|
398
|
+
cloudfront_invalidate: false,
|
|
399
|
+
bucket: 'test-bucket',
|
|
400
|
+
after_s3_sync: ->(results) { received_results = results }
|
|
401
|
+
)
|
|
402
|
+
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(callback_options)
|
|
403
|
+
allow(Middleman::S3Sync).to receive(:files_to_create).and_return(['file1.html'])
|
|
404
|
+
allow(Middleman::S3Sync).to receive(:files_to_update).and_return(['file2.html', 'file3.html'])
|
|
405
|
+
allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
|
|
406
|
+
|
|
407
|
+
Middleman::S3Sync.sync
|
|
408
|
+
|
|
409
|
+
expect(received_results).to be_a(Hash)
|
|
410
|
+
expect(received_results[:created]).to eq(1)
|
|
411
|
+
expect(received_results[:updated]).to eq(2)
|
|
412
|
+
expect(received_results[:deleted]).to eq(0)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it 'logs callback execution status' do
|
|
416
|
+
callback_options = double(
|
|
417
|
+
cloudfront_invalidate: false,
|
|
418
|
+
bucket: 'test-bucket',
|
|
419
|
+
after_s3_sync: -> { 'done' }
|
|
420
|
+
)
|
|
421
|
+
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(callback_options)
|
|
422
|
+
allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
|
|
423
|
+
allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
|
|
424
|
+
allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
|
|
425
|
+
|
|
426
|
+
expect(Middleman::S3Sync).to receive(:say_status).with('callback', /Running after_s3_sync/)
|
|
427
|
+
expect(Middleman::S3Sync).to receive(:say_status).with('callback', /after_s3_sync completed/)
|
|
428
|
+
|
|
429
|
+
Middleman::S3Sync.sync
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
it 'handles callback errors gracefully' do
|
|
433
|
+
error_callback_options = double(
|
|
434
|
+
cloudfront_invalidate: false,
|
|
435
|
+
bucket: 'test-bucket',
|
|
436
|
+
after_s3_sync: ->(_results) { raise 'Callback error!' }
|
|
437
|
+
)
|
|
438
|
+
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(error_callback_options)
|
|
439
|
+
allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
|
|
440
|
+
allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
|
|
441
|
+
allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
|
|
442
|
+
|
|
443
|
+
expect(Middleman::S3Sync).to receive(:say_status).with('error', /Callback error!/)
|
|
444
|
+
expect { Middleman::S3Sync.sync }.not_to raise_error
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
context 'when after_s3_sync callback is nil' do
|
|
449
|
+
it 'does not attempt to execute a callback' do
|
|
450
|
+
nil_callback_options = double(
|
|
451
|
+
cloudfront_invalidate: false,
|
|
452
|
+
bucket: 'test-bucket',
|
|
453
|
+
after_s3_sync: nil
|
|
454
|
+
)
|
|
455
|
+
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(nil_callback_options)
|
|
456
|
+
|
|
457
|
+
# Should not log callback execution
|
|
458
|
+
expect(Middleman::S3Sync).not_to receive(:say_status).with('callback', anything)
|
|
459
|
+
|
|
460
|
+
Middleman::S3Sync.sync
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
context 'when after_s3_sync callback is a method name' do
|
|
465
|
+
it 'executes the method on the app' do
|
|
466
|
+
method_executed = false
|
|
467
|
+
allow(app).to receive(:my_callback) { method_executed = true }
|
|
468
|
+
allow(app).to receive(:respond_to?).with(:my_callback).and_return(true)
|
|
469
|
+
allow(app).to receive(:method).with(:my_callback).and_return(double(arity: 0))
|
|
470
|
+
|
|
471
|
+
method_callback_options = double(
|
|
472
|
+
cloudfront_invalidate: false,
|
|
473
|
+
bucket: 'test-bucket',
|
|
474
|
+
after_s3_sync: :my_callback
|
|
475
|
+
)
|
|
476
|
+
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(method_callback_options)
|
|
477
|
+
allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
|
|
478
|
+
allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
|
|
479
|
+
allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
|
|
480
|
+
|
|
481
|
+
Middleman::S3Sync.sync
|
|
482
|
+
|
|
483
|
+
expect(method_executed).to be true
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
198
487
|
end
|