jekyll-theme-zer0 1.19.1 → 1.21.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/CHANGELOG.md +404 -0
- data/README.md +27 -19
- data/_data/authors.yml +154 -5
- data/_data/backlog.yml +12 -5
- data/_data/content_statistics.yml +273 -297
- data/_data/features.yml +4 -25
- data/_data/navigation/README.md +24 -0
- data/_data/navigation/about.yml +2 -0
- data/_data/navigation/main.yml +2 -7
- data/_data/roadmap.yml +86 -12
- data/_data/roadmap_plan.yml +37 -0
- data/_data/routing.yml +73 -0
- data/_includes/components/author-avatar-url.html +28 -0
- data/_includes/components/author-bio.html +86 -0
- data/_includes/components/author-card.html +184 -121
- data/_includes/components/author-eeat.html +10 -4
- data/_includes/components/info-section.html +1 -1
- data/_includes/components/mermaid.html +0 -3
- data/_includes/components/post-card.html +19 -9
- data/_includes/content/giscus.html +3 -2
- data/_includes/core/footer-fabs.html +28 -0
- data/_includes/core/footer.html +7 -17
- data/_includes/core/head.html +2 -2
- data/_includes/navigation/breadcrumbs.html +20 -2
- data/_includes/navigation/local-graph.html +18 -2
- data/_includes/obsidian/full-graph.html +4 -6
- data/_layouts/article.html +44 -74
- data/_layouts/author.html +274 -0
- data/_layouts/authors.html +55 -0
- data/_layouts/news.html +3 -3
- data/_layouts/note.html +21 -6
- data/_layouts/notebook.html +21 -6
- data/_layouts/root.html +31 -17
- data/_layouts/section.html +3 -3
- data/_plugins/author_pages_generator.rb +121 -0
- data/_sass/components/_author.scss +219 -0
- data/_sass/components/_content-tables.scss +16 -1
- data/_sass/components/_notes-index.scss +102 -0
- data/_sass/components/_search-modal.scss +40 -0
- data/_sass/components/_ui-enhancements.scss +570 -0
- data/_sass/core/_docs-code-examples.scss +463 -0
- data/_sass/core/_docs-layout.scss +0 -453
- data/_sass/core/_navbar.scss +253 -0
- data/_sass/core/_sidebar-extras.scss +79 -0
- data/_sass/core/_toc.scss +87 -0
- data/_sass/core/_variables.scss +7 -142
- data/_sass/custom.scss +24 -1122
- data/_sass/layouts/_global-chrome.scss +59 -0
- data/assets/css/main.scss +19 -2
- data/assets/js/author-profile.js +190 -0
- data/assets/js/modules/navigation/navbar.js +104 -0
- data/assets/js/obsidian-graph.js +2 -2
- data/assets/js/obsidian-local-graph.js +11 -5
- data/assets/vendor/cytoscape/cytoscape.min.js +32 -0
- data/scripts/README.md +39 -0
- data/scripts/bin/validate +11 -1
- data/scripts/dev/css-diff.sh +49 -0
- data/scripts/dev/shot.js +37 -0
- data/scripts/features/generate-preview-images +110 -6
- data/scripts/features/pixelate-preview-images +126 -0
- data/scripts/features/pixelate_images.py +662 -0
- data/scripts/github-setup.sh +0 -0
- data/scripts/lib/preview_generator.py +47 -3
- data/scripts/pixelate-preview-images.sh +12 -0
- data/scripts/sync-backlog.rb +149 -26
- data/scripts/sync-plan.rb +164 -0
- data/scripts/sync-plan.sh +21 -0
- data/scripts/test/integration/auto-version +10 -8
- data/scripts/test/lib/run_tests.sh +2 -0
- data/scripts/test/lib/test_content_review.sh +205 -0
- data/scripts/test/lib/test_pixelate_images.sh +108 -0
- metadata +29 -20
- data/_data/hub.yml +0 -68
- data/_data/hub_index.yml +0 -203
- data/_data/navigation/hub.yml +0 -110
- data/assets/vendor/font-awesome/css/all.min.css +0 -9
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
- data/assets/vendor/jquery/jquery-3.7.1.min.js +0 -2
- data/scripts/lib/hub.rb +0 -208
- data/scripts/provision-org-sites.rb +0 -252
- data/scripts/provision-org-sites.sh +0 -23
- data/scripts/sync-hub-metadata.rb +0 -184
- data/scripts/sync-hub-metadata.sh +0 -22
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# =============================================================================
|
|
5
|
-
# provision-org-sites.rb
|
|
6
|
-
# =============================================================================
|
|
7
|
-
#
|
|
8
|
-
# Rolls the GitHub Pages scaffold out to the org repos registered in
|
|
9
|
-
# `_data/hub.yml`, so each publishes its own project site at
|
|
10
|
-
# https://<org>.github.io/<repo>/ rendered with the zer0-mistakes theme
|
|
11
|
-
# (`remote_theme`) — content never leaves the source repo.
|
|
12
|
-
#
|
|
13
|
-
# Per repo it:
|
|
14
|
-
# 1. clones the repo and analyzes its content layout (sections, timeline),
|
|
15
|
-
# 2. renders templates/org-site/* (_config.yml + _data/navigation/main.yml),
|
|
16
|
-
# 3. opens a pull request with the scaffold (branch: zer0/pages-scaffold) —
|
|
17
|
-
# or pushes directly with --direct,
|
|
18
|
-
# 4. with --enable-pages, enables GitHub Pages ("deploy from branch").
|
|
19
|
-
#
|
|
20
|
-
# Safety:
|
|
21
|
-
# - A repo whose _config.yml was NOT generated by this script is skipped
|
|
22
|
-
# (never clobbers a hand-rolled site config).
|
|
23
|
-
# - Re-running is idempotent: identical scaffold -> no new PR/commit.
|
|
24
|
-
#
|
|
25
|
-
# Usage:
|
|
26
|
-
# ruby scripts/provision-org-sites.rb --dry-run # preview everything
|
|
27
|
-
# ruby scripts/provision-org-sites.rb # open scaffold PRs
|
|
28
|
-
# ruby scripts/provision-org-sites.rb --enable-pages # PRs + enable Pages
|
|
29
|
-
# ruby scripts/provision-org-sites.rb --repos 2005 --direct
|
|
30
|
-
# ruby scripts/provision-org-sites.rb --stage /tmp/out # render files locally only
|
|
31
|
-
# ruby scripts/provision-org-sites.rb --check # validate registry + templates
|
|
32
|
-
#
|
|
33
|
-
# Requires `gh` (repo + pages write for the real run) and `git`.
|
|
34
|
-
# =============================================================================
|
|
35
|
-
|
|
36
|
-
require 'optparse'
|
|
37
|
-
require 'fileutils'
|
|
38
|
-
require 'tmpdir'
|
|
39
|
-
require_relative 'lib/hub'
|
|
40
|
-
|
|
41
|
-
TEMPLATE_DIR = File.join(Hub::ROOT, 'templates', 'org-site')
|
|
42
|
-
CONFIG_TEMPLATE = File.join(TEMPLATE_DIR, '_config.yml.template')
|
|
43
|
-
NAV_TEMPLATE = File.join(TEMPLATE_DIR, 'navigation-main.yml.template')
|
|
44
|
-
SCAFFOLD_MARKER = 'zer0-mistakes org hub scaffold'
|
|
45
|
-
BRANCH_NAME = 'zer0/pages-scaffold'
|
|
46
|
-
|
|
47
|
-
def render(template, vars)
|
|
48
|
-
vars.reduce(File.read(template, encoding: 'utf-8')) do |text, (key, value)|
|
|
49
|
-
text.gsub("{{#{key}}}", value.to_s)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def yaml_quote(text)
|
|
54
|
-
text.to_s.gsub('"', '\"')
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Nav entries for the repo's root content files and sections, rendered as
|
|
58
|
-
# site-relative URLs (the theme's relative_url filter prefixes the baseurl).
|
|
59
|
-
def nav_items(clone, files)
|
|
60
|
-
items = +''
|
|
61
|
-
files.reject { |f| f.include?('/') }
|
|
62
|
-
.reject { |f| %w[index readme].include?(File.basename(f, '.md').downcase) }
|
|
63
|
-
.each do |f|
|
|
64
|
-
base = File.basename(f, '.md')
|
|
65
|
-
items << "- title: #{Hub.title_of(File.read(File.join(clone, f), encoding: 'utf-8'), base)}\n" \
|
|
66
|
-
" icon: bi-clock-history\n" \
|
|
67
|
-
" url: /#{base}/\n"
|
|
68
|
-
end
|
|
69
|
-
files.select { |f| f.include?('/') }.map { |f| f.split('/').first }.uniq.sort.each do |dir|
|
|
70
|
-
index = File.join(clone, dir, 'index.md')
|
|
71
|
-
title = File.exist?(index) ? Hub.title_of(File.read(index, encoding: 'utf-8'), dir) : Hub.humanize(dir)
|
|
72
|
-
items << "- title: #{title}\n" \
|
|
73
|
-
" icon: bi-folder\n" \
|
|
74
|
-
" url: /#{dir}/\n"
|
|
75
|
-
end
|
|
76
|
-
items.chomp
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def scaffold_files(repo, cfg, clone)
|
|
80
|
-
excludes = (cfg['defaults'] || {})['exclude'] || []
|
|
81
|
-
files = Hub.content_paths(Dir.chdir(clone) { Dir.glob('**/*.md') }, excludes)
|
|
82
|
-
|
|
83
|
-
vars = {
|
|
84
|
-
'ORG' => cfg['org'],
|
|
85
|
-
'REPO' => repo['name'],
|
|
86
|
-
'REPO_TITLE' => yaml_quote(repo['title'] || repo['name']),
|
|
87
|
-
'REPO_DESCRIPTION' => yaml_quote(repo['description']),
|
|
88
|
-
'THEME_REPO' => cfg.dig('pages', 'theme_repo'),
|
|
89
|
-
'BRANCH' => repo['branch'],
|
|
90
|
-
'NAV_ITEMS' => nav_items(clone, files)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
'_config.yml' => render(CONFIG_TEMPLATE, vars),
|
|
95
|
-
'_data/navigation/main.yml' => render(NAV_TEMPLATE, vars)
|
|
96
|
-
}
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def write_scaffold(base_dir, rendered)
|
|
100
|
-
changed = []
|
|
101
|
-
rendered.each do |rel, content|
|
|
102
|
-
path = File.join(base_dir, rel)
|
|
103
|
-
next if File.exist?(path) && File.read(path, encoding: 'utf-8') == content
|
|
104
|
-
|
|
105
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
106
|
-
File.write(path, content)
|
|
107
|
-
changed << rel
|
|
108
|
-
end
|
|
109
|
-
changed
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def open_pr(repo, cfg, clone, changed, direct:)
|
|
113
|
-
org = cfg['org']
|
|
114
|
-
slug = "#{org}/#{repo['name']}"
|
|
115
|
-
|
|
116
|
-
Dir.chdir(clone) do
|
|
117
|
-
if direct
|
|
118
|
-
Hub.run_cmd('git', 'add', *changed)
|
|
119
|
-
Hub.run_cmd('git', 'commit', '-m', 'feat(pages): publish via GitHub Pages with the zer0-mistakes theme')
|
|
120
|
-
Hub.run_cmd('git', 'push', 'origin', repo['branch'])
|
|
121
|
-
Hub.log_info " pushed scaffold directly to #{slug}@#{repo['branch']}"
|
|
122
|
-
return
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
Hub.run_cmd('git', 'checkout', '-B', BRANCH_NAME)
|
|
126
|
-
Hub.run_cmd('git', 'add', *changed)
|
|
127
|
-
Hub.run_cmd('git', 'commit', '-m', 'feat(pages): publish via GitHub Pages with the zer0-mistakes theme')
|
|
128
|
-
Hub.run_cmd('git', 'push', '--force', 'origin', BRANCH_NAME)
|
|
129
|
-
|
|
130
|
-
existing = JSON.parse(Hub.run_cmd('gh', 'pr', 'list', '--repo', slug, '--head', BRANCH_NAME,
|
|
131
|
-
'--state', 'open', '--json', 'url'))
|
|
132
|
-
if existing.empty?
|
|
133
|
-
body = <<~BODY
|
|
134
|
-
Adds the GitHub Pages scaffold generated by
|
|
135
|
-
[zer0-mistakes/scripts/provision-org-sites.rb](https://github.com/#{cfg.dig('pages', 'theme_repo')}/blob/main/scripts/provision-org-sites.rb):
|
|
136
|
-
|
|
137
|
-
- `_config.yml` — publishes this repo at https://#{org}.github.io/#{repo['name']}/ using `remote_theme: #{cfg.dig('pages', 'theme_repo')}`. Content stays plain markdown; the GitHub Pages default plugins render it.
|
|
138
|
-
- `_data/navigation/main.yml` — navbar/sidebar entries for the repo's sections.
|
|
139
|
-
|
|
140
|
-
Once merged (with Pages enabled, "deploy from branch"), the site builds automatically on every push.
|
|
141
|
-
|
|
142
|
-
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
143
|
-
BODY
|
|
144
|
-
url = Hub.run_cmd('gh', 'pr', 'create', '--repo', slug, '--base', repo['branch'], '--head', BRANCH_NAME,
|
|
145
|
-
'--title', 'feat(pages): publish via GitHub Pages with the zer0-mistakes theme',
|
|
146
|
-
'--body', body).strip
|
|
147
|
-
Hub.log_info " opened PR: #{url}"
|
|
148
|
-
else
|
|
149
|
-
Hub.log_info " updated existing PR: #{existing.first['url']}"
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def enable_pages(repo, cfg)
|
|
155
|
-
org = cfg['org']
|
|
156
|
-
out, err, status = Open3.capture3(
|
|
157
|
-
'gh', 'api', '-X', 'POST', "repos/#{org}/#{repo['name']}/pages",
|
|
158
|
-
'-f', "source[branch]=#{repo['branch']}", '-f', 'source[path]=/'
|
|
159
|
-
)
|
|
160
|
-
if status.success?
|
|
161
|
-
Hub.log_info " Pages enabled: #{JSON.parse(out)['html_url']}"
|
|
162
|
-
elsif err.include?('409') || out.include?('already exists')
|
|
163
|
-
Hub.log_info ' Pages already enabled'
|
|
164
|
-
else
|
|
165
|
-
Hub.log_warn " could not enable Pages: #{err.lines.first&.strip}"
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# ---------------------------------------------------------------------------
|
|
170
|
-
# Main
|
|
171
|
-
# ---------------------------------------------------------------------------
|
|
172
|
-
|
|
173
|
-
options = { mode: :provision, dry_run: false, repos: nil, direct: false, enable_pages: false, stage: nil }
|
|
174
|
-
OptionParser.new do |opts|
|
|
175
|
-
opts.banner = 'Usage: ruby scripts/provision-org-sites.rb [options]'
|
|
176
|
-
opts.on('--check', 'validate registry and templates, no network') { options[:mode] = :check }
|
|
177
|
-
opts.on('--dry-run', 'show planned changes without pushing anything') { options[:dry_run] = true }
|
|
178
|
-
opts.on('--repos LIST', 'comma-separated subset of repos') { |v| options[:repos] = v.split(',').map(&:strip) }
|
|
179
|
-
opts.on('--direct', 'commit to the default branch instead of a PR') { options[:direct] = true }
|
|
180
|
-
opts.on('--enable-pages', 'enable GitHub Pages (deploy from branch)') { options[:enable_pages] = true }
|
|
181
|
-
opts.on('--stage DIR', 'render scaffold files into DIR/<repo>/ only') { |v| options[:stage] = v }
|
|
182
|
-
end.parse!
|
|
183
|
-
|
|
184
|
-
cfg = Hub.load_registry
|
|
185
|
-
errors = Hub.validate_registry(cfg)
|
|
186
|
-
[CONFIG_TEMPLATE, NAV_TEMPLATE].each do |t|
|
|
187
|
-
errors << "missing template: #{t.sub("#{Hub::ROOT}/", '')}" unless File.exist?(t)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
unless errors.empty?
|
|
191
|
-
errors.each { |e| Hub.log_error(e) }
|
|
192
|
-
exit 1
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
if options[:mode] == :check
|
|
196
|
-
Hub.log_info 'hub registry and org-site templates are valid'
|
|
197
|
-
exit 0
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
repos = Hub.discover_repos(cfg)
|
|
201
|
-
repos.select! { |r| options[:repos].include?(r['name']) } if options[:repos]
|
|
202
|
-
if repos.empty?
|
|
203
|
-
Hub.log_warn 'no repos to provision (check org, exclude_repos, or --repos filter)'
|
|
204
|
-
exit 0
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
Hub.log_info "Provisioning #{repos.size} repo(s) in #{cfg['org']}: #{repos.map { |r| r['name'] }.join(', ')}"
|
|
208
|
-
|
|
209
|
-
Dir.mktmpdir('hub-provision') do |tmp|
|
|
210
|
-
repos.each do |repo|
|
|
211
|
-
slug = "#{cfg['org']}/#{repo['name']}"
|
|
212
|
-
clone = File.join(tmp, repo['name'])
|
|
213
|
-
Hub.log_info "#{slug}:"
|
|
214
|
-
Hub.run_cmd('git', 'clone', '--quiet', '--branch', repo['branch'],
|
|
215
|
-
"https://github.com/#{slug}.git", clone)
|
|
216
|
-
|
|
217
|
-
existing_config = File.join(clone, '_config.yml')
|
|
218
|
-
if File.exist?(existing_config) && !File.read(existing_config, encoding: 'utf-8').include?(SCAFFOLD_MARKER)
|
|
219
|
-
Hub.log_warn ' has a hand-rolled _config.yml — skipping (manage it in the repo itself)'
|
|
220
|
-
next
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
rendered = scaffold_files(repo, cfg, clone)
|
|
224
|
-
|
|
225
|
-
if options[:stage]
|
|
226
|
-
stage_dir = File.join(options[:stage], repo['name'])
|
|
227
|
-
write_scaffold(stage_dir, rendered)
|
|
228
|
-
Hub.log_info " staged scaffold in #{stage_dir}"
|
|
229
|
-
next
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
changed = options[:dry_run] ? rendered.keys.reject { |rel| File.exist?(File.join(clone, rel)) && File.read(File.join(clone, rel), encoding: 'utf-8') == rendered[rel] } : write_scaffold(clone, rendered)
|
|
233
|
-
|
|
234
|
-
if changed.empty?
|
|
235
|
-
Hub.log_info ' scaffold already up to date'
|
|
236
|
-
elsif options[:dry_run]
|
|
237
|
-
Hub.log_info " DRY: would update #{changed.join(', ')} and open a PR"
|
|
238
|
-
else
|
|
239
|
-
open_pr(repo, cfg, clone, changed, direct: options[:direct])
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
if options[:enable_pages]
|
|
243
|
-
if options[:dry_run]
|
|
244
|
-
Hub.log_info ' DRY: would enable GitHub Pages (deploy from branch)'
|
|
245
|
-
else
|
|
246
|
-
enable_pages(repo, cfg)
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
Hub.log_info 'Provisioning complete'
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# provision-org-sites.sh
|
|
4
|
-
# =============================================================================
|
|
5
|
-
#
|
|
6
|
-
# Thin wrapper around scripts/provision-org-sites.rb.
|
|
7
|
-
#
|
|
8
|
-
# Rolls the GitHub Pages scaffold (templates/org-site/*) out to the org repos
|
|
9
|
-
# registered in _data/hub.yml so each publishes at https://<org>.github.io/<repo>/
|
|
10
|
-
# using this theme via remote_theme. Content never leaves the source repo.
|
|
11
|
-
#
|
|
12
|
-
# Usage:
|
|
13
|
-
# ./scripts/provision-org-sites.sh --dry-run # preview everything
|
|
14
|
-
# ./scripts/provision-org-sites.sh # open scaffold PRs
|
|
15
|
-
# ./scripts/provision-org-sites.sh --enable-pages # PRs + enable Pages
|
|
16
|
-
# ./scripts/provision-org-sites.sh --check # validate registry + templates
|
|
17
|
-
#
|
|
18
|
-
# =============================================================================
|
|
19
|
-
|
|
20
|
-
set -euo pipefail
|
|
21
|
-
|
|
22
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
-
exec ruby "${SCRIPT_DIR}/provision-org-sites.rb" "$@"
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# =============================================================================
|
|
5
|
-
# sync-hub-metadata.rb
|
|
6
|
-
# =============================================================================
|
|
7
|
-
#
|
|
8
|
-
# Refreshes the org content hub dashboard data. Content stays in the source
|
|
9
|
-
# repos (each publishes its own GitHub Pages site at
|
|
10
|
-
# https://<org>.github.io/<repo>/ — see scripts/provision-org-sites.rb); this
|
|
11
|
-
# script only gathers METADATA about them via the GitHub API:
|
|
12
|
-
#
|
|
13
|
-
# _data/hub_index.yml — per-repo stats for the /hub/ dashboard
|
|
14
|
-
# (page counts, sections, Pages status, links)
|
|
15
|
-
# _data/navigation/hub.yml — sidebar tree for the hub pages (sidebar.nav: hub)
|
|
16
|
-
#
|
|
17
|
-
# Nothing is cloned and no content is copied into this repository. Output is
|
|
18
|
-
# deterministic for unchanged sources, so the scheduled workflow only commits
|
|
19
|
-
# real changes.
|
|
20
|
-
#
|
|
21
|
-
# Usage:
|
|
22
|
-
# ruby scripts/sync-hub-metadata.rb # refresh dashboard data
|
|
23
|
-
# ruby scripts/sync-hub-metadata.rb --check # validate registry/output only (CI gate)
|
|
24
|
-
# ruby scripts/sync-hub-metadata.rb --dry-run # print planned writes, change nothing
|
|
25
|
-
#
|
|
26
|
-
# Requires the `gh` CLI (read scope). `--check` needs only the Ruby stdlib.
|
|
27
|
-
# =============================================================================
|
|
28
|
-
|
|
29
|
-
require 'optparse'
|
|
30
|
-
require 'fileutils'
|
|
31
|
-
require_relative 'lib/hub'
|
|
32
|
-
|
|
33
|
-
INDEX_FILE = File.join(Hub::ROOT, '_data', 'hub_index.yml')
|
|
34
|
-
NAV_FILE = File.join(Hub::ROOT, '_data', 'navigation', 'hub.yml')
|
|
35
|
-
|
|
36
|
-
def check_generated_output
|
|
37
|
-
errors = []
|
|
38
|
-
if File.exist?(INDEX_FILE)
|
|
39
|
-
index = Hub.load_yaml(INDEX_FILE)
|
|
40
|
-
if index.is_a?(Hash) && index['repos'].is_a?(Array)
|
|
41
|
-
index['repos'].each do |repo|
|
|
42
|
-
errors << "hub_index repo #{repo['name']}: missing site_url" unless repo['site_url'].to_s.start_with?('https://')
|
|
43
|
-
end
|
|
44
|
-
else
|
|
45
|
-
errors << '_data/hub_index.yml: missing repos list'
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
errors
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Gathers everything the dashboard needs about one repo from the GitHub API:
|
|
52
|
-
# the markdown tree (for counts/sections), the Pages site status, and whether
|
|
53
|
-
# the Pages scaffold (_config.yml) has landed.
|
|
54
|
-
def inspect_repo(repo, cfg)
|
|
55
|
-
org = cfg['org']
|
|
56
|
-
excludes = (cfg['defaults'] || {})['exclude'] || []
|
|
57
|
-
|
|
58
|
-
tree = Hub.gh_api("repos/#{org}/#{repo['name']}/git/trees/#{repo['branch']}?recursive=1")
|
|
59
|
-
paths = (tree ? tree['tree'] : []).select { |e| e['type'] == 'blob' }.map { |e| e['path'] }
|
|
60
|
-
files = Hub.content_paths(paths, excludes)
|
|
61
|
-
|
|
62
|
-
pages = Hub.gh_api("repos/#{org}/#{repo['name']}/pages")
|
|
63
|
-
site_url = (pages && pages['html_url']) || "https://#{org}.github.io/#{repo['name']}/"
|
|
64
|
-
site_url += '/' unless site_url.end_with?('/')
|
|
65
|
-
|
|
66
|
-
sections = files.select { |f| f.include?('/') }.group_by { |f| f.split('/').first }.sort.map do |dir, pages_in|
|
|
67
|
-
{
|
|
68
|
-
'name' => dir,
|
|
69
|
-
'title' => section_title(org, repo, dir),
|
|
70
|
-
'url' => "#{site_url}#{dir}/",
|
|
71
|
-
'count' => pages_in.count { |f| File.basename(f, '.md').downcase != 'index' }
|
|
72
|
-
}
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
root_pages = files.reject { |f| f.include?('/') }
|
|
76
|
-
.reject { |f| %w[index readme].include?(File.basename(f, '.md').downcase) }
|
|
77
|
-
.map { |f| { 'title' => Hub.humanize(File.basename(f, '.md')), 'url' => Hub.page_url(site_url, f) } }
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
'name' => repo['name'],
|
|
81
|
-
'title' => repo['title'] || repo['name'],
|
|
82
|
-
'description' => repo['description'].to_s,
|
|
83
|
-
'url' => "https://github.com/#{org}/#{repo['name']}",
|
|
84
|
-
'site_url' => site_url,
|
|
85
|
-
'pages_enabled' => !pages.nil?,
|
|
86
|
-
'scaffolded' => paths.include?('_config.yml'),
|
|
87
|
-
'branch' => repo['branch'],
|
|
88
|
-
'pushed_at' => repo['pushed_at'].to_s,
|
|
89
|
-
'page_count' => files.size,
|
|
90
|
-
'sections' => sections,
|
|
91
|
-
'root_pages' => root_pages
|
|
92
|
-
}
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Title of a section's index page (one contents API call), falling back to a
|
|
96
|
-
# humanized directory name.
|
|
97
|
-
def section_title(org, repo, dir)
|
|
98
|
-
doc = Hub.gh_api("repos/#{org}/#{repo['name']}/contents/#{dir}/index.md?ref=#{repo['branch']}")
|
|
99
|
-
return Hub.humanize(dir) unless doc && doc['content']
|
|
100
|
-
|
|
101
|
-
Hub.title_of(Base64.decode64(doc['content']).force_encoding('utf-8'), dir)
|
|
102
|
-
rescue StandardError
|
|
103
|
-
Hub.humanize(dir)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def build_index(cfg, repos)
|
|
107
|
-
{
|
|
108
|
-
'org' => cfg['org'],
|
|
109
|
-
'totals' => { 'repos' => repos.size, 'pages' => repos.sum { |r| r['page_count'] } },
|
|
110
|
-
'repos' => repos
|
|
111
|
-
}
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def build_nav(repos)
|
|
115
|
-
items = [{ 'title' => 'Hub Dashboard', 'icon' => 'bi-grid-1x2', 'url' => '/hub/' }]
|
|
116
|
-
repos.each do |repo|
|
|
117
|
-
children = [{ 'title' => 'Site Home', 'url' => repo['site_url'] }]
|
|
118
|
-
repo['root_pages'].each { |p| children << { 'title' => p['title'], 'url' => p['url'] } }
|
|
119
|
-
repo['sections'].each { |s| children << { 'title' => s['title'], 'url' => s['url'] } }
|
|
120
|
-
items << {
|
|
121
|
-
'title' => repo['title'],
|
|
122
|
-
'icon' => 'bi-journal-richtext',
|
|
123
|
-
'url' => repo['site_url'],
|
|
124
|
-
'children' => children
|
|
125
|
-
}
|
|
126
|
-
end
|
|
127
|
-
items
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def write_generated_yaml(path, data, dry_run:)
|
|
131
|
-
content = Hub::GENERATED_HEADER + data.to_yaml.sub(/\A---\n/, '')
|
|
132
|
-
if File.exist?(path) && File.read(path, encoding: 'utf-8') == content
|
|
133
|
-
Hub.log_info "unchanged: #{path.sub("#{Hub::ROOT}/", '')}"
|
|
134
|
-
elsif dry_run
|
|
135
|
-
Hub.log_info "DRY: would write #{path.sub("#{Hub::ROOT}/", '')}"
|
|
136
|
-
else
|
|
137
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
138
|
-
File.write(path, content)
|
|
139
|
-
Hub.log_info "wrote #{path.sub("#{Hub::ROOT}/", '')}"
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# ---------------------------------------------------------------------------
|
|
144
|
-
# Main
|
|
145
|
-
# ---------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
options = { mode: :sync, dry_run: false }
|
|
148
|
-
OptionParser.new do |opts|
|
|
149
|
-
opts.banner = 'Usage: ruby scripts/sync-hub-metadata.rb [--check|--dry-run]'
|
|
150
|
-
opts.on('--check', 'validate registry and generated data, no writes') { options[:mode] = :check }
|
|
151
|
-
opts.on('--dry-run', 'print planned actions without writing') { options[:dry_run] = true }
|
|
152
|
-
end.parse!
|
|
153
|
-
|
|
154
|
-
cfg = Hub.load_registry
|
|
155
|
-
errors = Hub.validate_registry(cfg)
|
|
156
|
-
errors += check_generated_output if options[:mode] == :check && errors.empty?
|
|
157
|
-
|
|
158
|
-
unless errors.empty?
|
|
159
|
-
errors.each { |e| Hub.log_error(e) }
|
|
160
|
-
exit 1
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
if options[:mode] == :check
|
|
164
|
-
Hub.log_info 'hub registry and generated data are valid'
|
|
165
|
-
exit 0
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
repos = Hub.discover_repos(cfg)
|
|
169
|
-
if repos.empty?
|
|
170
|
-
Hub.log_warn 'no repos found (check org and exclude_repos)'
|
|
171
|
-
exit 0
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
Hub.log_info "Inspecting #{repos.size} repo(s) in #{cfg['org']}: #{repos.map { |r| r['name'] }.join(', ')}"
|
|
175
|
-
inspected = repos.map do |repo|
|
|
176
|
-
info = inspect_repo(repo, cfg)
|
|
177
|
-
state = info['pages_enabled'] ? "live at #{info['site_url']}" : 'Pages NOT enabled'
|
|
178
|
-
Hub.log_info " #{info['name']}: #{info['page_count']} pages, #{info['sections'].size} sections — #{state}"
|
|
179
|
-
info
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
write_generated_yaml(INDEX_FILE, build_index(cfg, inspected), dry_run: options[:dry_run])
|
|
183
|
-
write_generated_yaml(NAV_FILE, build_nav(inspected), dry_run: options[:dry_run])
|
|
184
|
-
Hub.log_info 'Hub metadata refresh complete'
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# sync-hub-metadata.sh
|
|
4
|
-
# =============================================================================
|
|
5
|
-
#
|
|
6
|
-
# Thin wrapper around scripts/sync-hub-metadata.rb.
|
|
7
|
-
#
|
|
8
|
-
# Refreshes the org content hub dashboard data (_data/hub_index.yml and
|
|
9
|
-
# _data/navigation/hub.yml) from the GitHub API. Content stays in the source
|
|
10
|
-
# repos — nothing is cloned or copied here.
|
|
11
|
-
#
|
|
12
|
-
# Usage:
|
|
13
|
-
# ./scripts/sync-hub-metadata.sh # refresh dashboard data
|
|
14
|
-
# ./scripts/sync-hub-metadata.sh --check # validate registry/output only (CI/PR gate)
|
|
15
|
-
# ./scripts/sync-hub-metadata.sh --dry-run # print planned actions only
|
|
16
|
-
#
|
|
17
|
-
# =============================================================================
|
|
18
|
-
|
|
19
|
-
set -euo pipefail
|
|
20
|
-
|
|
21
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
-
exec ruby "${SCRIPT_DIR}/sync-hub-metadata.rb" "$@"
|