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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 311c5c378bf84cb75344dad471da0f2638381cf02bbc3fa0df7efb2334d4d2dc
4
- data.tar.gz: 3a105b308d61ea92afee0d7c2cc3c5dfe98a0a1038bb2bf1aa58f3c45eb30ce0
3
+ metadata.gz: a948a64341e82725be8159f090200f98fac9a935d96048740fc5ce971a84466a
4
+ data.tar.gz: 566e9c3db9d558b0161c7ec821b1e32388e4b0fe5d12a1896392af17a7827b5a
5
5
  SHA512:
6
- metadata.gz: ece6ab6b41a0afcf7d92a85e73407708451df2e3914031a39518d11c13e45df25625b5bc47fbc93a609920efb7ff9dd543adc29994b2a932770d844c60a80a96
7
- data.tar.gz: b90842fe6f7b7a6ef987a725cf06c7df0885991407b832cf0fbbcd9a8df4ebd5ee3a8b62df69257b2ce110609fb333fae264cda54fb1c078de010866c49c6979
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`.
@@ -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
- check_core_override_drift(findings)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AlFolioUpgrade
4
- VERSION = "1.0.2"
4
+ VERSION = "1.0.3"
5
5
  end
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.2
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: 1980-01-02 00:00:00.000000000 Z
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: 4.0.6
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: []