jekyll-theme-zer0 1.18.0 → 1.19.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.
@@ -198,6 +198,13 @@ def strip_code_fences(body)
198
198
  body.gsub(/^```.*?^```/m, '').gsub(/`[^`]*`/, '')
199
199
  end
200
200
 
201
+ # Remove Liquid {% raw %}...{% endraw %} regions. Content inside them is a
202
+ # literal display example (often showing ``` fences or {{ }} tags), not real
203
+ # page structure, so it must not be counted by the quality/style checks.
204
+ def strip_liquid_raw(body)
205
+ body.gsub(/\{%-?\s*raw\s*-?%\}.*?\{%-?\s*endraw\s*-?%\}/m, '')
206
+ end
207
+
201
208
  # --- Collection detection ----------------------------------------------------
202
209
  def detect_collection(path, schema)
203
210
  collections = schema['collections'] || {}
@@ -286,6 +293,8 @@ end
286
293
 
287
294
  def check_quality(body, quality)
288
295
  issues = []
296
+ # Liquid {% raw %} examples are display-only — exclude them from every check.
297
+ body = strip_liquid_raw(body)
289
298
  prose = strip_code_fences(body)
290
299
  words = prose.split(/\s+/).reject(&:empty?)
291
300
  wc = words.length
@@ -314,9 +323,18 @@ def check_quality(body, quality)
314
323
 
315
324
  # Code fences must declare a language.
316
325
  if quality['require_code_fence_language']
