diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..b72663b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,28 @@ +name: Deploy Docs + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + - ".github/workflows/docs.yml" + workflow_dispatch: + +concurrency: + group: docs-deploy + cancel-in-progress: true + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + - run: just docs-deploy diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index ccc1ee6..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,13 +0,0 @@ -version: 2 - -build: - os: "ubuntu-22.04" - tools: - python: "3.12" - -python: - install: - - requirements: docs/requirements.txt - -mkdocs: - configuration: mkdocs.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0bfbb37..8dc80b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing The contributing guide is published as part of the project documentation: -**https://httpware.readthedocs.io/en/latest/dev/contributing/** +**https://httpware.modern-python.org/dev/contributing/** Source: [`docs/dev/contributing.md`](docs/dev/contributing.md). diff --git a/Justfile b/Justfile index 493f3b3..7d9310e 100644 --- a/Justfile +++ b/Justfile @@ -29,3 +29,8 @@ publish: uv version $GITHUB_REF_NAME uv build uv publish --token $PYPI_TOKEN + +# 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 diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..2eef52d --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +httpware.modern-python.org diff --git a/docs/recipes/modern-di.md b/docs/recipes/modern-di.md index bac0a30..35eb0bb 100644 --- a/docs/recipes/modern-di.md +++ b/docs/recipes/modern-di.md @@ -1,6 +1,6 @@ # Wiring `AsyncClient` into `modern-di` -If you wire your app's dependencies with [`modern-di`](https://modern-di.readthedocs.io/) and want connection-pool teardown and middleware composition to flow through the container's lifecycle, this is the bridge. Both libraries ship under the [`modern-python`](https://github.com/modern-python) org. +If you wire your app's dependencies with [`modern-di`](https://modern-di.modern-python.org/) and want connection-pool teardown and middleware composition to flow through the container's lifecycle, this is the bridge. Both libraries ship under the [`modern-python`](https://github.com/modern-python) org. ## The minimal wire-up @@ -34,7 +34,7 @@ Breaking that down: A common first instinct here is `finalizer=lambda c: c.aclose()`. **That does not work** — the lambda itself is sync, so `modern-di` calls it synchronously and discards the returned coroutine unawaited. The underlying connection pool leaks. Pass the unbound async method directly, or wrap in `async def`. -See the [`modern-di` factories docs](https://modern-di.readthedocs.io/providers/factories/) for the broader `CacheSettings` story (scopes, `clear_cache`, sync vs async finalizers). +See the [`modern-di` factories docs](https://modern-di.modern-python.org/providers/factories/) for the broader `CacheSettings` story (scopes, `clear_cache`, sync vs async finalizers). ## Adding a second backend hits a type collision @@ -135,4 +135,4 @@ Each cached singleton owns its own `AsyncBulkhead` and `AsyncRetry` state — wh - **[Quick-Start](../index.md)** — the base `AsyncClient` API. - **[Middleware guide](../middleware.md)** — what `AsyncBulkhead` and `AsyncRetry` are doing in `kwargs[middleware]`. - **[Resilience reference](../resilience.md)** — every parameter on `AsyncRetry`, `RetryBudget`, `AsyncBulkhead`. -- **[`modern-di` factories](https://modern-di.readthedocs.io/providers/factories/)** — `CacheSettings`, scopes, the broader provider story. +- **[`modern-di` factories](https://modern-di.modern-python.org/providers/factories/)** — `CacheSettings`, scopes, the broader provider story. diff --git a/mkdocs.yml b/mkdocs.yml index 5456fdb..24f9cba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: httpware -site_url: https://httpware.readthedocs.io/ +site_url: https://httpware.modern-python.org repo_url: https://github.com/modern-python/httpware docs_dir: docs edit_uri: edit/main/docs/ diff --git a/planning/engineering.md b/planning/engineering.md index 2f7c16d..21cb66c 100644 --- a/planning/engineering.md +++ b/planning/engineering.md @@ -142,7 +142,7 @@ Post-pivot, the roadmap has three categories. Topic slugs in `planning/specs/` a - **v0.8.0:** sync `Client` with full feature parity (middleware chain, decoder seam, `Retry`, `Bulkhead`, `stream()`); async surface renamed to `Async*`/`async_*` prefix; `attempt_timeout=` removed from `AsyncRetry`. Breaking release for every public async middleware import. - **Epic 4 — Streaming: SHIPPED in v0.5.** `AsyncClient.stream()` context manager + Retry refuses streamed-body requests. See [`planning/archive/specs/2026-06-05-streaming-design.md`](archive/specs/2026-06-05-streaming-design.md) and [`planning/archive/plans/2026-06-05-streaming-plan.md`](archive/plans/2026-06-05-streaming-plan.md). - **Epic 5 — Observability: SHIPPED in v0.6** — re-scoped from the original 4-story plan. `Retry` and `Bulkhead` emit operational events via stdlib `logging` + opt-in OpenTelemetry span events. Stories `5-1` (Layer 1 middleware hooks) and `5-4` (standalone OTel middleware) RETIRED — `opentelemetry-instrumentation-httpx` already covers transport-level tracing; a separate httpware middleware would duplicate it. See [`planning/archive/specs/2026-06-05-observability-design.md`](archive/specs/2026-06-05-observability-design.md) and [`planning/archive/plans/2026-06-05-observability-plan.md`](archive/plans/2026-06-05-observability-plan.md). -- **Epic 6 — Ship v1.0: SHIPPED.** `6-2` docs site live at (mkdocs-material, hand-written content only, auto-publishing from `main`). Stories `6-3` (benchmarks) and `6-5` (Trusted Publishers + Sigstore release flow) RETIRED — neither carries enough value to maintain. Tag-driven release via the existing `publish.yml` workflow stays as-is. +- **Epic 6 — Ship v1.0: SHIPPED.** `6-2` docs site live at (mkdocs-material, hand-written content only, auto-publishing from `main`). Stories `6-3` (benchmarks) and `6-5` (Trusted Publishers + Sigstore release flow) RETIRED — neither carries enough value to maintain. Tag-driven release via the existing `publish.yml` workflow stays as-is. - **Carry-forward decoder:** `1-6` msgspec decoder via extras — second `ResponseDecoder` adapter, already implemented; verified surviving in the pivot. - **Middleware protocol:** `2-1` and `2-2` already implemented in the pivot (protocol, chain, phase decorators). diff --git a/planning/plans/2026-06-06-modern-di-recipe-plan.md b/planning/plans/2026-06-06-modern-di-recipe-plan.md index 2f2a9d3..3c692a9 100644 --- a/planning/plans/2026-06-06-modern-di-recipe-plan.md +++ b/planning/plans/2026-06-06-modern-di-recipe-plan.md @@ -170,7 +170,7 @@ Write `docs/recipes/modern-di.md` with the full content below. ````markdown # Wiring `AsyncClient` into `modern-di` -If you wire your app's dependencies with [`modern-di`](https://modern-di.readthedocs.io/) and want connection-pool teardown and middleware composition to flow through the container's lifecycle, this is the bridge. Both libraries ship under the [`modern-python`](https://github.com/modern-python) org. +If you wire your app's dependencies with [`modern-di`](https://modern-di.modern-python.org/) and want connection-pool teardown and middleware composition to flow through the container's lifecycle, this is the bridge. Both libraries ship under the [`modern-python`](https://github.com/modern-python) org. ## The minimal wire-up @@ -204,7 +204,7 @@ Breaking that down: A common first instinct here is `finalizer=lambda c: c.aclose()`. **That does not work** — the lambda itself is sync, so `modern-di` calls it synchronously and discards the returned coroutine unawaited. The underlying connection pool leaks. Pass the unbound async method directly, or wrap in `async def`. -See the [`modern-di` factories docs](https://modern-di.readthedocs.io/providers/factories/) for the broader `CacheSettings` story (scopes, `clear_cache`, sync vs async finalizers). +See the [`modern-di` factories docs](https://modern-di.modern-python.org/providers/factories/) for the broader `CacheSettings` story (scopes, `clear_cache`, sync vs async finalizers). ## Adding a second backend hits a type collision @@ -304,7 +304,7 @@ Each cached singleton owns its own `Bulkhead` and `Retry` state — what you wan - **[Quick-Start](../index.md)** — the base `AsyncClient` API. - **[Middleware guide](../middleware.md)** — what `Bulkhead` and `Retry` are doing in `kwargs[middleware]`. - **[Resilience reference](../resilience.md)** — every parameter on `Retry`, `RetryBudget`, `Bulkhead`. -- **[`modern-di` factories](https://modern-di.readthedocs.io/providers/factories/)** — `CacheSettings`, scopes, the broader provider story. +- **[`modern-di` factories](https://modern-di.modern-python.org/providers/factories/)** — `CacheSettings`, scopes, the broader provider story. ```` - [ ] **Step 2: Update `mkdocs.yml` to add the `Recipes` nav section** 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