docs: publish docs/ to the GitHub wiki and reorganise the user guide

- scripts/publish-wiki.sh publishes every page listed in SECTIONS, copies
  images, writes a sectioned sidebar and footer, and removes stale pages.
  --check fails on links to missing files or headings and on docs pages
  that are neither published nor excluded.
- New wiki.yml runs the check on pull requests and pushes to main, and
  publishes when release.yml calls it or when started by hand.
- Split USER_GUIDE.md into topic pages (files, synced folders, photos,
  sharing, computers, launcher and tray); add Getting started and FAQ.
- Make the COPR repository the main Fedora path, with update and
  uninstall steps for every package type.
- Fix the stale CAPTCHA note in CLI.md, document pdfs unlock and lock,
  and drop the broken AGENTS.md link from CONTRIBUTING.md.
This commit is contained in:
nvrl committed 2026-10-17 13:57:55 +02:00
1 parent d1d4d23dd5
commit 9c30ff8276
24 files changed
+1503 -724

No files matched your search

+211 -68
View File
@@ -1,32 +1,40 @@
#!/usr/bin/env bash
# Publish the user documentation in docs/ to the GitHub wiki.
# Publish the documentation in docs/ to the GitHub wiki.
#
# The pages in docs/ are the source of truth; the wiki is a generated copy for
# people who look there first. Each run clones the wiki, replaces the pages
# listed below, rewrites links between them to wiki page names and every other
# repository link to an absolute GitHub URL, regenerates Home and _Sidebar, and
# commits. Nothing is pushed without --push.
# people who look there first. Each run checks every link in docs/, clones the
# wiki, replaces all of its pages with the ones listed in SECTIONS below,
# rewrites links between them to wiki page names and every other repository
# link to an absolute GitHub URL, copies the images they show, writes the
# sidebar and footer, and commits. Nothing is pushed without --push.
#
# --check only checks the links and that every page in docs/ is listed or
# excluded, without touching the wiki. CI runs it for every change to docs/.
#
# GitHub creates the wiki repository only when the first page is saved, so
# create any page once in the web UI before the first run.
# create any page once in the web UI before the first run. WIKI_REMOTE
# overrides the remote, which is how the workflow pushes with its token.
#
# usage: scripts/publish-wiki.sh [--push] [WORKDIR]
# usage: scripts/publish-wiki.sh [--check | --push] [WORKDIR]
set -euo pipefail
repo_url="https://github.com/narrrl/proton-drive-linux"
wiki_remote="git@github.com:narrrl/proton-drive-linux.wiki.git"
wiki_remote="${WIKI_REMOTE:-git@github.com:narrrl/proton-drive-linux.wiki.git}"
push=0
if [ "${1:-}" = "--push" ]; then
push=1
shift
fi
mode=commit
case "${1:-}" in
--check) mode=check; shift ;;
--push) mode=push; shift ;;
esac
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
workdir="${1:-$(mktemp -d)}"
wiki="$workdir/wiki"
if [ -d "$wiki/.git" ]; then
if [ "$mode" = check ]; then
wiki="$workdir/check"
mkdir -p "$wiki"
elif [ -d "$wiki/.git" ]; then
git -C "$wiki" pull --ff-only --quiet
else
git clone --quiet "$wiki_remote" "$wiki"
@@ -35,88 +43,223 @@ fi
rev="$(git -C "$root" rev-parse --short HEAD)"
version="$(grep -m1 '^version' "$root/Cargo.toml" | cut -d'"' -f2)"
python3 - "$root" "$wiki" "$repo_url" "$version" <<'EOF'
python3 - "$root" "$wiki" "$repo_url" "$version" "$rev" <<'EOF'
import pathlib
import re
import shutil
import sys
root, wiki, repo_url, version = sys.argv[1:]
root, wiki, repo_url, version, rev = sys.argv[1:]
root = pathlib.Path(root)
wiki = pathlib.Path(wiki)
docs = root / "docs"
# docs/ file -> wiki page name, in sidebar order.
PAGES = {
"INSTALL.md": "Installation",
"USER_GUIDE.md": "User-Guide",
"CLI.md": "Command-Line-Reference",
"CONFIGURATION.md": "Configuration",
"TROUBLESHOOTING.md": "Troubleshooting",
"RECOVERY.md": "Recovery",
}
HOME = ("README.md", "Home")
LINK = re.compile(r"(\]\()([^)\s]+)(\))")
# Sidebar sections: docs/ file -> wiki page name, in order.
SECTIONS = [
("Start here", [
("GETTING_STARTED.md", "Getting-Started"),
("INSTALL.md", "Installation"),
("FAQ.md", "FAQ"),
]),
("Using it", [
("USER_GUIDE.md", "User-Guide"),
("FILES.md", "Files-and-Offline"),
("SYNCED_FOLDERS.md", "Synced-Folders"),
("PHOTOS.md", "Photos"),
("SHARING.md", "Sharing"),
("COMPUTERS.md", "Computers"),
("LAUNCHER_AND_TRAY.md", "Launcher-and-Tray"),
]),
("Reference", [
("CLI.md", "Command-Line-Reference"),
("CONFIGURATION.md", "Configuration"),
("SECURITY_MODEL.md", "Security-Model"),
]),
("When something goes wrong", [
("TROUBLESHOOTING.md", "Troubleshooting"),
("RECOVERY.md", "Recovery"),
]),
("Contributing", [
("DEVELOPMENT.md", "Development"),
("ARCHITECTURE.md", "Architecture"),
("TESTING.md", "Testing"),
("TRANSLATING.md", "Translating"),
("ROADMAP.md", "Roadmap"),
]),
]
# Too long or too internal for the wiki; links to them go to GitHub.
EXCLUDED = {"BUGS.md", "CHANGELOG.md"}
# Files outside docs/ whose links are checked but not published.
CHECKED = ["README.md", "CONTRIBUTING.md", "SECURITY.md"]
PAGES = dict([HOME] + [page for _, pages in SECTIONS for page in pages])
images_url = f"https://raw.githubusercontent.com/wiki/{repo_url.removeprefix('https://github.com/')}"
LINK = re.compile(r"(\]\(|src=\")([^)\"\s]+)([)\"])")
FENCE = re.compile(r"^\s*(```|~~~)")
INLINE_CODE = re.compile(r"(`[^`\n]*`)")
HEADING = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
EXPLICIT_ID = re.compile(r"<a\s+id=\"([^\"]+)\"")
errors = []
images = set()
def rewrite(target: str) -> str:
if re.match(r"^[a-z]+:|^#", target):
def outside_fences(text):
"""Yield (is_prose, chunk) so links in code blocks are left alone."""
chunk, fenced = [], False
for line in text.splitlines(keepends=True):
if FENCE.match(line):
if chunk:
yield not fenced, "".join(chunk)
chunk, fenced = [], not fenced
yield False, line
continue
chunk.append(line)
if chunk:
yield not fenced, "".join(chunk)
def slug(heading):
"""GitHub's anchor for a heading."""
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", heading)
return re.sub(r"[^\w\- ]", "", text.lower()).replace(" ", "-")
anchor_cache = {}
def anchors(path):
if path not in anchor_cache:
found, seen = set(), {}
for prose, chunk in outside_fences(path.read_text()):
if not prose:
continue
for line in chunk.splitlines():
found.update(EXPLICIT_ID.findall(line))
m = HEADING.match(line)
if not m:
continue
base = slug(m.group(2))
n = seen.get(base, 0)
seen[base] = n + 1
found.add(base if n == 0 else f"{base}-{n}")
anchor_cache[path] = found
return anchor_cache[path]
def rewrite(source, target):
"""The wiki form of one link in source, recording broken links."""
if re.match(r"^[a-z]+:", target):
return target
path, _, anchor = target.partition("#")
resolved = (source.parent / path).resolve() if path else source
where = f"{source.relative_to(root)}: {target}"
if not resolved.is_relative_to(root) or not resolved.exists():
errors.append(f"{where}: no such file")
return target
if anchor and resolved.suffix == ".md" and anchor not in anchors(resolved):
errors.append(f"{where}: no heading #{anchor}")
relative = resolved.relative_to(root)
anchor = f"#{anchor}" if anchor else ""
if path in PAGES:
return PAGES[path] + anchor
# Anything else is a file in the repository: link to it on GitHub.
resolved = (root / "docs" / path).resolve().relative_to(root)
kind = "tree" if (root / resolved).is_dir() else "blob"
return f"{repo_url}/{kind}/main/{resolved}{anchor}"
if not path:
return anchor
if relative.parent == pathlib.Path("docs") and relative.name in PAGES:
return PAGES[relative.name] + anchor
if relative.parts[0] == "images":
images.add(relative)
return f"{images_url}/{relative}"
kind = "tree" if resolved.is_dir() else "blob"
return f"{repo_url}/{kind}/main/{relative}{anchor}"
def convert(text: str) -> str:
return LINK.sub(lambda m: m.group(1) + rewrite(m.group(2)) + m.group(3), text)
def convert(source):
out = []
for prose, chunk in outside_fences(source.read_text()):
if prose:
# Odd pieces are `inline code`, which is left alone too.
pieces = INLINE_CODE.split(chunk)
pieces[::2] = [
LINK.sub(lambda m: m.group(1) + rewrite(source, m.group(2)) + m.group(3), p)
for p in pieces[::2]
]
chunk = "".join(pieces)
out.append(chunk)
return "".join(out)
def title(source):
return (docs / source).read_text().splitlines()[0].lstrip("# ").strip()
def without_title(text):
lines = text.splitlines(keepends=True)
if lines and lines[0].startswith("# "):
lines = lines[1:]
while lines and not lines[0].strip():
lines = lines[1:]
return "".join(lines)
for name in sorted(p.name for p in docs.glob("*.md")):
if name not in PAGES and name not in EXCLUDED:
errors.append(f"docs/{name}: add it to SECTIONS or EXCLUDED in scripts/publish-wiki.sh")
converted = {source: convert(docs / source) for source in PAGES}
published_images = set(images)
for name in CHECKED:
convert(root / name)
if errors:
print("Broken documentation links:", file=sys.stderr)
for error in errors:
print(f" {error}", file=sys.stderr)
sys.exit(1)
# The wiki is generated: anything not written below is stale.
for old in wiki.glob("*.md"):
old.unlink()
shutil.rmtree(wiki / "images", ignore_errors=True)
note = (
"<!-- Generated from docs/ in the repository by scripts/publish-wiki.sh."
" Edit the source there; changes made here are overwritten. -->\n\n"
)
for source, page in PAGES.items():
text = (root / "docs" / source).read_text()
(wiki / f"{page}.md").write_text(note + convert(text))
(wiki / f"{page}.md").write_text(note + without_title(converted[source]))
for image in published_images:
(wiki / image).parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(root / image, wiki / image)
def title(source: str) -> str:
return (root / "docs" / source).read_text().splitlines()[0].lstrip("# ").strip()
home = [
note,
"# Proton Drive for Linux\n\n",
"An unofficial Proton Drive client for the Linux desktop: your files as a folder that ",
"downloads on demand, synced local folders, Photos, sharing, a GTK4 app with tray and search ",
"launcher, and a scriptable CLI.\n\n",
f"This wiki mirrors the documentation of version {version}. The source, releases and issue ",
f"tracker are in the [repository]({repo_url}).\n\n",
"## Pages\n\n",
]
home += [f"- [{title(source)}]({page})\n" for source, page in PAGES.items()]
home += [
"\n## More\n\n",
f"- [Changelog]({repo_url}/blob/main/docs/CHANGELOG.md)\n",
f"- [Architecture]({repo_url}/blob/main/docs/ARCHITECTURE.md)\n",
f"- [Contributing]({repo_url}/blob/main/CONTRIBUTING.md)\n",
f"- [Report a problem]({repo_url}/issues/new/choose)\n",
]
(wiki / "Home.md").write_text("".join(home))
sidebar = ["**[Home](Home)**\n\n"]
sidebar += [f"- [{title(source)}]({page})\n" for source, page in PAGES.items()]
sidebar = ["**[Home](Home)**\n"]
for section, pages in SECTIONS:
sidebar.append(f"\n**{section}**\n\n")
sidebar += [f"- [{title(source)}]({page})\n" for source, page in pages]
sidebar += [
"\n---\n\n",
f"[Changelog]({repo_url}/blob/main/docs/CHANGELOG.md) · ",
f"[Releases]({repo_url}/releases) · [Issues]({repo_url}/issues)\n",
]
(wiki / "_Sidebar.md").write_text("".join(sidebar))
(wiki / "_Sidebar.md").write_text(note + "".join(sidebar))
(wiki / "_Footer.md").write_text(
note
+ f"Documentation of Proton Drive for Linux {version}, generated from "
+ f"[`docs/`]({repo_url}/tree/{rev}/docs) at `{rev}`. "
+ f"To change a page, edit it in the [repository]({repo_url}).\n"
)
EOF
if [ "$mode" = check ]; then
echo "Documentation links are fine."
exit 0
fi
cd "$wiki"
git add -A
if git diff --cached --quiet; then
@@ -126,9 +269,9 @@ fi
git commit --quiet -m "Update from docs at $rev"
git show --stat --format='%s' HEAD
if [ "$push" = 1 ]; then
if [ "$mode" = push ]; then
git push --quiet
echo "Pushed to $wiki_remote."
echo "Pushed to the wiki."
else
echo "Committed in $wiki. Review it, then push from there or re-run with --push."
fi