diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index eef2579..c56dc4c 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,34 +5,52 @@ on: branches: [main] paths: - 'site/**' + - 'scripts/check_site.py' + - '.github/workflows/pages.yml' + pull_request: + paths: + - 'site/**' + - 'scripts/check_site.py' - '.github/workflows/pages.yml' workflow_dispatch: -# Allow the workflow to publish to GitHub Pages. permissions: contents: read - pages: write - id-token: write -# One deployment at a time; queued runs supersede each other. concurrency: - group: pages + group: pages-${{ github.ref }} cancel-in-progress: false jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check documentation links and examples + shell: bash + run: | + mkdir -p artifacts/logs + python3 scripts/check_site.py 2>&1 | tee artifacts/logs/site-check.log + deploy: + needs: validate + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Configure Pages uses: actions/configure-pages@v5 - name: Upload site - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: site diff --git a/README.md b/README.md index bb2cfa5..0762786 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ single, immutable point of truth that can be shared across modules. Documentation: +Usage guide: — a runnable +Python application, typed accessors, INI sections and troubleshooting. + +For local preview and publishing instructions, see [Maintaining the documentation site](docs/github-pages.md). + ## Motivation @@ -299,7 +304,7 @@ config.getdict("EXTRA") # dict An optional `fallback` argument is returned when the key is not found: ```python -config.get("MISSING_KEY", "default_value") +config.get("MISSING_KEY", fallback="default_value") ``` ### Initialization diff --git a/docs/github-pages.md b/docs/github-pages.md new file mode 100644 index 0000000..1f39a0e --- /dev/null +++ b/docs/github-pages.md @@ -0,0 +1,49 @@ +# Maintaining the GitHub Pages site + +The published documentation lives in `site/`: + +- `index.html`: installation, CLI and configuration reference. +- `usage.html`: runnable application examples and troubleshooting. +- `styles.css`: shared responsive styles, including dark mode. + +The site uses static HTML and CSS, with no build dependencies. Use relative links +so navigation also works under the repository's GitHub Pages URL prefix. + +## Preview and validation + +Run from the repository root: + +```bash +python3 -m http.server 8000 --directory site --bind 127.0.0.1 +``` + +Open or . + +In another terminal, validate links, duplicate IDs and Python snippet syntax: + +```bash +mkdir -p artifacts/logs +python3 scripts/check_site.py > artifacts/logs/site-check.log 2>&1 +``` + +The checker uses only Python's standard library (Python 3.10+). It does not +check external URLs or execute the snippets. When changing runnable examples, +also run them in a temporary project with NavConfig installed. + +## Publishing + +1. In repository **Settings → Pages → Build and deployment**, select **GitHub Actions** + as the source. +2. Merge the site changes into `main`. The **Deploy GitHub Pages** workflow validates + the site, uploads only `site/` and deploys it to the `github-pages` environment. +3. Open the URL reported by the deployment job. For this repository, the README + uses ; use the URL GitHub reports if + repository capitalization or Pages configuration differs. + +Pull requests validate the site without deployment permissions. You can also run +the workflow manually from **Actions → Deploy GitHub Pages → Run workflow**; +select `main` to publish. Other branches only run validation. If the repository +uses environment protection rules, `github-pages` must allow deployments from `main`. + +See GitHub's [custom Pages workflow documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages) +for repository setup and deployment requirements. diff --git a/pyproject.toml b/pyproject.toml index e9082ff..46797fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ kardex = "navconfig.cli:main" [project.urls] Homepage = "https://github.com/phenobarbital/navconfig" Repository = "https://github.com/phenobarbital/navconfig" -Documentation = "https://github.com/phenobarbital/navconfig" +Documentation = "https://phenobarbital.github.io/navconfig/" "Bug Tracker" = "https://github.com/phenobarbital/navconfig/issues" Funding = "https://paypal.me/phenobarbital" "Say Thanks!" = "https://saythanks.io/to/phenobarbital" diff --git a/scripts/check_site.py b/scripts/check_site.py new file mode 100644 index 0000000..b9ded9b --- /dev/null +++ b/scripts/check_site.py @@ -0,0 +1,92 @@ +"""Check static documentation links and Python snippet syntax without dependencies.""" + +import ast +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import unquote, urlsplit + + +class Page(HTMLParser): + """Collect local link targets and executable Python examples.""" + + def __init__(self, path: Path) -> None: + super().__init__(convert_charrefs=True) + self.ids: set[str] = set() + self.links: list[str] = [] + self.errors: list[str] = [] + self.snippets: list[str] = [] + self._python = False + self._code: list[str] = [] + self.feed(path.read_text(encoding="utf-8")) + self.close() + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + attributes = dict(attrs) + if identifier := attributes.get("id"): + if identifier in self.ids: + self.errors.append(f"duplicate id: {identifier}") + self.ids.add(identifier) + for key in ("href", "src"): + if target := attributes.get(key): + self.links.append(target) + if ( + tag == "code" + and "language-python" in (attributes.get("class") or "").split() + ): + self._python = True + self._code = [] + + def handle_data(self, data: str) -> None: + if self._python: + self._code.append(data) + + def handle_endtag(self, tag: str) -> None: + if tag == "code" and self._python: + self.snippets.append("".join(self._code)) + self._python = False + + +def main() -> int: + root = Path(__file__).resolve().parents[1] / "site" + pages = {path.resolve(): Page(path) for path in sorted(root.rglob("*.html"))} + errors: list[str] = [] + if root / "index.html" not in pages: + errors.append("site/index.html is missing") + for path, page in pages.items(): + label = path.relative_to(root) + errors.extend(f"{label}: {error}" for error in page.errors) + for snippet in page.snippets: + try: + ast.parse(snippet) + except SyntaxError as error: + errors.append(f"{label}: invalid Python example: {error}") + for link in page.links: + url = urlsplit(link) + if url.scheme or url.netloc: + continue + if url.path.startswith("/"): + errors.append(f"{label}: use a project-relative URL: {link}") + continue + target = (path.parent / unquote(url.path)).resolve() if url.path else path + if not target.is_relative_to(root): + errors.append(f"{label}: link escapes the published site: {link}") + continue + if target.is_dir(): + target /= "index.html" + if not target.is_file(): + errors.append(f"{label}: missing target: {link}") + elif url.fragment and target in pages: + if unquote(url.fragment) not in pages[target].ids: + errors.append(f"{label}: missing fragment: {link}") + if errors: + print("\n".join(errors)) + return 1 + snippets = sum(len(page.snippets) for page in pages.values()) + print( + f"Checked {len(pages)} HTML pages: local links, IDs and {snippets} Python snippets OK." + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/site/index.html b/site/index.html index f1b7e7d..a78d20b 100644 --- a/site/index.html +++ b/site/index.html @@ -5,105 +5,10 @@ NavConfig — Universal Configuration Management for Python - + +
@@ -112,16 +17,20 @@

NavConfig

INI/TOML/YAML, Redis and HashiCorp Vault behind a single, shared point of truth.

MIT License - Python ≥ 3.10 - v3.0 + Python ≥ 3.10.1 GitHub PyPI
+
-