al_folio_upgrade 1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE +20 -0
- data/README.md +17 -0
- data/exe/al-folio +6 -0
- data/lib/al_folio_upgrade/cli.rb +534 -0
- data/lib/al_folio_upgrade/version.rb +5 -0
- data/lib/al_folio_upgrade.rb +7 -0
- metadata +142 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 91910554f7cb55cf4d0183137c940f89c90770981a1094e471b51cd034044fc9
|
|
4
|
+
data.tar.gz: 5c4d24dbbec94b1169f39955a0c33986d43957ac6a9b995f1f324594fca342c8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a2dfa76729a1c268b5d7f0215721833e0caf8112e6e9bcf0550ed79532c1c7889e46ec215ea2a67f85dd723cc87f0acd5eae847f2b5de08a9cbcad3eab3c3e72
|
|
7
|
+
data.tar.gz: 84bd06b00efebb6668cb4992e58f7b2d67a310c120c3d0462700096d1e67392cb12366e9ce2cd1f9a9200438fa97f312cc2d014dcb9e0eba9d24865d5bf8974c
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Maruan Al-Shedivat.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Al-Folio Upgrade
|
|
2
|
+
|
|
3
|
+
Upgrade CLI for al-folio v1.x.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `al-folio upgrade audit`
|
|
8
|
+
- `al-folio upgrade apply --safe`
|
|
9
|
+
- `al-folio upgrade report`
|
|
10
|
+
|
|
11
|
+
## What it checks
|
|
12
|
+
|
|
13
|
+
- Core config contract (`al_folio.*`, Tailwind, Distill)
|
|
14
|
+
- Legacy Bootstrap/jQuery markers
|
|
15
|
+
- Distill remote-loader policy
|
|
16
|
+
- Local override drift when `theme: al_folio_core` is enabled
|
|
17
|
+
- Migration manifest availability from `al_folio_core`
|
data/exe/al-folio
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "date"
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
require "al_folio_core"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
# Optional at runtime; fallback lookup paths are used if unavailable.
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module AlFolioUpgrade
|
|
15
|
+
class CLI
|
|
16
|
+
REPORT_PATH = "al-folio-upgrade-report.md"
|
|
17
|
+
|
|
18
|
+
Finding = Struct.new(:id, :severity, :message, :file, :line, :snippet, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
FILE_GLOBS = [
|
|
21
|
+
"_config.yml",
|
|
22
|
+
"_includes/**/*.{liquid,html}",
|
|
23
|
+
"_layouts/**/*.{liquid,html}",
|
|
24
|
+
"_pages/**/*.{md,markdown,liquid,html}",
|
|
25
|
+
"_posts/**/*.{md,markdown,liquid,html}",
|
|
26
|
+
"assets/js/**/*.js",
|
|
27
|
+
"assets/css/**/*.css",
|
|
28
|
+
"assets/tailwind/**/*.css",
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
IGNORE_PATH_PATTERNS = [
|
|
32
|
+
/\/distillpub\//,
|
|
33
|
+
/\/search\/ninja-footer\.min\.js$/,
|
|
34
|
+
/\/bootstrap\.bundle\.min\.js$/,
|
|
35
|
+
/\/bootstrap-toc\.min\.js$/,
|
|
36
|
+
/\.min\.js$/,
|
|
37
|
+
/\.map$/,
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
SAFE_REPLACEMENTS = [
|
|
41
|
+
{ from: /\bfont-weight-bold\b/, to: "font-bold" },
|
|
42
|
+
{ from: /\bfont-weight-medium\b/, to: "font-medium" },
|
|
43
|
+
{ from: /\bfont-weight-lighter\b/, to: "font-light" },
|
|
44
|
+
{ from: %r{https://distill\.pub/template\.v2\.js}, to: "/assets/js/distillpub/template.v2.js" },
|
|
45
|
+
{ from: %r{assets/tailwind/input\.css}, to: "assets/tailwind/app.css" },
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
CORE_OVERRIDE_FILES = %w[
|
|
49
|
+
_includes/head.liquid
|
|
50
|
+
_includes/scripts.liquid
|
|
51
|
+
_layouts/default.liquid
|
|
52
|
+
_layouts/post.liquid
|
|
53
|
+
_layouts/page.liquid
|
|
54
|
+
_layouts/distill.liquid
|
|
55
|
+
assets/js/common.js
|
|
56
|
+
assets/js/theme.js
|
|
57
|
+
assets/js/tooltips-setup.js
|
|
58
|
+
assets/tailwind/app.css
|
|
59
|
+
tailwind.config.js
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
def initialize(root: Dir.pwd, stdout: $stdout, stderr: $stderr)
|
|
63
|
+
@root = Pathname.new(root)
|
|
64
|
+
@stdout = stdout
|
|
65
|
+
@stderr = stderr
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def run(argv)
|
|
69
|
+
return usage(1) if argv.empty?
|
|
70
|
+
|
|
71
|
+
command = argv.shift
|
|
72
|
+
unless command == "upgrade"
|
|
73
|
+
@stderr.puts("Unsupported command: #{command}")
|
|
74
|
+
return usage(1)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
subcommand = argv.shift
|
|
78
|
+
case subcommand
|
|
79
|
+
when "audit"
|
|
80
|
+
options = { fail_on_blocking: true }
|
|
81
|
+
OptionParser.new do |opts|
|
|
82
|
+
opts.on("--no-fail", "Do not fail even when blocking findings exist") do
|
|
83
|
+
options[:fail_on_blocking] = false
|
|
84
|
+
end
|
|
85
|
+
end.parse!(argv)
|
|
86
|
+
|
|
87
|
+
findings = audit
|
|
88
|
+
write_report(findings)
|
|
89
|
+
print_summary(findings)
|
|
90
|
+
return 1 if options[:fail_on_blocking] && blocking?(findings)
|
|
91
|
+
|
|
92
|
+
0
|
|
93
|
+
when "apply"
|
|
94
|
+
options = { safe: false }
|
|
95
|
+
OptionParser.new do |opts|
|
|
96
|
+
opts.on("--safe", "Apply only deterministic safe codemods") do
|
|
97
|
+
options[:safe] = true
|
|
98
|
+
end
|
|
99
|
+
end.parse!(argv)
|
|
100
|
+
|
|
101
|
+
unless options[:safe]
|
|
102
|
+
@stderr.puts("Only --safe mode is supported in v1.x.")
|
|
103
|
+
return 1
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
changed_files = apply_safe_codemods
|
|
107
|
+
findings = audit
|
|
108
|
+
write_report(findings)
|
|
109
|
+
@stdout.puts("Applied safe codemods to #{changed_files} file(s).")
|
|
110
|
+
print_summary(findings)
|
|
111
|
+
0
|
|
112
|
+
when "report"
|
|
113
|
+
findings = audit
|
|
114
|
+
write_report(findings)
|
|
115
|
+
print_summary(findings)
|
|
116
|
+
0
|
|
117
|
+
else
|
|
118
|
+
@stderr.puts("Unsupported subcommand: #{subcommand.inspect}")
|
|
119
|
+
usage(1)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def usage(code)
|
|
126
|
+
@stdout.puts("Usage: al-folio upgrade [audit|apply --safe|report] [--no-fail]")
|
|
127
|
+
code
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def blocking?(findings)
|
|
131
|
+
findings.any? { |finding| finding.severity == :blocking }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def audit
|
|
135
|
+
findings = []
|
|
136
|
+
check_manifest_contract(findings)
|
|
137
|
+
check_config_contract(findings)
|
|
138
|
+
check_legacy_assets(findings)
|
|
139
|
+
check_legacy_patterns(findings)
|
|
140
|
+
check_distill_runtime(findings)
|
|
141
|
+
check_core_override_drift(findings)
|
|
142
|
+
findings
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def check_manifest_contract(findings)
|
|
146
|
+
manifests = manifest_paths
|
|
147
|
+
if manifests.empty?
|
|
148
|
+
findings << Finding.new(
|
|
149
|
+
id: "missing_migration_manifests",
|
|
150
|
+
severity: :warning,
|
|
151
|
+
message: "No migration manifests found. Install/update `al_folio_core` to get release contracts.",
|
|
152
|
+
file: "migrations/",
|
|
153
|
+
line: 1,
|
|
154
|
+
snippet: "Expected at least one `x.y.z_to_a.b.c.yml` manifest."
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def check_config_contract(findings)
|
|
160
|
+
config_path = @root.join("_config.yml")
|
|
161
|
+
return unless config_path.file?
|
|
162
|
+
|
|
163
|
+
content = config_path.read
|
|
164
|
+
parsed = begin
|
|
165
|
+
parse_yaml(content) || {}
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
findings << Finding.new(
|
|
168
|
+
id: "invalid_config_yaml",
|
|
169
|
+
severity: :blocking,
|
|
170
|
+
message: "_config.yml could not be parsed: #{e.message}",
|
|
171
|
+
file: "_config.yml",
|
|
172
|
+
line: 1,
|
|
173
|
+
snippet: "Fix YAML syntax before running upgrade codemods."
|
|
174
|
+
)
|
|
175
|
+
return
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
al_folio = parsed.is_a?(Hash) ? parsed["al_folio"] : nil
|
|
179
|
+
unless al_folio.is_a?(Hash)
|
|
180
|
+
findings << Finding.new(
|
|
181
|
+
id: "missing_al_folio_namespace",
|
|
182
|
+
severity: :blocking,
|
|
183
|
+
message: "Missing `al_folio` config namespace required for v1.x.",
|
|
184
|
+
file: "_config.yml",
|
|
185
|
+
line: 1,
|
|
186
|
+
snippet: "Add al_folio.api_version, style_engine, compat, and upgrade keys."
|
|
187
|
+
)
|
|
188
|
+
return
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
unless al_folio["style_engine"] == "tailwind"
|
|
192
|
+
findings << Finding.new(
|
|
193
|
+
id: "style_engine_not_tailwind",
|
|
194
|
+
severity: :blocking,
|
|
195
|
+
message: "`al_folio.style_engine` should be set to `tailwind` for v1.x.",
|
|
196
|
+
file: "_config.yml",
|
|
197
|
+
line: 1,
|
|
198
|
+
snippet: "Set al_folio.style_engine: tailwind"
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
unless al_folio["tailwind"].is_a?(Hash)
|
|
203
|
+
findings << Finding.new(
|
|
204
|
+
id: "missing_tailwind_namespace",
|
|
205
|
+
severity: :warning,
|
|
206
|
+
message: "Missing `al_folio.tailwind` namespace for v1 tailwind runtime contract.",
|
|
207
|
+
file: "_config.yml",
|
|
208
|
+
line: 1,
|
|
209
|
+
snippet: "Add al_folio.tailwind.version/preflight/css_entry."
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
unless al_folio["distill"].is_a?(Hash)
|
|
214
|
+
findings << Finding.new(
|
|
215
|
+
id: "missing_distill_namespace",
|
|
216
|
+
severity: :warning,
|
|
217
|
+
message: "Missing `al_folio.distill` namespace for Distill runtime contract.",
|
|
218
|
+
file: "_config.yml",
|
|
219
|
+
line: 1,
|
|
220
|
+
snippet: "Add al_folio.distill.engine/source/allow_remote_loader."
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def check_legacy_assets(findings)
|
|
226
|
+
files = ["_includes/head.liquid", "_includes/scripts.liquid"]
|
|
227
|
+
patterns = [
|
|
228
|
+
/bootstrap\.min\.css/,
|
|
229
|
+
/mdbootstrap|mdb\.min\.(?:css|js)/,
|
|
230
|
+
/third_party_libraries\.jquery/,
|
|
231
|
+
/bootstrap\.bundle\.min\.js/,
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
files.each do |file|
|
|
235
|
+
path = @root.join(file)
|
|
236
|
+
next unless path.file?
|
|
237
|
+
|
|
238
|
+
path.each_line.with_index(1) do |line, number|
|
|
239
|
+
next unless patterns.any? { |pattern| line.match?(pattern) }
|
|
240
|
+
|
|
241
|
+
findings << Finding.new(
|
|
242
|
+
id: "legacy_bootstrap_runtime_asset",
|
|
243
|
+
severity: :blocking,
|
|
244
|
+
message: "Legacy Bootstrap/jQuery/MDB runtime assets are still referenced in core includes.",
|
|
245
|
+
file: file,
|
|
246
|
+
line: number,
|
|
247
|
+
snippet: line.strip
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def check_legacy_patterns(findings)
|
|
254
|
+
each_candidate_file do |relative, line, number|
|
|
255
|
+
if line.match?(/data-toggle\s*=\s*["'](?:collapse|dropdown|tooltip|popover|table)["']/)
|
|
256
|
+
findings << Finding.new(
|
|
257
|
+
id: "legacy_data_toggle",
|
|
258
|
+
severity: :warning,
|
|
259
|
+
message: "Legacy Bootstrap `data-toggle` marker found.",
|
|
260
|
+
file: relative,
|
|
261
|
+
line: number,
|
|
262
|
+
snippet: line.strip
|
|
263
|
+
)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
if line.match?(/\$\(|jQuery\b/)
|
|
267
|
+
findings << Finding.new(
|
|
268
|
+
id: "legacy_jquery_usage",
|
|
269
|
+
severity: :warning,
|
|
270
|
+
message: "jQuery usage found; migrate to vanilla JS APIs.",
|
|
271
|
+
file: relative,
|
|
272
|
+
line: number,
|
|
273
|
+
snippet: line.strip
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def check_distill_runtime(findings)
|
|
280
|
+
config_path = @root.join("_config.yml")
|
|
281
|
+
allow_remote_loader = false
|
|
282
|
+
if config_path.file?
|
|
283
|
+
begin
|
|
284
|
+
parsed = parse_yaml(config_path.read) || {}
|
|
285
|
+
allow_remote_loader = parsed.dig("al_folio", "distill", "allow_remote_loader") == true
|
|
286
|
+
rescue StandardError
|
|
287
|
+
allow_remote_loader = false
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
return if allow_remote_loader
|
|
292
|
+
|
|
293
|
+
distill_runtime_paths.each do |transforms_path|
|
|
294
|
+
report_file = if transforms_path.to_s.start_with?("#{@root}#{File::SEPARATOR}")
|
|
295
|
+
transforms_path.relative_path_from(@root).to_s
|
|
296
|
+
else
|
|
297
|
+
"al_folio_distill:#{transforms_path}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
transforms_path.each_line.with_index(1) do |line, number|
|
|
301
|
+
next unless line.match?(%r{https://distill\.pub/template\.v2\.js})
|
|
302
|
+
|
|
303
|
+
findings << Finding.new(
|
|
304
|
+
id: "distill_remote_loader_enabled",
|
|
305
|
+
severity: :blocking,
|
|
306
|
+
message: "Distill runtime still references remote template loader while allow_remote_loader is false.",
|
|
307
|
+
file: report_file,
|
|
308
|
+
line: number,
|
|
309
|
+
snippet: line.strip
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def distill_runtime_paths
|
|
316
|
+
paths = [@root.join("assets/js/distillpub/transforms.v2.js")]
|
|
317
|
+
specs = []
|
|
318
|
+
specs << Gem.loaded_specs["al_folio_distill"] if Gem.loaded_specs.key?("al_folio_distill")
|
|
319
|
+
begin
|
|
320
|
+
specs << Gem::Specification.find_by_name("al_folio_distill")
|
|
321
|
+
rescue Gem::LoadError
|
|
322
|
+
# Optional gem; ignore when not installed.
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
specs.compact.uniq(&:full_gem_path).each do |spec|
|
|
326
|
+
paths << Pathname.new(File.join(spec.full_gem_path, "assets/js/distillpub/transforms.v2.js"))
|
|
327
|
+
end
|
|
328
|
+
paths.select(&:file?).uniq
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def check_core_override_drift(findings)
|
|
332
|
+
return unless using_core_theme?
|
|
333
|
+
|
|
334
|
+
CORE_OVERRIDE_FILES.each do |relative|
|
|
335
|
+
path = @root.join(relative)
|
|
336
|
+
next unless path.file?
|
|
337
|
+
|
|
338
|
+
findings << Finding.new(
|
|
339
|
+
id: "core_override_drift",
|
|
340
|
+
severity: :warning,
|
|
341
|
+
message: "Local override shadows `al_folio_core` theme file and may need manual review during upgrades.",
|
|
342
|
+
file: relative,
|
|
343
|
+
line: 1,
|
|
344
|
+
snippet: "Local override present."
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def each_candidate_file
|
|
350
|
+
FILE_GLOBS.each do |glob|
|
|
351
|
+
Dir.glob(@root.join(glob)).sort.each do |path|
|
|
352
|
+
next unless File.file?(path)
|
|
353
|
+
next if ignored_path?(path)
|
|
354
|
+
|
|
355
|
+
rel = Pathname.new(path).relative_path_from(@root).to_s
|
|
356
|
+
File.foreach(path).with_index(1) do |line, number|
|
|
357
|
+
yield rel, line, number
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def apply_safe_codemods
|
|
364
|
+
changed_files = 0
|
|
365
|
+
|
|
366
|
+
each_text_file do |path|
|
|
367
|
+
original = File.read(path)
|
|
368
|
+
updated = original.dup
|
|
369
|
+
|
|
370
|
+
SAFE_REPLACEMENTS.each do |rule|
|
|
371
|
+
updated = updated.gsub(rule[:from], rule[:to])
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
if Pathname.new(path).relative_path_from(@root).to_s == "_config.yml"
|
|
375
|
+
updated = ensure_al_folio_namespace(updated)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
next if updated == original
|
|
379
|
+
|
|
380
|
+
File.write(path, updated)
|
|
381
|
+
changed_files += 1
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
changed_files
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def ensure_al_folio_namespace(content)
|
|
388
|
+
if content.match?(/^al_folio:\s*$/)
|
|
389
|
+
content = ensure_tailwind_namespace(content)
|
|
390
|
+
content = ensure_distill_namespace(content)
|
|
391
|
+
return content
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
block = <<~YAML
|
|
395
|
+
|
|
396
|
+
al_folio:
|
|
397
|
+
api_version: 1
|
|
398
|
+
style_engine: tailwind
|
|
399
|
+
tailwind:
|
|
400
|
+
version: 4.1.18
|
|
401
|
+
preflight: false
|
|
402
|
+
css_entry: assets/tailwind/app.css
|
|
403
|
+
distill:
|
|
404
|
+
engine: distillpub-template
|
|
405
|
+
source: al-org-dev/distill-template#al-folio
|
|
406
|
+
allow_remote_loader: true
|
|
407
|
+
compat:
|
|
408
|
+
bootstrap:
|
|
409
|
+
enabled: false
|
|
410
|
+
support_window: v1.0-v1.2
|
|
411
|
+
deprecates_in: v1.3
|
|
412
|
+
removed_in: v2.0
|
|
413
|
+
upgrade:
|
|
414
|
+
channel: stable
|
|
415
|
+
auto_apply_safe_fixes: false
|
|
416
|
+
YAML
|
|
417
|
+
content + block
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def ensure_tailwind_namespace(content)
|
|
421
|
+
return content if nested_namespace_present?(content, "tailwind")
|
|
422
|
+
|
|
423
|
+
insertion = [
|
|
424
|
+
" tailwind:",
|
|
425
|
+
" version: 4.1.18",
|
|
426
|
+
" preflight: false",
|
|
427
|
+
" css_entry: assets/tailwind/app.css",
|
|
428
|
+
].join("\n")
|
|
429
|
+
content.sub(/^al_folio:\s*$/) { |match| "#{match}\n#{insertion}" }
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def ensure_distill_namespace(content)
|
|
433
|
+
return content if nested_namespace_present?(content, "distill")
|
|
434
|
+
|
|
435
|
+
insertion = [
|
|
436
|
+
" distill:",
|
|
437
|
+
" engine: distillpub-template",
|
|
438
|
+
" source: al-org-dev/distill-template#al-folio",
|
|
439
|
+
" allow_remote_loader: true",
|
|
440
|
+
].join("\n")
|
|
441
|
+
content.sub(/^al_folio:\s*$/) { |match| "#{match}\n#{insertion}" }
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def nested_namespace_present?(content, key)
|
|
445
|
+
parsed = parse_yaml(content) || {}
|
|
446
|
+
return false unless parsed.is_a?(Hash)
|
|
447
|
+
|
|
448
|
+
al_folio = parsed["al_folio"]
|
|
449
|
+
al_folio.is_a?(Hash) && al_folio[key].is_a?(Hash)
|
|
450
|
+
rescue StandardError
|
|
451
|
+
false
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def using_core_theme?
|
|
455
|
+
config_path = @root.join("_config.yml")
|
|
456
|
+
return false unless config_path.file?
|
|
457
|
+
|
|
458
|
+
begin
|
|
459
|
+
parsed = parse_yaml(config_path.read) || {}
|
|
460
|
+
parsed["theme"] == "al_folio_core" || Array(parsed["plugins"]).include?("al_folio_core")
|
|
461
|
+
rescue StandardError
|
|
462
|
+
false
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def parse_yaml(content)
|
|
467
|
+
YAML.safe_load(content, permitted_classes: [Date, Time], aliases: true)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def manifest_paths
|
|
471
|
+
if defined?(AlFolioCore) && AlFolioCore.respond_to?(:migration_manifest_paths)
|
|
472
|
+
return Array(AlFolioCore.migration_manifest_paths).select { |path| File.file?(path) }
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
Dir.glob(@root.join("migrations/*.yml")).sort
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def each_text_file
|
|
479
|
+
FILE_GLOBS.each do |glob|
|
|
480
|
+
Dir.glob(@root.join(glob)).sort.each do |path|
|
|
481
|
+
next unless File.file?(path)
|
|
482
|
+
next if ignored_path?(path)
|
|
483
|
+
|
|
484
|
+
yield path
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def ignored_path?(path)
|
|
490
|
+
normalized = path.to_s
|
|
491
|
+
IGNORE_PATH_PATTERNS.any? { |pattern| normalized.match?(pattern) }
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def write_report(findings)
|
|
495
|
+
by_severity = findings.group_by(&:severity)
|
|
496
|
+
blocking = by_severity.fetch(:blocking, [])
|
|
497
|
+
warning = by_severity.fetch(:warning, [])
|
|
498
|
+
|
|
499
|
+
File.write(@root.join(REPORT_PATH), <<~MD)
|
|
500
|
+
# al-folio upgrade report
|
|
501
|
+
|
|
502
|
+
Generated by `bundle exec al-folio upgrade report`.
|
|
503
|
+
|
|
504
|
+
## Summary
|
|
505
|
+
|
|
506
|
+
- Blocking findings: #{blocking.count}
|
|
507
|
+
- Non-blocking findings: #{warning.count}
|
|
508
|
+
|
|
509
|
+
## Blocking
|
|
510
|
+
|
|
511
|
+
#{format_findings(blocking)}
|
|
512
|
+
|
|
513
|
+
## Non-blocking
|
|
514
|
+
|
|
515
|
+
#{format_findings(warning)}
|
|
516
|
+
MD
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def format_findings(findings)
|
|
520
|
+
return "- None\n" if findings.empty?
|
|
521
|
+
|
|
522
|
+
findings.map do |finding|
|
|
523
|
+
"- [#{finding.id}] #{finding.message} (`#{finding.file}:#{finding.line}`)\n - Snippet: `#{finding.snippet}`"
|
|
524
|
+
end.join("\n") + "\n"
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def print_summary(findings)
|
|
528
|
+
blocking = findings.count { |finding| finding.severity == :blocking }
|
|
529
|
+
warning = findings.count { |finding| finding.severity == :warning }
|
|
530
|
+
@stdout.puts("Upgrade audit complete. Blocking: #{blocking}, Non-blocking: #{warning}.")
|
|
531
|
+
@stdout.puts("Report: #{REPORT_PATH}")
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: al_folio_upgrade
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- al-folio maintainers
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: jekyll
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.9'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '5.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '3.9'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: liquid
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '4.0'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '6.0'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '4.0'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '6.0'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: al_folio_core
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 1.0.0
|
|
60
|
+
type: :runtime
|
|
61
|
+
prerelease: false
|
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 1.0.0
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: bundler
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '2.0'
|
|
74
|
+
- - "<"
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '3.0'
|
|
77
|
+
type: :development
|
|
78
|
+
prerelease: false
|
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '2.0'
|
|
84
|
+
- - "<"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '3.0'
|
|
87
|
+
- !ruby/object:Gem::Dependency
|
|
88
|
+
name: rake
|
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - "~>"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '13.0'
|
|
94
|
+
type: :development
|
|
95
|
+
prerelease: false
|
|
96
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '13.0'
|
|
101
|
+
description: Provides audit, report, and safe codemod commands for al-folio upgrades.
|
|
102
|
+
email:
|
|
103
|
+
- maintainers@al-folio.dev
|
|
104
|
+
executables:
|
|
105
|
+
- al-folio
|
|
106
|
+
extensions: []
|
|
107
|
+
extra_rdoc_files: []
|
|
108
|
+
files:
|
|
109
|
+
- CHANGELOG.md
|
|
110
|
+
- LICENSE
|
|
111
|
+
- README.md
|
|
112
|
+
- exe/al-folio
|
|
113
|
+
- lib/al_folio_upgrade.rb
|
|
114
|
+
- lib/al_folio_upgrade/cli.rb
|
|
115
|
+
- lib/al_folio_upgrade/version.rb
|
|
116
|
+
homepage: https://github.com/al-org-dev/al-folio-upgrade
|
|
117
|
+
licenses:
|
|
118
|
+
- MIT
|
|
119
|
+
metadata:
|
|
120
|
+
allowed_push_host: https://rubygems.org
|
|
121
|
+
homepage_uri: https://github.com/al-org-dev/al-folio-upgrade
|
|
122
|
+
source_code_uri: https://github.com/al-org-dev/al-folio-upgrade
|
|
123
|
+
post_install_message:
|
|
124
|
+
rdoc_options: []
|
|
125
|
+
require_paths:
|
|
126
|
+
- lib
|
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '2.7'
|
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
requirements: []
|
|
138
|
+
rubygems_version: 3.4.10
|
|
139
|
+
signing_key:
|
|
140
|
+
specification_version: 4
|
|
141
|
+
summary: Upgrade tooling for al-folio v1.x
|
|
142
|
+
test_files: []
|