al_folio_upgrade 1.0.2 → 1.0.3
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/CHANGELOG.md +4 -0
- data/README.md +15 -0
- data/lib/al_folio_upgrade/cli.rb +293 -2
- data/lib/al_folio_upgrade/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a948a64341e82725be8159f090200f98fac9a935d96048740fc5ce971a84466a
|
|
4
|
+
data.tar.gz: 566e9c3db9d558b0161c7ec821b1e32388e4b0fe5d12a1896392af17a7827b5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69308f22a57519e24253b7e85f64275c9d27fa8eb43a30210e12a92d2ace66d615f1f7c3db0781a9f30367285cfe3ac0f635905c4dab4b98b10f37830c0ae0a7
|
|
7
|
+
data.tar.gz: 37879887dcbaa46cff34f17a8968f7335b800b47f4541712616422e97b7fb13f45e30317b94912493be1fb45966fd15016c37892c539482d2aa9dede9812e5c9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.3 - 2026-05-25
|
|
4
|
+
|
|
5
|
+
- Added local override audit/diff/acknowledgement commands so customized sites can detect when local copies shadow changed plugin-owned files.
|
|
6
|
+
|
|
3
7
|
## 1.0.2 - 2026-05-24
|
|
4
8
|
|
|
5
9
|
- Expanded upgrade audit coverage for legacy local plugins and runtime assets that are now owned by v1 feature gems.
|
data/README.md
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
- `al-folio upgrade audit`
|
|
8
8
|
- `al-folio upgrade apply --safe`
|
|
9
9
|
- `al-folio upgrade report`
|
|
10
|
+
- `al-folio upgrade overrides audit`
|
|
11
|
+
- `al-folio upgrade overrides diff LOCAL_PATH`
|
|
12
|
+
- `al-folio upgrade overrides accept [--all|LOCAL_PATH ...]`
|
|
10
13
|
|
|
11
14
|
## What it checks
|
|
12
15
|
|
|
@@ -16,8 +19,20 @@
|
|
|
16
19
|
- Distill remote-loader policy
|
|
17
20
|
- Local override drift when `theme: al_folio_core` is enabled
|
|
18
21
|
- Plugin-owned local asset drift (for example copied search, icon, Distill, citation, and external-post runtime files)
|
|
22
|
+
- Local files that shadow installed plugin-owned layouts, includes, Sass, templates, and assets
|
|
19
23
|
- Migration manifest availability from `al_folio_core`
|
|
20
24
|
|
|
25
|
+
## Local override workflow
|
|
26
|
+
|
|
27
|
+
Customized sites can keep local copies of plugin-owned files, but those copies do not produce Git merge conflicts when the owning gem changes. Use the overrides workflow after dependency updates:
|
|
28
|
+
|
|
29
|
+
1. Run `bundle exec al-folio upgrade overrides audit`.
|
|
30
|
+
2. For every stale or unacknowledged override, run `bundle exec al-folio upgrade overrides diff LOCAL_PATH`.
|
|
31
|
+
3. Reconcile the local file with the plugin-owned upstream file.
|
|
32
|
+
4. Run `bundle exec al-folio upgrade overrides accept LOCAL_PATH` or `bundle exec al-folio upgrade overrides accept --all`.
|
|
33
|
+
|
|
34
|
+
Acknowledgements are stored in `.al-folio-overrides.yml`. Commit that file in customized sites so future gem updates can flag upstream changes explicitly.
|
|
35
|
+
|
|
21
36
|
## Ecosystem context
|
|
22
37
|
|
|
23
38
|
- Starter execution/docs live in `al-folio`.
|
data/lib/al_folio_upgrade/cli.rb
CHANGED
|
@@ -4,6 +4,8 @@ require "optparse"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
require "yaml"
|
|
6
6
|
require "date"
|
|
7
|
+
require "digest"
|
|
8
|
+
require "English"
|
|
7
9
|
|
|
8
10
|
begin
|
|
9
11
|
require "al_folio_core"
|
|
@@ -14,8 +16,19 @@ end
|
|
|
14
16
|
module AlFolioUpgrade
|
|
15
17
|
class CLI
|
|
16
18
|
REPORT_PATH = "al-folio-upgrade-report.md"
|
|
19
|
+
OVERRIDE_ACK_PATH = ".al-folio-overrides.yml"
|
|
17
20
|
|
|
18
21
|
Finding = Struct.new(:id, :severity, :message, :file, :line, :snippet, keyword_init: true)
|
|
22
|
+
OverrideResult = Struct.new(
|
|
23
|
+
:local_path,
|
|
24
|
+
:plugin_path,
|
|
25
|
+
:owner,
|
|
26
|
+
:version,
|
|
27
|
+
:local_sha,
|
|
28
|
+
:upstream_sha,
|
|
29
|
+
:status,
|
|
30
|
+
keyword_init: true
|
|
31
|
+
)
|
|
19
32
|
|
|
20
33
|
FILE_GLOBS = [
|
|
21
34
|
"_config.yml",
|
|
@@ -129,6 +142,8 @@ module AlFolioUpgrade
|
|
|
129
142
|
write_report(findings)
|
|
130
143
|
print_summary(findings)
|
|
131
144
|
0
|
|
145
|
+
when "overrides"
|
|
146
|
+
run_overrides(argv)
|
|
132
147
|
else
|
|
133
148
|
@stderr.puts("Unsupported subcommand: #{subcommand.inspect}")
|
|
134
149
|
usage(1)
|
|
@@ -138,10 +153,53 @@ module AlFolioUpgrade
|
|
|
138
153
|
private
|
|
139
154
|
|
|
140
155
|
def usage(code)
|
|
141
|
-
@stdout.puts("Usage: al-folio upgrade [audit|apply --safe|report] [--no-fail]")
|
|
156
|
+
@stdout.puts("Usage: al-folio upgrade [audit|apply --safe|report|overrides] [--no-fail]")
|
|
157
|
+
@stdout.puts(" al-folio upgrade overrides audit [--fail-on-stale]")
|
|
158
|
+
@stdout.puts(" al-folio upgrade overrides diff LOCAL_PATH")
|
|
159
|
+
@stdout.puts(" al-folio upgrade overrides accept [--all|LOCAL_PATH ...]")
|
|
142
160
|
code
|
|
143
161
|
end
|
|
144
162
|
|
|
163
|
+
def run_overrides(argv)
|
|
164
|
+
command = argv.shift
|
|
165
|
+
case command
|
|
166
|
+
when "audit"
|
|
167
|
+
options = { fail_on_stale: false }
|
|
168
|
+
OptionParser.new do |opts|
|
|
169
|
+
opts.on("--fail-on-stale", "Exit non-zero when stale/unacknowledged overrides are found") do
|
|
170
|
+
options[:fail_on_stale] = true
|
|
171
|
+
end
|
|
172
|
+
end.parse!(argv)
|
|
173
|
+
|
|
174
|
+
results = local_override_results
|
|
175
|
+
print_override_audit(results)
|
|
176
|
+
return 1 if options[:fail_on_stale] && override_attention_required?(results)
|
|
177
|
+
|
|
178
|
+
0
|
|
179
|
+
when "diff"
|
|
180
|
+
local_path = argv.shift
|
|
181
|
+
if local_path.nil? || local_path.empty?
|
|
182
|
+
@stderr.puts("Usage: al-folio upgrade overrides diff LOCAL_PATH")
|
|
183
|
+
return 1
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
diff_override(local_path)
|
|
187
|
+
when "accept"
|
|
188
|
+
options = { all: false }
|
|
189
|
+
OptionParser.new do |opts|
|
|
190
|
+
opts.on("--all", "Acknowledge all detected local overrides") do
|
|
191
|
+
options[:all] = true
|
|
192
|
+
end
|
|
193
|
+
end.parse!(argv)
|
|
194
|
+
|
|
195
|
+
paths = options[:all] ? :all : argv
|
|
196
|
+
acknowledge_overrides(paths)
|
|
197
|
+
else
|
|
198
|
+
@stderr.puts("Unsupported overrides subcommand: #{command.inspect}")
|
|
199
|
+
usage(1)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
145
203
|
def blocking?(findings)
|
|
146
204
|
findings.any? { |finding| finding.severity == :blocking }
|
|
147
205
|
end
|
|
@@ -153,7 +211,7 @@ module AlFolioUpgrade
|
|
|
153
211
|
check_legacy_assets(findings)
|
|
154
212
|
check_legacy_patterns(findings)
|
|
155
213
|
check_distill_runtime(findings)
|
|
156
|
-
|
|
214
|
+
check_local_override_drift(findings)
|
|
157
215
|
check_plugin_owned_local_assets(findings)
|
|
158
216
|
findings
|
|
159
217
|
end
|
|
@@ -356,6 +414,239 @@ module AlFolioUpgrade
|
|
|
356
414
|
paths.select(&:file?).uniq
|
|
357
415
|
end
|
|
358
416
|
|
|
417
|
+
def check_local_override_drift(findings)
|
|
418
|
+
local_override_results.each do |override|
|
|
419
|
+
case override.status
|
|
420
|
+
when :identical
|
|
421
|
+
findings << Finding.new(
|
|
422
|
+
id: "local_override_identical",
|
|
423
|
+
severity: :warning,
|
|
424
|
+
message: "Local override is identical to `#{override.owner}` #{override.version}; remove it unless it is intentional.",
|
|
425
|
+
file: override.local_path,
|
|
426
|
+
line: 1,
|
|
427
|
+
snippet: "Matches #{override.owner}:#{override.plugin_path}."
|
|
428
|
+
)
|
|
429
|
+
when :unacknowledged
|
|
430
|
+
findings << Finding.new(
|
|
431
|
+
id: "local_override_unacknowledged",
|
|
432
|
+
severity: :warning,
|
|
433
|
+
message: "Local override shadows `#{override.owner}` #{override.version}; review and acknowledge it with `al-folio upgrade overrides accept #{override.local_path}`.",
|
|
434
|
+
file: override.local_path,
|
|
435
|
+
line: 1,
|
|
436
|
+
snippet: "Diff with `al-folio upgrade overrides diff #{override.local_path}`."
|
|
437
|
+
)
|
|
438
|
+
when :stale
|
|
439
|
+
findings << Finding.new(
|
|
440
|
+
id: "local_override_upstream_changed",
|
|
441
|
+
severity: :warning,
|
|
442
|
+
message: "`#{override.owner}` changed the upstream file since this local override was acknowledged.",
|
|
443
|
+
file: override.local_path,
|
|
444
|
+
line: 1,
|
|
445
|
+
snippet: "Reconcile with `al-folio upgrade overrides diff #{override.local_path}`."
|
|
446
|
+
)
|
|
447
|
+
when :local_changed
|
|
448
|
+
findings << Finding.new(
|
|
449
|
+
id: "local_override_changed_since_ack",
|
|
450
|
+
severity: :warning,
|
|
451
|
+
message: "Local override changed since it was last acknowledged.",
|
|
452
|
+
file: override.local_path,
|
|
453
|
+
line: 1,
|
|
454
|
+
snippet: "Review and re-acknowledge with `al-folio upgrade overrides accept #{override.local_path}`."
|
|
455
|
+
)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def local_override_results
|
|
461
|
+
acknowledgements = override_acknowledgements
|
|
462
|
+
override_candidates.map do |candidate|
|
|
463
|
+
local_sha = sha256(@root.join(candidate[:local_path]))
|
|
464
|
+
upstream_sha = sha256(candidate[:plugin_absolute_path])
|
|
465
|
+
ack = acknowledgements[candidate[:local_path]]
|
|
466
|
+
|
|
467
|
+
status = if local_sha == upstream_sha
|
|
468
|
+
:identical
|
|
469
|
+
elsif ack.nil?
|
|
470
|
+
:unacknowledged
|
|
471
|
+
elsif ack["upstream_sha256"] != upstream_sha
|
|
472
|
+
:stale
|
|
473
|
+
elsif ack["local_sha256"] != local_sha
|
|
474
|
+
:local_changed
|
|
475
|
+
else
|
|
476
|
+
:acknowledged
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
OverrideResult.new(
|
|
480
|
+
local_path: candidate[:local_path],
|
|
481
|
+
plugin_path: candidate[:plugin_path],
|
|
482
|
+
owner: candidate[:owner],
|
|
483
|
+
version: candidate[:version],
|
|
484
|
+
local_sha: local_sha,
|
|
485
|
+
upstream_sha: upstream_sha,
|
|
486
|
+
status: status
|
|
487
|
+
)
|
|
488
|
+
end.sort_by(&:local_path)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def print_override_audit(results)
|
|
492
|
+
if results.empty?
|
|
493
|
+
@stdout.puts("No local overrides shadowing installed al-folio plugin files were detected.")
|
|
494
|
+
return
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
@stdout.puts("Detected #{results.length} local override(s):")
|
|
498
|
+
results.each do |override|
|
|
499
|
+
@stdout.puts(
|
|
500
|
+
"- #{override.local_path}: #{override.status} " \
|
|
501
|
+
"(#{override.owner} #{override.version}, upstream #{override.plugin_path})"
|
|
502
|
+
)
|
|
503
|
+
end
|
|
504
|
+
@stdout.puts("Acknowledgement file: #{OVERRIDE_ACK_PATH}")
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def diff_override(local_path)
|
|
508
|
+
override = local_override_results.find { |result| result.local_path == normalize_relative_path(local_path) }
|
|
509
|
+
unless override
|
|
510
|
+
@stderr.puts("No plugin-owned override found for #{local_path}.")
|
|
511
|
+
return 1
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
candidate = override_candidates.find { |entry| entry[:local_path] == override.local_path }
|
|
515
|
+
system("diff", "-u", candidate[:plugin_absolute_path].to_s, @root.join(override.local_path).to_s)
|
|
516
|
+
$CHILD_STATUS&.exitstatus || 0
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def acknowledge_overrides(paths)
|
|
520
|
+
results = local_override_results
|
|
521
|
+
selected = if paths == :all
|
|
522
|
+
results
|
|
523
|
+
else
|
|
524
|
+
wanted = Array(paths).map { |path| normalize_relative_path(path) }
|
|
525
|
+
results.select { |result| wanted.include?(result.local_path) }
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
if selected.empty?
|
|
529
|
+
@stderr.puts("No matching local overrides to acknowledge.")
|
|
530
|
+
return 1
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
data = override_ack_file
|
|
534
|
+
data["version"] = 1
|
|
535
|
+
data["overrides"] ||= {}
|
|
536
|
+
|
|
537
|
+
selected.each do |override|
|
|
538
|
+
data["overrides"][override.local_path] = {
|
|
539
|
+
"owner" => override.owner,
|
|
540
|
+
"gem_version" => override.version,
|
|
541
|
+
"upstream_path" => override.plugin_path,
|
|
542
|
+
"upstream_sha256" => override.upstream_sha,
|
|
543
|
+
"local_sha256" => override.local_sha,
|
|
544
|
+
"acknowledged_at" => Date.today.iso8601,
|
|
545
|
+
}
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
sorted = data["overrides"].sort.to_h
|
|
549
|
+
data["overrides"] = sorted
|
|
550
|
+
File.write(@root.join(OVERRIDE_ACK_PATH), YAML.dump(data))
|
|
551
|
+
@stdout.puts("Acknowledged #{selected.length} local override(s) in #{OVERRIDE_ACK_PATH}.")
|
|
552
|
+
0
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def override_attention_required?(results)
|
|
556
|
+
results.any? { |result| %i[unacknowledged stale local_changed identical].include?(result.status) }
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def override_candidates
|
|
560
|
+
plugin_file_entries.each_with_object([]) do |entry, candidates|
|
|
561
|
+
local = @root.join(entry[:local_path])
|
|
562
|
+
next unless local.file?
|
|
563
|
+
|
|
564
|
+
candidates << entry
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def plugin_file_entries
|
|
569
|
+
entries = []
|
|
570
|
+
al_folio_plugin_specs.each do |spec|
|
|
571
|
+
root = Pathname.new(spec.full_gem_path)
|
|
572
|
+
next unless root.directory?
|
|
573
|
+
|
|
574
|
+
plugin_globs.each do |glob|
|
|
575
|
+
Dir.glob(root.join(glob)).sort.each do |path|
|
|
576
|
+
next unless File.file?(path)
|
|
577
|
+
|
|
578
|
+
plugin_path = Pathname.new(path).relative_path_from(root).to_s
|
|
579
|
+
local_path = local_override_path(plugin_path)
|
|
580
|
+
next unless local_path
|
|
581
|
+
|
|
582
|
+
entries << {
|
|
583
|
+
local_path: local_path,
|
|
584
|
+
plugin_path: plugin_path,
|
|
585
|
+
plugin_absolute_path: Pathname.new(path),
|
|
586
|
+
owner: spec.name,
|
|
587
|
+
version: spec.version.to_s,
|
|
588
|
+
}
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
entries.uniq { |entry| [entry[:owner], entry[:local_path], entry[:plugin_path]] }
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def plugin_globs
|
|
597
|
+
[
|
|
598
|
+
"_includes/**/*",
|
|
599
|
+
"_layouts/**/*",
|
|
600
|
+
"_sass/**/*",
|
|
601
|
+
"assets/**/*",
|
|
602
|
+
"templates/**/*",
|
|
603
|
+
"lib/assets/**/*",
|
|
604
|
+
"lib/templates/**/*",
|
|
605
|
+
]
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def local_override_path(plugin_path)
|
|
609
|
+
case plugin_path
|
|
610
|
+
when %r{\Atemplates/(.+)}
|
|
611
|
+
"_includes/#{Regexp.last_match(1)}"
|
|
612
|
+
when %r{\Alib/templates/(.+)}
|
|
613
|
+
"_includes/#{Regexp.last_match(1)}"
|
|
614
|
+
when %r{\Alib/assets/(.+)}
|
|
615
|
+
"assets/#{Regexp.last_match(1)}"
|
|
616
|
+
when %r{\A(?:_includes|_layouts|_sass|assets)/}
|
|
617
|
+
plugin_path
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def al_folio_plugin_specs
|
|
622
|
+
Gem::Specification.each.select do |spec|
|
|
623
|
+
spec.name.start_with?("al_") && File.directory?(spec.full_gem_path)
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def override_ack_file
|
|
628
|
+
path = @root.join(OVERRIDE_ACK_PATH)
|
|
629
|
+
return {} unless path.file?
|
|
630
|
+
|
|
631
|
+
parsed = parse_yaml(path.read) || {}
|
|
632
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
633
|
+
rescue StandardError
|
|
634
|
+
{}
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def override_acknowledgements
|
|
638
|
+
overrides = override_ack_file["overrides"]
|
|
639
|
+
overrides.is_a?(Hash) ? overrides.transform_keys(&:to_s) : {}
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def sha256(path)
|
|
643
|
+
Digest::SHA256.file(path).hexdigest
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def normalize_relative_path(path)
|
|
647
|
+
Pathname.new(path.to_s).cleanpath.to_s.sub(%r{\A\./}, "")
|
|
648
|
+
end
|
|
649
|
+
|
|
359
650
|
def check_core_override_drift(findings)
|
|
360
651
|
return unless using_core_theme?
|
|
361
652
|
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: al_folio_upgrade
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- al-folio maintainers
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-05-25 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: jekyll
|
|
@@ -119,6 +120,7 @@ metadata:
|
|
|
119
120
|
allowed_push_host: https://rubygems.org
|
|
120
121
|
homepage_uri: https://github.com/al-org-dev/al-folio-upgrade
|
|
121
122
|
source_code_uri: https://github.com/al-org-dev/al-folio-upgrade
|
|
123
|
+
post_install_message:
|
|
122
124
|
rdoc_options: []
|
|
123
125
|
require_paths:
|
|
124
126
|
- lib
|
|
@@ -133,7 +135,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
133
135
|
- !ruby/object:Gem::Version
|
|
134
136
|
version: '0'
|
|
135
137
|
requirements: []
|
|
136
|
-
rubygems_version:
|
|
138
|
+
rubygems_version: 3.0.3.1
|
|
139
|
+
signing_key:
|
|
137
140
|
specification_version: 4
|
|
138
141
|
summary: Upgrade tooling for al-folio v1.x
|
|
139
142
|
test_files: []
|