317
- body.scan(/^```([^\n]*)\n/).each do |m|
318
- info = m[0].strip
319
- issues << Issue.new('info', 'quality', 'Code fence without a language (use ```bash, ```ruby, …)') if info.empty?
326
+ # Only opening fences need a language; toggle state so the matching closing
327
+ # fence (a bare ```) is not counted.
328
+ in_fence = false
329
+ body.each_line do |line|
330
+ next unless line =~ /^\s*```(.*)$/
331
+
332
+ if in_fence
333
+ in_fence = false # closing fence — no language required
334
+ else
335
+ in_fence = true
336
+ issues << Issue.new('info', 'quality', 'Code fence without a language (use ```bash, ```ruby, …)') if Regexp.last_match(1).strip.empty?
337
+ end
320
338
  end
321
339
  end
322
340
 
@@ -337,7 +355,7 @@ end
337
355
 
338
356
  def check_style(body, style)
339
357
  issues = []
340
- prose = strip_code_fences(body)
358
+ prose = strip_code_fences(strip_liquid_raw(body))
341
359
  (style['terminology'] || {}).each do |wrong, right|
342
360
  next if wrong.to_s.empty?
343
361
 
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+ # scripts/lib/hub.rb — shared helpers for the org content hub tooling
5
+ # =============================================================================
6
+ #
7
+ # Used by:
8
+ # scripts/sync-hub-metadata.rb dashboard data refresh (_data/hub_index.yml)
9
+ # scripts/provision-org-sites.rb Pages scaffold rollout to org repos
10
+ #
11
+ # The registry (_data/hub.yml) is the single source of truth; see its header
12
+ # for the architecture. Everything here is read-only with respect to this
13
+ # repository — callers decide what to write.
14
+ # =============================================================================
15
+
16
+ require 'yaml'
17
+ require 'json'
18
+ require 'date'
19
+ require 'base64'
20
+ require 'open3'
21
+
22
+ module Hub
23
+ ROOT = File.expand_path('../..', __dir__)
24
+ CONFIG_FILE = File.join(ROOT, '_data', 'hub.yml')
25
+
26
+ GENERATED_HEADER = <<~HEADER
27
+ # =============================================================================
28
+ # GENERATED FILE — do not edit by hand.
29
+ # Regenerate with: ./scripts/sync-hub-metadata.sh
30
+ # Source registry: _data/hub.yml
31
+ # =============================================================================
32
+ HEADER
33
+
34
+ module_function
35
+
36
+ def log_info(msg)
37
+ puts "[INFO] #{msg}"
38
+ end
39
+
40
+ def log_warn(msg)
41
+ warn "[WARN] #{msg}"
42
+ end
43
+
44
+ def log_error(msg)
45
+ warn "[ERROR] #{msg}"
46
+ end
47
+
48
+ # Permit Date/Time; fall back for the older macOS system Ruby (2.6) whose
49
+ # safe loader signature differs.
50
+ def load_yaml(path)
51
+ YAML.load_file(path, permitted_classes: [Date, Time])
52
+ rescue ArgumentError
53
+ YAML.load_file(path)
54
+ end
55
+
56
+ def run_cmd(*cmd)
57
+ out, err, status = Open3.capture3(*cmd)
58
+ raise "command failed: #{cmd.join(' ')}\n#{err}" unless status.success?
59
+
60
+ out
61
+ end
62
+
63
+ # gh api wrapper returning parsed JSON, or nil on a 404.
64
+ def gh_api(path)
65
+ out, err, status = Open3.capture3('gh', 'api', path)
66
+ return JSON.parse(out) if status.success?
67
+ return nil if err.include?('404') || out.include?('"status": "404"')
68
+
69
+ raise "gh api #{path} failed:\n#{err}"
70
+ end
71
+
72
+ def load_registry
73
+ raise "registry not found: #{CONFIG_FILE}" unless File.exist?(CONFIG_FILE)
74
+
75
+ load_yaml(CONFIG_FILE)
76
+ end
77
+
78
+ def validate_registry(cfg)
79
+ return ['registry is not a YAML mapping'] unless cfg.is_a?(Hash)
80
+
81
+ errors = []
82
+ errors << 'org: must be a non-empty string' unless cfg['org'].is_a?(String) && !cfg['org'].strip.empty?
83
+
84
+ if cfg.key?('auto_discover') && ![true, false].include?(cfg['auto_discover'])
85
+ errors << 'auto_discover: must be true or false'
86
+ end
87
+
88
+ if cfg.key?('exclude_repos') && !(cfg['exclude_repos'].is_a?(Array) && cfg['exclude_repos'].all? { |r| r.is_a?(String) })
89
+ errors << 'exclude_repos: must be a list of strings'
90
+ end
91
+
92
+ pages = cfg['pages'] || {}
93
+ errors << 'pages: must be a mapping' unless pages.is_a?(Hash)
94
+ if pages.is_a?(Hash) && !(pages['theme_repo'].is_a?(String) && pages['theme_repo'].include?('/'))
95
+ errors << 'pages.theme_repo: must be an <owner>/<repo> string'
96
+ end
97
+
98
+ defaults = cfg['defaults'] || {}
99
+ errors << 'defaults: must be a mapping' unless defaults.is_a?(Hash)
100
+ if defaults.is_a?(Hash) && defaults.key?('exclude') &&
101
+ !(defaults['exclude'].is_a?(Array) && defaults['exclude'].all? { |p| p.is_a?(String) })
102
+ errors << 'defaults.exclude: must be a list of glob strings'
103
+ end
104
+
105
+ repos = cfg['repos'] || []
106
+ errors << 'repos: must be a list' unless repos.is_a?(Array)
107
+ if repos.is_a?(Array)
108
+ repos.each_with_index do |repo, i|
109
+ unless repo.is_a?(Hash) && repo['name'].is_a?(String) && !repo['name'].strip.empty?
110
+ errors << "repos[#{i}]: needs a non-empty `name`"
111
+ next
112
+ end
113
+ errors << "repos[#{i}] (#{repo['name']}): name contains unsafe characters" unless repo['name'].match?(%r{\A[\w.-]+\z})
114
+ end
115
+ end
116
+
117
+ if cfg['auto_discover'] == false && repos.empty?
118
+ errors << 'auto_discover is false but repos: is empty — nothing to include'
119
+ end
120
+
121
+ errors
122
+ end
123
+
124
+ # Org repos that are hub content sites, with per-repo registry overrides
125
+ # merged on top. Requires `gh` unless auto_discover is false.
126
+ def discover_repos(cfg)
127
+ org = cfg['org']
128
+ excluded = cfg['exclude_repos'] || []
129
+ manual = (cfg['repos'] || []).map { |r| [r['name'], r] }.to_h
130
+
131
+ discovered =
132
+ if cfg.fetch('auto_discover', true)
133
+ json = run_cmd('gh', 'repo', 'list', org, '--limit', '200',
134
+ '--json', 'name,description,defaultBranchRef,isArchived,pushedAt')
135
+ JSON.parse(json).reject { |r| r['isArchived'] }.map do |r|
136
+ {
137
+ 'name' => r['name'],
138
+ 'description' => r['description'].to_s,
139
+ 'branch' => r.dig('defaultBranchRef', 'name') || 'main',
140
+ 'pushed_at' => r['pushedAt'].to_s
141
+ }
142
+ end
143
+ else
144
+ manual.values.map { |r| { 'branch' => 'main' }.merge(r) }
145
+ end
146
+
147
+ discovered
148
+ .reject { |r| excluded.include?(r['name']) }
149
+ .map { |r| r.merge(manual[r['name']] || {}) { |_k, auto, over| (over.nil? || over == '') ? auto : over } }
150
+ .sort_by { |r| r['name'] }
151
+ end
152
+
153
+ def excluded?(rel, patterns)
154
+ patterns.any? do |pat|
155
+ rel == pat ||
156
+ File.fnmatch?(pat, rel, File::FNM_PATHNAME) ||
157
+ (pat.end_with?('/**') && rel.start_with?("#{pat[0..-4]}/"))
158
+ end
159
+ end
160
+
161
+ def slugify(segment)
162
+ segment.to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-+|-+\z/, '')
163
+ end
164
+
165
+ def humanize(name)
166
+ name.tr('-_', ' ').split.map(&:capitalize).join(' ')
167
+ end
168
+
169
+ FRONT_MATTER_RE = /\A---\s*\n(.*?\n?)^---\s*\n/m
170
+
171
+ def split_front_matter(raw)
172
+ match = raw.match(FRONT_MATTER_RE)
173
+ return [nil, raw] unless match
174
+
175
+ fm = begin
176
+ YAML.safe_load(match[1], permitted_classes: [Date, Time]) || {}
177
+ rescue StandardError
178
+ nil
179
+ end
180
+ return [nil, raw] unless fm.is_a?(Hash)
181
+
182
+ [fm, match.post_match]
183
+ end
184
+
185
+ # Title of a markdown document: front matter `title`, else first H1, else
186
+ # the humanized fallback.
187
+ def title_of(raw, fallback)
188
+ fm, body = split_front_matter(raw)
189
+ (fm && fm['title']) || body[/^\#\s+(.+?)\s*$/, 1] || humanize(fallback)
190
+ end
191
+
192
+ # The markdown content files of a repo tree (array of path strings),
193
+ # filtered through the registry's exclude patterns.
194
+ def content_paths(paths, exclude_patterns)
195
+ paths.select { |p| p.end_with?('.md') }
196
+ .reject { |p| p.split('/').any? { |seg| seg.start_with?('.') } }
197
+ .reject { |p| excluded?(p, exclude_patterns) }
198
+ .sort
199
+ end
200
+
201
+ # Pretty-permalink URL of a repo-root or nested page on the published site.
202
+ # site_url must end with '/'. README/index map to their directory root.
203
+ def page_url(site_url, rel)
204
+ segments = rel.sub(/\.md\z/i, '').split('/')
205
+ segments.pop if %w[index readme].include?(segments.last.downcase)
206
+ segments.empty? ? site_url : "#{site_url}#{segments.join('/')}/"
207
+ end
208
+ end
@@ -0,0 +1,252 @@
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'
@@ -0,0 +1,23 @@
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" "$@"