From d22103230deb021c0243a8ef19502b7c3771c787 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 21:19:46 +0300 Subject: [PATCH 01/12] docs(spec): mkdocs GitHub Pages migration design Spec for moving docs hosting from ReadTheDocs (httpware.readthedocs.io) to GitHub Pages (httpware.modern-python.org), modeled on modern-di. Single structural PR; clean cutover. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...-06-08-mkdocs-gh-pages-migration-design.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 planning/specs/2026-06-08-mkdocs-gh-pages-migration-design.md diff --git a/planning/specs/2026-06-08-mkdocs-gh-pages-migration-design.md b/planning/specs/2026-06-08-mkdocs-gh-pages-migration-design.md new file mode 100644 index 0000000..d82f44b --- /dev/null +++ b/planning/specs/2026-06-08-mkdocs-gh-pages-migration-design.md @@ -0,0 +1,116 @@ +# Spec: Migrate docs hosting from ReadTheDocs to GitHub Pages + +**Date:** 2026-06-08 +**Topic slug:** `mkdocs-gh-pages-migration` +**Status:** Approved, awaiting plan + +## Goal + +Replace ReadTheDocs (`httpware.readthedocs.io`) with GitHub Pages (`httpware.modern-python.org`) as the authoritative host for the mkdocs site, modeled on `modern-di`'s setup. Single structural PR — docs content unchanged. + +## Motivation + +- Parity with the sibling `modern-di` project, which already runs this pipeline. +- Custom subdomain on `modern-python.org` reads as part of the org rather than a generic ReadTheDocs project. +- Removes ReadTheDocs as a third-party build dependency; the workflow lives in the repo alongside `ci.yml` / `publish.yml`. + +## Non-goals + +- Archiving the ReadTheDocs project on `readthedocs.org` (manual web-UI action; flagged as a post-merge follow-up). +- Changing docs content, navigation, or theme. +- Versioned docs (mkdocs `mike` style). Site stays single-version, latest-only. + +## Architecture + +``` +push to main (docs/** | mkdocs.yml | .github/workflows/docs.yml changed) + │ + ▼ + docs.yml workflow ── checkout (fetch-depth: 0) ── setup-just ── setup-uv + │ + ▼ + just docs-deploy → uvx --with-requirements docs/requirements.txt mkdocs gh-deploy --force + │ + ▼ + mkdocs writes built site to gh-pages branch (force-push) + │ + ▼ + GitHub Pages serves gh-pages, sees docs/CNAME → routes httpware.modern-python.org +``` + +This is a direct mirror of `modern-di`'s pipeline (its current `docs.yml`, last touched 2026-06-07 in commit `a3c5aa7`). + +## File changes + +### Add + +- **`.github/workflows/docs.yml`** — verbatim port of `modern-di/.github/workflows/docs.yml`: + - Triggers: `push` to `main` filtered on `docs/**`, `mkdocs.yml`, `.github/workflows/docs.yml`; plus `workflow_dispatch` for manual reruns. + - `concurrency: { group: docs-deploy, cancel-in-progress: true }` so newer runs supersede older in-flight ones. + - `permissions: contents: write` — required for `mkdocs gh-deploy --force` to push the `gh-pages` branch. + - Single `deploy` job: `actions/checkout@v4` with `fetch-depth: 0`, `extractions/setup-just@v2`, `astral-sh/setup-uv@v3`, `just docs-deploy`. + +- **`docs/CNAME`** — single line `httpware.modern-python.org`. Lives under `docs/` so mkdocs treats it as a static asset and copies it into the built site on every deploy. Without this, every `gh-deploy --force` wipes the custom-domain setting GitHub stores on `gh-pages` (modern-di learned this the hard way in commit `5eac5fa`). + +- **Justfile `docs-deploy` recipe** — append: + ``` + # Force-pushes built site to gh-pages; CI runs this on push to main. + # Manual invocation from a stale checkout will roll the live site back. + docs-deploy: + uvx --with-requirements docs/requirements.txt mkdocs gh-deploy --force + ``` + Uses `uvx` (not `uv run`) so CI doesn't need a full project `uv sync` just to publish docs. The warning comment is intentional — `--force` makes local invocation from an out-of-date `main` destructive. + +### Modify + +- **`mkdocs.yml`** line 2: `site_url: https://httpware.readthedocs.io/` → `site_url: https://httpware.modern-python.org`. +- **`pyproject.toml`** line 42: `docs = "https://httpware.readthedocs.io"` → `docs = "https://httpware.modern-python.org"`. +- **`CONTRIBUTING.md`** line 4: `https://httpware.readthedocs.io/en/latest/dev/contributing/` → `https://httpware.modern-python.org/dev/contributing/`. Drop the `/en/latest/` segment — that's ReadTheDocs versioning; GH Pages serves at the root. +- **`planning/engineering.md`** line 145: replace RTD URL with the new one. +- **`docs/recipes/modern-di.md`** lines 37 and 138: replace `https://modern-di.readthedocs.io/providers/factories/` with `https://modern-di.modern-python.org/providers/factories/`. (modern-di already moved to GH Pages; its RTD URL is dead.) +- **`planning/plans/2026-06-06-modern-di-recipe-plan.md`** lines 207 and 307, and **`planning/specs/2026-06-06-modern-di-recipe-design.md`** line 209: same modern-di URL replacement. + +### Delete + +- **`.readthedocs.yaml`** — RTD config file. After deletion, RTD will keep building from the latest commit it saw, but the project effectively becomes unmaintained. + +### Unchanged + +- **`docs/requirements.txt`** — already `mkdocs` + `mkdocs-material`, identical to modern-di's. Consumed by `uvx --with-requirements`. +- All other files in `docs/`, the existing `ci.yml` and `publish.yml` workflows. + +## Operational prerequisites (NOT code changes — flag in PR description) + +These must happen for the deployed site to actually serve. They are outside the PR's diff but inside the work's scope. + +1. **DNS:** A `CNAME` record for `httpware.modern-python.org` → `modern-python.github.io` must exist before the custom domain resolves. Org-level action on `modern-python.org`'s DNS. +2. **First-deploy bootstrap:** The `gh-pages` branch doesn't exist yet. The first workflow run creates it via `gh-deploy --force`. After it runs, a repo admin must go to **Settings → Pages** and set **Source = Deploy from a branch**, **Branch = `gh-pages` / (root)**. One-time manual step; can't be done from a workflow. +3. **HTTPS:** Once Pages serves the custom domain, tick **Enforce HTTPS** in Settings → Pages (auto-eligible after Let's Encrypt provisions). + +## Post-merge follow-ups (out of scope for this PR) + +1. **Archive the ReadTheDocs project** at `readthedocs.org/projects/httpware/` so `httpware.readthedocs.io` stops serving stale content. Manual web-UI action. +2. Update any external references that point at the old URL (PyPI project page picks up `pyproject.toml` automatically on next publish; GitHub repo "About" sidebar may need a manual nudge). + +## Edge cases and verification + +- **Concurrency:** `cancel-in-progress: true` on the `docs-deploy` group prevents two simultaneous force-pushes racing each other. +- **CNAME preservation:** Verified by inspecting the `gh-pages` branch after the first deploy — `CNAME` must be present at root with the custom domain. +- **Path filter correctness:** The workflow trigger watches `docs/**`, `mkdocs.yml`, and `.github/workflows/docs.yml`. Other changes (code, tests, planning) won't trigger a docs rebuild — matches modern-di's behavior. +- **Manual local rollback risk:** `just docs-deploy` from a developer machine on a stale `main` will roll the live site back. The warning comment in the Justfile is the only mitigation; we accept this trade-off (modern-di does too) because the alternative — locking the recipe behind an env check — adds complexity disproportionate to the risk. + +## Testing + +The workflow itself can't be unit-tested. Verification path: + +1. After merge, watch the first `Deploy Docs` workflow run on `main` succeed. +2. Confirm the `gh-pages` branch exists and contains `CNAME` with the custom domain. +3. Complete the operational prerequisites (DNS + Settings → Pages). +4. Load `https://httpware.modern-python.org/` and verify the rendered site matches the current RTD content. +5. Touch a doc file in a follow-up PR; confirm the workflow re-triggers and the site updates. + +If any step fails, the rollback is to revert the PR — RTD remains live and unaltered, so the old URL keeps serving. + +## Scope check + +This is a single-PR structural change. No decomposition needed. Aligns with the stated preference: clean cutover landing as one structural PR before any substantive follow-up work. From 6a4cfd5253ebf192ea0f73d2149511b7a3aff8f2 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 21:27:05 +0300 Subject: [PATCH 02/12] docs(plan): mkdocs GitHub Pages migration implementation plan Bite-sized task plan derived from the 2026-06-08 spec; 11 tasks covering the workflow, CNAME pinning, Justfile recipe, URL flips, .readthedocs.yaml deletion, and the PR with operational prereqs. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...26-06-08-mkdocs-gh-pages-migration-plan.md | 620 ++++++++++++++++++ 1 file changed, 620 insertions(+) create mode 100644 planning/plans/2026-06-08-mkdocs-gh-pages-migration-plan.md diff --git a/planning/plans/2026-06-08-mkdocs-gh-pages-migration-plan.md b/planning/plans/2026-06-08-mkdocs-gh-pages-migration-plan.md new file mode 100644 index 0000000..be92571 --- /dev/null +++ b/planning/plans/2026-06-08-mkdocs-gh-pages-migration-plan.md @@ -0,0 +1,620 @@ +# mkdocs GitHub Pages Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move httpware's docs hosting from ReadTheDocs to GitHub Pages at `httpware.modern-python.org`, modeled on the `modern-di` pipeline, as a single structural PR. + +**Architecture:** A GitHub Actions workflow (`docs.yml`) runs `just docs-deploy` on every push to `main` that touches docs, mkdocs config, or the workflow itself. The recipe uses `uvx --with-requirements docs/requirements.txt mkdocs gh-deploy --force`, which builds the site and force-pushes it to the `gh-pages` branch. A `docs/CNAME` file pins the custom domain across deploys. ReadTheDocs config is deleted as part of the cutover. + +**Tech Stack:** GitHub Actions, mkdocs + mkdocs-material, `just`, `uv` / `uvx`, GitHub Pages. + +**Spec:** [`planning/specs/2026-06-08-mkdocs-gh-pages-migration-design.md`](../specs/2026-06-08-mkdocs-gh-pages-migration-design.md) + +--- + +## Working assumptions + +- You are working on a branch off `main` (not directly on `main`). If unsure, create one: `git checkout -b docs/migrate-to-gh-pages`. +- `just`, `uv` (which provides `uvx`), and Python ≥3.11 are installed locally. +- DNS for `httpware.modern-python.org` and the Settings → Pages bootstrap are OPERATIONAL prerequisites and are NOT part of this plan — they're called out in the spec for the PR description. +- The current working directory throughout the plan is the repo root: `/Users/kevinsmith/src/pypi/httpware` (or your equivalent checkout). + +## File map + +**Create:** +- `docs/CNAME` — pins the custom domain. +- `.github/workflows/docs.yml` — GitHub Actions deploy workflow. + +**Modify:** +- `Justfile` — add `docs-deploy` recipe. +- `mkdocs.yml` — flip `site_url`. +- `pyproject.toml` — flip `docs` project URL. +- `CONTRIBUTING.md` — flip docs link, drop RTD versioning path. +- `planning/engineering.md` — flip RTD URL. +- `docs/recipes/modern-di.md` — flip two dead `modern-di.readthedocs.io` URLs. +- `planning/plans/2026-06-06-modern-di-recipe-plan.md` — flip two dead modern-di URLs. +- `planning/specs/2026-06-06-modern-di-recipe-design.md` — flip one dead modern-di URL. + +**Delete:** +- `.readthedocs.yaml`. + +--- + +## Task 1: Add the `docs-deploy` recipe to the Justfile + +**Files:** +- Modify: `Justfile` + +- [ ] **Step 1: Append the recipe to the Justfile** + +Append to the bottom of `Justfile` (after the existing `publish:` recipe): + +``` +# Force-pushes built site to gh-pages; CI runs this on push to main. +# Manual invocation from a stale checkout will roll the live site back. +docs-deploy: + uvx --with-requirements docs/requirements.txt mkdocs gh-deploy --force +``` + +The comment is intentional — `--force` makes local invocation destructive if `main` is stale. + +- [ ] **Step 2: Verify the recipe is registered** + +Run: `just --list` + +Expected output includes: +``` +Available recipes: + default + docs-deploy # Force-pushes built site to gh-pages; CI runs this on push to main. + install + lint + lint-ci + publish + test *args + test-branch +``` + +- [ ] **Step 3: Smoke-test that the mkdocs build still succeeds with the current config** + +This catches any pre-existing site-build failure before the cutover changes. Run: + +``` +uvx --with-requirements docs/requirements.txt mkdocs build --strict --site-dir /tmp/httpware-docs-smoke +``` + +Expected: `INFO - Documentation built in s` with no warnings (because `--strict`). + +If this fails with broken-link warnings unrelated to this PR, STOP and surface them — that's a pre-existing problem, not a migration issue. + +- [ ] **Step 4: Commit** + +```bash +git add Justfile +git commit -m "build(just): add docs-deploy recipe for mkdocs gh-deploy" +``` + +--- + +## Task 2: Add `docs/CNAME` to pin the custom domain + +**Files:** +- Create: `docs/CNAME` + +- [ ] **Step 1: Create the file** + +Content of `docs/CNAME` (single line, no trailing data): + +``` +httpware.modern-python.org +``` + +- [ ] **Step 2: Verify mkdocs treats it as a static asset** + +Run: `uvx --with-requirements docs/requirements.txt mkdocs build --strict --site-dir /tmp/httpware-docs-smoke` + +Then check the build output includes the CNAME file at the root: + +```bash +test -f /tmp/httpware-docs-smoke/CNAME && cat /tmp/httpware-docs-smoke/CNAME +``` + +Expected: prints `httpware.modern-python.org`. If the file is absent, mkdocs is not copying it — abort and investigate before continuing. + +- [ ] **Step 3: Commit** + +```bash +git add docs/CNAME +git commit -m "docs(cname): pin httpware.modern-python.org for GitHub Pages" +``` + +--- + +## Task 3: Flip `site_url` in `mkdocs.yml` + +**Files:** +- Modify: `mkdocs.yml` line 2 + +- [ ] **Step 1: Edit the file** + +Change line 2 of `mkdocs.yml` from: + +```yaml +site_url: https://httpware.readthedocs.io/ +``` + +to: + +```yaml +site_url: https://httpware.modern-python.org +``` + +(No trailing slash, matching the modern-di convention.) + +- [ ] **Step 2: Verify the build still passes and the new URL is baked in** + +Run: `uvx --with-requirements docs/requirements.txt mkdocs build --strict --site-dir /tmp/httpware-docs-smoke` + +Expected: build succeeds. Then confirm the new URL is embedded: + +```bash +grep -c "httpware.modern-python.org" /tmp/httpware-docs-smoke/index.html +``` + +Expected: a positive integer (the URL appears in canonical/og meta tags). + +```bash +grep -c "httpware.readthedocs.io" /tmp/httpware-docs-smoke/index.html +``` + +Expected: `0`. If positive, find the leftover reference and resolve it before proceeding. + +- [ ] **Step 3: Commit** + +```bash +git add mkdocs.yml +git commit -m "docs(mkdocs): switch site_url to httpware.modern-python.org" +``` + +--- + +## Task 4: Update the `docs` project URL in `pyproject.toml` + +**Files:** +- Modify: `pyproject.toml` line 42 + +- [ ] **Step 1: Edit the file** + +Change line 42 of `pyproject.toml` from: + +```toml +docs = "https://httpware.readthedocs.io" +``` + +to: + +```toml +docs = "https://httpware.modern-python.org" +``` + +- [ ] **Step 2: Verify the project still resolves** + +Run: `uv lock --check` + +Expected: exits 0 with no output (or a brief "Resolved N packages in