ci: auto-sync GitHub wiki module pages from man pages
Add tooling that makes the scdoc man pages the single source of truth and regenerates the wiki module pages from them: - .github/wiki/mapping.json: man-page -> wiki-page mapping (aggregation-aware) - .github/wiki/generate.py: scdoc -> pandoc -> gfm, strips man-only sections, concatenates aggregated pages, appends optional extras/<Page>.md - .github/wiki/extras/: hand-kept appendices (screenshots) with no man equivalent - .github/workflows/wiki.yml: on push to man/** (or the tooling), regenerate and push the wiki; other wiki pages are left untouched Covers all 65 man pages -> 45 wiki pages (5 new: GPS, Inhibitor, Mango, Menu, WWAN). Non-module and hand-written wiki pages are never modified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
# Wiki sync
|
||||||
|
|
||||||
|
The scdoc man pages under [`man/`](../../man) are the **single source of truth**
|
||||||
|
for module documentation. The GitHub wiki module pages are generated from them by
|
||||||
|
[`generate.py`](generate.py) and kept in sync automatically by the
|
||||||
|
[`wiki.yml`](../workflows/wiki.yml) workflow on every push to `master` that
|
||||||
|
touches `man/**` or this tooling.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `mapping.json` — maps each wiki page to the ordered list of man-page basenames
|
||||||
|
that compose it. Several man pages are concatenated into one aggregated page
|
||||||
|
(e.g. `Module:-Hyprland` ← the four `waybar-hyprland-*` pages). **Add an entry
|
||||||
|
here when you add a new man page** — the workflow fails the mapping check
|
||||||
|
otherwise.
|
||||||
|
- `generate.py` — `scdoc → roff → pandoc → gfm`, strips the `NAME`/`FILES`/`AUTHOR`
|
||||||
|
man sections, and writes one `Module:-*.md` per mapping entry.
|
||||||
|
- `extras/<Page>.md` — optional hand-maintained appendix (screenshots, showcase
|
||||||
|
snippets that have no man equivalent), appended verbatim after the generated body.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
Only the pages listed in `mapping.json` are (re)written. Every other wiki page
|
||||||
|
(`Home`, `Installation`, user showcases, the hand-written `Module:-Cava:-GLSL`,
|
||||||
|
`Module:-Group`, `Module:-Load`, …) is left untouched.
|
||||||
|
|
||||||
|
## To edit a module's docs
|
||||||
|
|
||||||
|
Edit the man page under `man/`, not the wiki. The wiki page is overwritten on the
|
||||||
|
next sync. Put anything with no man equivalent (images, etc.) in `extras/`.
|
||||||
|
|
||||||
|
## Run locally
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 .github/wiki/generate.py --check # validate mapping only
|
||||||
|
python3 .github/wiki/generate.py --out-dir /tmp/wiki-out # generate a preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## One-time setup
|
||||||
|
|
||||||
|
The repository wiki must be enabled (Settings → Features → Wikis) with at least
|
||||||
|
one initial page so the `.wiki.git` remote exists.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
Executable
+137
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate GitHub wiki pages from the scdoc man pages.
|
||||||
|
|
||||||
|
The man pages under man/ are the single source of truth for module
|
||||||
|
documentation. This script converts them to GitHub-flavoured Markdown
|
||||||
|
(scdoc -> roff -> pandoc -> gfm) and writes one wiki page per entry in
|
||||||
|
mapping.json, concatenating several man pages into one page where the
|
||||||
|
wiki keeps an aggregated page (e.g. Module:-Hyprland).
|
||||||
|
|
||||||
|
Only the pages listed in mapping.json are (re)written; every other wiki
|
||||||
|
page (Home, Installation, user showcases, ...) is left untouched.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
generate.py --man-dir man --out-dir <wiki-checkout> [--check]
|
||||||
|
|
||||||
|
Requires: scdoc, pandoc.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Man-page sections that are meaningless on the wiki and must be dropped.
|
||||||
|
DROP_SECTIONS = {"NAME", "FILES", "AUTHOR", "AUTHORS"}
|
||||||
|
|
||||||
|
HEADING_RE = re.compile(r"^(#+)\s+(.*)$")
|
||||||
|
|
||||||
|
|
||||||
|
def sh(cmd, stdin=None):
|
||||||
|
return subprocess.run(
|
||||||
|
cmd, input=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
check=True,
|
||||||
|
).stdout
|
||||||
|
|
||||||
|
|
||||||
|
def man_to_gfm(scd_path):
|
||||||
|
"""scdoc -> roff -> pandoc gfm, as raw markdown text."""
|
||||||
|
roff = sh(["scdoc"], stdin=open(scd_path, "rb").read())
|
||||||
|
gfm = sh(["pandoc", "-f", "man", "-t", "gfm", "--wrap=none"], stdin=roff)
|
||||||
|
return gfm.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def strip_sections(md):
|
||||||
|
"""Remove top-level man-only sections (NAME/FILES/AUTHOR)."""
|
||||||
|
out, drop = [], False
|
||||||
|
for line in md.splitlines():
|
||||||
|
m = HEADING_RE.match(line)
|
||||||
|
if m and len(m.group(1)) == 1:
|
||||||
|
drop = m.group(2).strip().upper() in DROP_SECTIONS
|
||||||
|
if not drop:
|
||||||
|
out.append(line)
|
||||||
|
return "\n".join(out).strip("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def demote(md, levels=1):
|
||||||
|
"""Add `levels` extra '#' to every heading (for aggregated pages)."""
|
||||||
|
out = []
|
||||||
|
for line in md.splitlines():
|
||||||
|
m = HEADING_RE.match(line)
|
||||||
|
out.append("#" * levels + line if m else line)
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def pretty(basename):
|
||||||
|
"""waybar-sway-mode -> sway/mode (submodule heading for aggregated pages)."""
|
||||||
|
return basename.removeprefix("waybar-").replace("-", "/", 1)
|
||||||
|
|
||||||
|
|
||||||
|
def build_page(man_dir, sources, page, extras_dir):
|
||||||
|
parts = []
|
||||||
|
banner = ("<!-- AUTOGENERATED from " + ", ".join(f"man/{s}.5.scd" for s in sources)
|
||||||
|
+ " by .github/wiki/generate.py — DO NOT EDIT HERE."
|
||||||
|
" Edit the man page(s); the wiki syncs automatically. -->")
|
||||||
|
parts.append(banner)
|
||||||
|
aggregated = len(sources) > 1
|
||||||
|
for src in sources:
|
||||||
|
scd = os.path.join(man_dir, src + ".5.scd")
|
||||||
|
body = strip_sections(man_to_gfm(scd))
|
||||||
|
if aggregated:
|
||||||
|
parts.append(f"\n# {pretty(src)}\n\n" + demote(body))
|
||||||
|
else:
|
||||||
|
parts.append("\n" + body)
|
||||||
|
# Optional hand-maintained appendix (screenshots, showcase snippets) that
|
||||||
|
# has no man-page equivalent: .github/wiki/extras/<Page>.md, appended verbatim.
|
||||||
|
extra = os.path.join(extras_dir, page + ".md")
|
||||||
|
if os.path.exists(extra):
|
||||||
|
parts.append("\n" + open(extra).read().strip("\n"))
|
||||||
|
return "\n".join(parts).rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--man-dir", default="man")
|
||||||
|
ap.add_argument("--out-dir")
|
||||||
|
ap.add_argument("--mapping", default=os.path.join(os.path.dirname(__file__), "mapping.json"))
|
||||||
|
ap.add_argument("--check", action="store_true",
|
||||||
|
help="Only validate the mapping vs man/, write nothing.")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
mapping = {k: v for k, v in json.load(open(args.mapping)).items()
|
||||||
|
if not k.startswith("_")}
|
||||||
|
|
||||||
|
# Validate: every man page mapped exactly once; every source exists.
|
||||||
|
mapped, errors = {}, []
|
||||||
|
for page, sources in mapping.items():
|
||||||
|
for s in sources:
|
||||||
|
if not os.path.exists(os.path.join(args.man_dir, s + ".5.scd")):
|
||||||
|
errors.append(f"{page}: man/{s}.5.scd does not exist")
|
||||||
|
if s in mapped:
|
||||||
|
errors.append(f"{s} mapped to both {mapped[s]} and {page}")
|
||||||
|
mapped[s] = page
|
||||||
|
on_disk = {f[:-6] for f in os.listdir(args.man_dir) if f.endswith(".5.scd")}
|
||||||
|
for s in sorted(on_disk - set(mapped)):
|
||||||
|
errors.append(f"man/{s}.5.scd is not referenced in mapping.json")
|
||||||
|
if errors:
|
||||||
|
print("Mapping errors:\n " + "\n ".join(errors), file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"Mapping OK: {len(on_disk)} man pages -> {len(mapping)} wiki pages")
|
||||||
|
if args.check:
|
||||||
|
return 0
|
||||||
|
if not args.out_dir:
|
||||||
|
print("--out-dir is required unless --check", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
extras_dir = os.path.join(os.path.dirname(args.mapping), "extras")
|
||||||
|
os.makedirs(args.out_dir, exist_ok=True)
|
||||||
|
for page, sources in mapping.items():
|
||||||
|
out = os.path.join(args.out_dir, page + ".md")
|
||||||
|
open(out, "w").write(build_page(args.man_dir, sources, page, extras_dir))
|
||||||
|
print(f" wrote {page}.md ({', '.join(sources)})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Maps each wiki page to the ordered list of man-page basenames that compose it. Edit this when adding a module or man page. The generator (generate.py) turns each man/<basename>.5.scd into the wiki page named by the key. Keys use the exact wiki page title (spaces become dashes in the file name, ':' is kept).",
|
||||||
|
|
||||||
|
"Module:-Backlight": ["waybar-backlight"],
|
||||||
|
"Module:-Backlight-Slider": ["waybar-backlight-slider"],
|
||||||
|
"Module:-Battery": ["waybar-battery"],
|
||||||
|
"Module:-Bluetooth": ["waybar-bluetooth"],
|
||||||
|
"Module:-Cava": ["waybar-cava"],
|
||||||
|
"Module:-CFFI": ["waybar-cffi"],
|
||||||
|
"Module:-Clock": ["waybar-clock"],
|
||||||
|
"Module:-CPU": ["waybar-cpu", "waybar-cpu-graph"],
|
||||||
|
"Module:-Custom": ["waybar-custom", "waybar-custom-graph"],
|
||||||
|
"Module:-Disk": ["waybar-disk"],
|
||||||
|
"Module:-Dwl": ["waybar-dwl-tags", "waybar-dwl-window"],
|
||||||
|
"Module:-Gamemode": ["waybar-gamemode"],
|
||||||
|
"Module:-GPS": ["waybar-gps"],
|
||||||
|
"Module:-Hyprland": ["waybar-hyprland-window", "waybar-hyprland-workspaces", "waybar-hyprland-submap", "waybar-hyprland-windowcount"],
|
||||||
|
"Module:-Idle-Inhibitor": ["waybar-idle-inhibitor"],
|
||||||
|
"Module:-Image": ["waybar-image"],
|
||||||
|
"Module:-Inhibitor": ["waybar-inhibitor"],
|
||||||
|
"Module:-JACK": ["waybar-jack"],
|
||||||
|
"Module:-Keyboard-State": ["waybar-keyboard-state"],
|
||||||
|
"Module:-Language": ["waybar-sway-language", "waybar-hyprland-language", "waybar-niri-language", "waybar-mango-language"],
|
||||||
|
"Module:-Mango": ["waybar-mango-window", "waybar-mango-workspaces", "waybar-mango-keymode", "waybar-mango-layout"],
|
||||||
|
"Module:-Memory": ["waybar-memory"],
|
||||||
|
"Module:-Menu": ["waybar-menu"],
|
||||||
|
"Module:-MPD": ["waybar-mpd"],
|
||||||
|
"Module:-MPRIS": ["waybar-mpris"],
|
||||||
|
"Module:-Network": ["waybar-network"],
|
||||||
|
"Module:-Niri": ["waybar-niri-window", "waybar-niri-workspaces"],
|
||||||
|
"Module:-PowerProfilesDaemon": ["waybar-power-profiles-daemon"],
|
||||||
|
"Module:-Privacy": ["waybar-privacy"],
|
||||||
|
"Module:-PulseAudio": ["waybar-pulseaudio"],
|
||||||
|
"Module:-PulseAudio-Slider": ["waybar-pulseaudio-slider"],
|
||||||
|
"Module:-River": ["waybar-river-tags", "waybar-river-mode", "waybar-river-window", "waybar-river-layout"],
|
||||||
|
"Module:-Sndio": ["waybar-sndio"],
|
||||||
|
"Module:-Sway": ["waybar-sway-workspaces", "waybar-sway-window", "waybar-sway-mode", "waybar-sway-scratchpad"],
|
||||||
|
"Module:-Systemd-failed-units": ["waybar-systemd-failed-units"],
|
||||||
|
"Module:-Taskbar": ["waybar-wlr-taskbar"],
|
||||||
|
"Module:-Temperature": ["waybar-temperature"],
|
||||||
|
"Module:-Tray": ["waybar-tray"],
|
||||||
|
"Module:-UPower": ["waybar-upower"],
|
||||||
|
"Module:-User": ["waybar-user"],
|
||||||
|
"Module:-Wayfire": ["waybar-wayfire-window", "waybar-wayfire-workspaces"],
|
||||||
|
"Module:-WirePlumber": ["waybar-wireplumber"],
|
||||||
|
"Module:-Workspaces": ["waybar-ext-workspaces"],
|
||||||
|
"Module:-WWAN": ["waybar-wwan"],
|
||||||
|
"States": ["waybar-states"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Sync wiki from man pages
|
||||||
|
|
||||||
|
# The scdoc man pages under man/ are the single source of truth for module
|
||||||
|
# documentation. This workflow regenerates the corresponding GitHub wiki pages
|
||||||
|
# whenever a man page (or the sync tooling) changes on the default branch.
|
||||||
|
#
|
||||||
|
# Only the pages listed in .github/wiki/mapping.json are (re)written; every
|
||||||
|
# other wiki page (Home, Installation, user showcases, ...) is left untouched.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'man/**'
|
||||||
|
- '.github/wiki/**'
|
||||||
|
- '.github/workflows/wiki.yml'
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: wiki-sync
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install scdoc and pandoc
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y scdoc pandoc
|
||||||
|
|
||||||
|
- name: Clone the wiki
|
||||||
|
run: |
|
||||||
|
git clone \
|
||||||
|
"https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git" \
|
||||||
|
wiki
|
||||||
|
|
||||||
|
- name: Generate wiki pages from man pages
|
||||||
|
run: python3 .github/wiki/generate.py --man-dir man --out-dir wiki
|
||||||
|
|
||||||
|
- name: Commit and push if changed
|
||||||
|
working-directory: wiki
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add -A
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "Wiki already up to date."
|
||||||
|
else
|
||||||
|
git commit -m "docs: sync module pages from man (${{ github.sha }})"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user