diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f222f9..9c226fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,9 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: extractions/setup-just@v2 - - uses: astral-sh/setup-uv@v3 + - uses: actions/checkout@v6 + - uses: extractions/setup-just@v4 + - uses: astral-sh/setup-uv@v8.2.0 with: enable-cache: true cache-dependency-glob: "**/pyproject.toml" @@ -46,8 +46,8 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.2.0 with: enable-cache: true cache-dependency-glob: "**/pyproject.toml" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..1a84455 --- /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@v6 + with: + fetch-depth: 0 + - uses: extractions/setup-just@v4 + - uses: astral-sh/setup-uv@v8.2.0 + - run: just docs-deploy diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b637272..916fc36 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,9 +9,9 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: extractions/setup-just@v2 - - uses: astral-sh/setup-uv@v3 + - uses: actions/checkout@v6 + - uses: extractions/setup-just@v4 + - uses: astral-sh/setup-uv@v8.2.0 - run: just publish env: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 571df87..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,13 +0,0 @@ -version: 2 - -build: - os: "ubuntu-22.04" - tools: - python: "3.10" - -python: - install: - - requirements: docs/requirements.txt - -mkdocs: - configuration: mkdocs.yml diff --git a/Justfile b/Justfile index 307bcee..e209b3b 100644 --- a/Justfile +++ b/Justfile @@ -33,3 +33,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/README.md b/README.md index aebe4d3..1c41d46 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ async with session_factory() as session, session.begin(): ) ``` -The same one-decorator pattern works for RabbitMQ, NATS, Redis, and Confluent. See the [relay tutorial](https://faststream-outbox.readthedocs.io/en/latest/usage/relay/) for the FastAPI lifecycle, header propagation, router shapes, and the at-least-once contract. +The same one-decorator pattern works for RabbitMQ, NATS, Redis, and Confluent. See the [relay tutorial](https://faststream-outbox.modern-python.org/usage/relay/) for the FastAPI lifecycle, header propagation, router shapes, and the at-least-once contract. ## Quickstart — standalone outbox queue @@ -81,13 +81,13 @@ async with session_factory() as session, session.begin(): ## How it works -A subscriber owns two async loops: a **fetch** loop claims available rows via a single CTE (`SELECT … FOR UPDATE SKIP LOCKED → UPDATE acquired_token=:uuid, acquired_at=now() RETURNING *`), and `max_workers` **worker** loops dispatch to the handler. On success, `DELETE WHERE id=:id AND acquired_token=:token`; on failure, the retry strategy schedules another attempt or terminally drops the row. Terminal failures `DELETE` by default; pass `dlq_table=make_dlq_table(metadata)` to atomically archive them into a sibling audit table instead — see [Dead-letter queue](https://faststream-outbox.readthedocs.io/en/latest/usage/dlq/). +A subscriber owns two async loops: a **fetch** loop claims available rows via a single CTE (`SELECT … FOR UPDATE SKIP LOCKED → UPDATE acquired_token=:uuid, acquired_at=now() RETURNING *`), and `max_workers` **worker** loops dispatch to the handler. On success, `DELETE WHERE id=:id AND acquired_token=:token`; on failure, the retry strategy schedules another attempt or terminally drops the row. Terminal failures `DELETE` by default; pass `dlq_table=make_dlq_table(metadata)` to atomically archive them into a sibling audit table instead — see [Dead-letter queue](https://faststream-outbox.modern-python.org/usage/dlq/). The `acquired_token` is the load-bearing invariant: a slow handler whose lease expired and was re-claimed by another worker finds its terminal `DELETE` to be a no-op (the token no longer matches), preventing it from clobbering the new lease holder. With the `asyncpg` driver, the fetch loop also `LISTEN`s on `outbox_` and `publish` emits `pg_notify(...)`, so idle dispatch latency is sub-100ms instead of up to `max_fetch_interval`. -See [How it works](https://faststream-outbox.readthedocs.io/en/latest/introduction/how-it-works/) for the full architecture. +See [How it works](https://faststream-outbox.modern-python.org/introduction/how-it-works/) for the full architecture. ## Optional extras @@ -107,7 +107,7 @@ Browse the full list of templates and libraries in [`modern-python`](https://github.com/modern-python) — see the org profile for the categorized index. -## 📚 [Documentation](https://faststream-outbox.readthedocs.io) +## 📚 [Documentation](https://faststream-outbox.modern-python.org) ## 📦 [PyPi](https://pypi.org/project/faststream-outbox) diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..2f00d83 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +faststream-outbox.modern-python.org diff --git a/mkdocs.yml b/mkdocs.yml index 6fbafa5..e8523ef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,5 @@ site_name: faststream-outbox +site_url: https://faststream-outbox.modern-python.org/ repo_url: https://github.com/modern-python/faststream-outbox docs_dir: docs edit_uri: edit/main/docs/ diff --git a/planning/plans/2026-06-09-mkdocs-github-pages-plan.md b/planning/plans/2026-06-09-mkdocs-github-pages-plan.md new file mode 100644 index 0000000..7ebd128 --- /dev/null +++ b/planning/plans/2026-06-09-mkdocs-github-pages-plan.md @@ -0,0 +1,604 @@ +# Migrate Docs Hosting from Read the Docs to GitHub Pages — 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:** Replace the Read the Docs deploy pipeline with a GitHub Actions + GitHub Pages deploy mirroring `modern-di`, served at `faststream-outbox.modern-python.org`. Bump CI action pins to the same baseline along the way. + +**Architecture:** Eight in-repo file changes (one new workflow, one new docs/CNAME, one Justfile target, one mkdocs.yml field, two file deletions/edits for RTD removal, two action-pin bumps). Out-of-repo DNS + GH Pages settings happen after merge (documented in the spec, not in this plan). All commits land on a single feature branch. + +**Tech Stack:** mkdocs + mkdocs-material (existing), `mkdocs gh-deploy` (force-push to `gh-pages`), GitHub Actions, `just`, `uvx`. + +**Spec:** `planning/specs/2026-06-09-mkdocs-github-pages-design.md` + +**Branch:** Work on a new branch `feat/mkdocs-github-pages` off `main`. Do not commit to `main` directly. + +--- + +## Pre-flight + +- [ ] **Step 1: Create the feature branch** + +```bash +git checkout -b feat/mkdocs-github-pages +git status +``` + +Expected: `On branch feat/mkdocs-github-pages` with clean working tree. + +- [ ] **Step 2: Confirm assumptions about current state** + +```bash +ls .readthedocs.yaml docs/requirements.txt docs/CNAME 2>&1 +grep -c "site_url" mkdocs.yml +grep "docs-deploy" Justfile +``` + +Expected: +- `.readthedocs.yaml` and `docs/requirements.txt` exist. +- `docs/CNAME` does NOT exist (`ls` reports no such file). +- `site_url` grep returns `0` (currently absent). +- `docs-deploy` grep returns no match (Justfile has no docs target yet). + +If any expectation fails, STOP and re-read the spec — assumptions changed. + +--- + +## Task 1: Add the `just docs-deploy` target + +**Files:** +- Modify: `Justfile` (append at end) + +- [ ] **Step 1: Read the current Justfile end** + +```bash +tail -5 Justfile +``` + +Expected: last target is `publish:` with `uv publish --token $PYPI_TOKEN`. Confirms there's no trailing blank-line surprise. + +- [ ] **Step 2: Append the `docs-deploy` target** + +Append these lines to the end of `Justfile` (preserve a single blank line before the new target, single trailing newline at EOF): + +```just + +# 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 +``` + +- [ ] **Step 3: Verify the target is discoverable** + +Run: `just --list | grep docs-deploy` + +Expected: one line — ` docs-deploy # Force-pushes built site to gh-pages; CI runs this on push to main.` + +- [ ] **Step 4: Smoke-test mkdocs without deploying** + +Run: `uvx --with-requirements docs/requirements.txt mkdocs build --strict --site-dir /tmp/mkdocs-smoke` + +Expected: build completes with no warnings (`--strict` turns warnings into errors). Output ends with `INFO - Documentation built in N.NNs`. The `/tmp/mkdocs-smoke` directory now contains `index.html`. Clean up with `rm -rf /tmp/mkdocs-smoke`. + +If the build fails, the current `mkdocs.yml` has a latent issue unrelated to this plan — STOP and report. + +- [ ] **Step 5: Commit** + +```bash +git add Justfile +git commit -m "chore: add just docs-deploy target for mkdocs gh-deploy + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 2: Add `docs/CNAME` + +**Files:** +- Create: `docs/CNAME` + +- [ ] **Step 1: Create the file with the custom domain** + +Create `docs/CNAME` containing exactly one line with a trailing newline: + +``` +faststream-outbox.modern-python.org +``` + +- [ ] **Step 2: Verify contents** + +Run: `cat docs/CNAME && wc -l docs/CNAME` + +Expected: +``` +faststream-outbox.modern-python.org + 1 docs/CNAME +``` + +(One line, one newline at EOF.) + +- [ ] **Step 3: Confirm mkdocs picks it up** + +Run: `uvx --with-requirements docs/requirements.txt mkdocs build --strict --site-dir /tmp/mkdocs-smoke && cat /tmp/mkdocs-smoke/CNAME && rm -rf /tmp/mkdocs-smoke` + +Expected: build succeeds, `CNAME` is copied into the built site with the same single-line contents. + +- [ ] **Step 4: Commit** + +```bash +git add docs/CNAME +git commit -m "docs: add CNAME for faststream-outbox.modern-python.org + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 3: Add `site_url` to `mkdocs.yml` + +**Files:** +- Modify: `mkdocs.yml` (insert one line after `site_name:`) + +- [ ] **Step 1: Read the first 6 lines of mkdocs.yml** + +```bash +head -6 mkdocs.yml +``` + +Expected current top: +```yaml +site_name: faststream-outbox +repo_url: https://github.com/modern-python/faststream-outbox +docs_dir: docs +edit_uri: edit/main/docs/ +nav: + - Introduction: +``` + +- [ ] **Step 2: Insert `site_url` after `site_name`** + +After line 1 (`site_name: faststream-outbox`), insert: + +```yaml +site_url: https://faststream-outbox.modern-python.org/ +``` + +Result: lines 1–2 are `site_name:` followed by `site_url:`; the rest of the file is unchanged. + +- [ ] **Step 3: Verify the edit** + +```bash +head -3 mkdocs.yml +``` + +Expected: +```yaml +site_name: faststream-outbox +site_url: https://faststream-outbox.modern-python.org/ +repo_url: https://github.com/modern-python/faststream-outbox +``` + +- [ ] **Step 4: Verify mkdocs still builds** + +Run: `uvx --with-requirements docs/requirements.txt mkdocs build --strict --site-dir /tmp/mkdocs-smoke && grep -c 'canonical' /tmp/mkdocs-smoke/index.html && rm -rf /tmp/mkdocs-smoke` + +Expected: build succeeds, grep returns `1` (the canonical link tag is now emitted in `index.html`). + +- [ ] **Step 5: Commit** + +```bash +git add mkdocs.yml +git commit -m "docs: set mkdocs site_url for canonical links and sitemap + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 4: Add `.github/workflows/docs.yml` + +**Files:** +- Create: `.github/workflows/docs.yml` + +- [ ] **Step 1: Create the workflow** + +Create `.github/workflows/docs.yml` with exactly this content (single trailing newline at EOF): + +```yaml +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@v6 + with: + fetch-depth: 0 + - uses: extractions/setup-just@v4 + - uses: astral-sh/setup-uv@v8.2.0 + - run: just docs-deploy +``` + +- [ ] **Step 2: Verify YAML parses** + +Run: `python3 -c "import yaml, sys; yaml.safe_load(open('.github/workflows/docs.yml')); print('OK')"` + +Expected: prints `OK`. (Any YAML error throws and exits non-zero.) + +- [ ] **Step 3: Verify load-bearing fields are present** + +Run: +```bash +python3 -c " +import yaml +d = yaml.safe_load(open('.github/workflows/docs.yml')) +assert d['name'] == 'Deploy Docs' +assert d['concurrency']['group'] == 'docs-deploy' +assert d['concurrency']['cancel-in-progress'] is True +assert d['permissions']['contents'] == 'write' +assert d['jobs']['deploy']['steps'][0]['with']['fetch-depth'] == 0 +assert d['jobs']['deploy']['steps'][-1]['run'] == 'just docs-deploy' +print('OK') +" +``` + +Expected: prints `OK`. If any assert fails, the YAML was mistyped — fix and re-run. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/docs.yml +git commit -m "ci: add mkdocs gh-deploy workflow + +Triggers on push to main when docs/, mkdocs.yml, or this workflow +change. Concurrency group serializes deploys; contents:write needed +for gh-pages branch push; fetch-depth: 0 needed for gh-deploy history. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 5: Delete `.readthedocs.yaml` + +**Files:** +- Delete: `.readthedocs.yaml` + +- [ ] **Step 1: Confirm the file exists and matches expectations** + +```bash +cat .readthedocs.yaml +``` + +Expected: the 14-line file (version 2, ubuntu-22.04, python 3.10, mkdocs config). If contents have drifted from the spec snapshot, pause and re-read the spec. + +- [ ] **Step 2: Delete the file** + +```bash +git rm .readthedocs.yaml +``` + +Expected: `rm '.readthedocs.yaml'`, working tree shows the deletion staged. + +- [ ] **Step 3: Verify** + +```bash +ls .readthedocs.yaml 2>&1; git status --short +``` + +Expected: `ls` reports the file does not exist; `git status --short` shows `D .readthedocs.yaml`. + +- [ ] **Step 4: Commit** + +```bash +git commit -m "ci: remove Read the Docs config + +Docs now deploy from .github/workflows/docs.yml. RTD project will be +archived in the RTD UI after the new URL is live (out-of-repo step). + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 6: Update `README.md` URLs + +**Files:** +- Modify: `README.md` (4 line edits — lines 52, 84, 90, 110) + +- [ ] **Step 1: Replace the three `/en/latest/` deep links** + +Three substitutions in `README.md`. Each replaces a substring; do them in order. Use Edit (string replacement), not regex. + +Edit 1 — line 52, relay tutorial: +- Old: `https://faststream-outbox.readthedocs.io/en/latest/usage/relay/` +- New: `https://faststream-outbox.modern-python.org/usage/relay/` + +Edit 2 — line 84, dead-letter queue: +- Old: `https://faststream-outbox.readthedocs.io/en/latest/usage/dlq/` +- New: `https://faststream-outbox.modern-python.org/usage/dlq/` + +Edit 3 — line 90, "How it works": +- Old: `https://faststream-outbox.readthedocs.io/en/latest/introduction/how-it-works/` +- New: `https://faststream-outbox.modern-python.org/introduction/how-it-works/` + +- [ ] **Step 2: Replace the bare-domain link** + +Edit 4 — line 110, top-level Documentation header: +- Old: `https://faststream-outbox.readthedocs.io` +- New: `https://faststream-outbox.modern-python.org` + +(Order matters: do this AFTER the three `/en/latest/` edits. Otherwise the bare-domain string also matches inside the deeper URLs and you'd over-replace.) + +- [ ] **Step 3: Verify no `readthedocs` references remain** + +Run: `grep -n readthedocs README.md` + +Expected: no output (exit code 1 from grep, which is fine). + +- [ ] **Step 4: Verify the four new URLs are in place** + +Run: `grep -nc "modern-python.org" README.md` + +Expected: `4`. + +- [ ] **Step 5: Commit** + +```bash +git add README.md +git commit -m "docs: point README links at modern-python.org + +Replaces readthedocs.io/en/latest// with +modern-python.org// for the three deep links (relay, dlq, +how-it-works) and the bare-domain Documentation header link. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 7: Bump action pins in `.github/workflows/ci.yml` + +**Files:** +- Modify: `.github/workflows/ci.yml` (lines 17, 18, 19, 49, 50) + +- [ ] **Step 1: Confirm current pin lines** + +Run: `grep -n "@v[0-9]" .github/workflows/ci.yml` + +Expected: +``` +17: - uses: actions/checkout@v4 +18: - uses: extractions/setup-just@v2 +19: - uses: astral-sh/setup-uv@v3 +49: - uses: actions/checkout@v4 +50: - uses: astral-sh/setup-uv@v3 +``` + +- [ ] **Step 2: Bump `actions/checkout@v4` → `@v6`** + +Two occurrences (lines 17 and 49). Use a `replace_all` for the exact string `actions/checkout@v4` → `actions/checkout@v6`. + +- [ ] **Step 3: Bump `extractions/setup-just@v2` → `@v4`** + +One occurrence (line 18 — pytest job doesn't use just). Replace exact string `extractions/setup-just@v2` → `extractions/setup-just@v4`. + +- [ ] **Step 4: Bump `astral-sh/setup-uv@v3` → `@v8.2.0`** + +Two occurrences (lines 19 and 50). Use `replace_all` for `astral-sh/setup-uv@v3` → `astral-sh/setup-uv@v8.2.0`. + +- [ ] **Step 5: Verify all five pins updated** + +Run: `grep -n "@v[0-9]" .github/workflows/ci.yml` + +Expected: +``` +17: - uses: actions/checkout@v6 +18: - uses: extractions/setup-just@v4 +19: - uses: astral-sh/setup-uv@v8.2.0 +49: - uses: actions/checkout@v6 +50: - uses: astral-sh/setup-uv@v8.2.0 +``` + +Also verify no stale pins linger: + +```bash +grep -E "@v[234]\b" .github/workflows/ci.yml +``` + +Expected: no output (exit 1). + +- [ ] **Step 6: Verify YAML still parses** + +Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml')); print('OK')"` + +Expected: `OK`. + +- [ ] **Step 7: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: bump action pins in ci.yml to drop Node.js 20 + +actions/checkout v4 -> v6 +extractions/setup-just v2 -> v4 +astral-sh/setup-uv v3 -> v8.2.0 + +Matches the baseline modern-di already runs. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 8: Bump action pins in `.github/workflows/publish.yml` + +**Files:** +- Modify: `.github/workflows/publish.yml` (lines 12, 13, 14) + +- [ ] **Step 1: Confirm current pin lines** + +Run: `grep -n "@v[0-9]" .github/workflows/publish.yml` + +Expected: +``` +12: - uses: actions/checkout@v4 +13: - uses: extractions/setup-just@v2 +14: - uses: astral-sh/setup-uv@v3 +``` + +- [ ] **Step 2: Apply the three bumps** + +Three single-occurrence string replacements: + +- `actions/checkout@v4` → `actions/checkout@v6` +- `extractions/setup-just@v2` → `extractions/setup-just@v4` +- `astral-sh/setup-uv@v3` → `astral-sh/setup-uv@v8.2.0` + +- [ ] **Step 3: Verify** + +Run: `grep -n "@v[0-9]" .github/workflows/publish.yml` + +Expected: +``` +12: - uses: actions/checkout@v6 +13: - uses: extractions/setup-just@v4 +14: - uses: astral-sh/setup-uv@v8.2.0 +``` + +And: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/publish.yml')); print('OK')"` → `OK`. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/publish.yml +git commit -m "ci: bump action pins in publish.yml to drop Node.js 20 + +Matches the pins applied to ci.yml and docs.yml in this branch. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Final verification + +- [ ] **Step 1: Run `just lint-ci` to catch formatting drift** + +Run: `just lint-ci` + +Expected: all four sub-steps pass (`eof-fixer`, `ruff format --check`, `ruff check --no-fix`, `ty check`). If `eof-fixer` flags any of the new files, run `just lint` once, then `git add` the fix into the appropriate prior commit via `git commit --amend` — or, if multiple files are touched, fold them into a single `chore: eof-fixer cleanup` commit at the end. + +- [ ] **Step 2: Verify the full diff against `main`** + +Run: `git log --oneline main..HEAD && git diff --stat main..HEAD` + +Expected: 8 commits (Tasks 1–8). The `--stat` summary should touch exactly these paths: + +``` +.github/workflows/ci.yml +.github/workflows/docs.yml +.github/workflows/publish.yml +.readthedocs.yaml +Justfile +README.md +docs/CNAME +mkdocs.yml +``` + +(8 entries; `.readthedocs.yaml` shows as a deletion.) + +- [ ] **Step 3: One final mkdocs strict build** + +Run: `uvx --with-requirements docs/requirements.txt mkdocs build --strict --site-dir /tmp/mkdocs-final && ls /tmp/mkdocs-final/CNAME && grep -q 'modern-python.org' /tmp/mkdocs-final/index.html && echo OK && rm -rf /tmp/mkdocs-final` + +Expected: prints `OK`. (Verifies: build is strict-clean, `CNAME` lands in the output, and the canonical URL points at the new domain.) + +- [ ] **Step 4: Push the branch** + +```bash +git push -u origin feat/mkdocs-github-pages +``` + +Expected: branch tracks `origin/feat/mkdocs-github-pages`. + +- [ ] **Step 5: Open the PR** + +```bash +gh pr create --title "Migrate docs hosting from Read the Docs to GitHub Pages" --body "$(cat <<'EOF' +## Summary + +- Adds `.github/workflows/docs.yml` mirroring `modern-di`'s pattern: `mkdocs gh-deploy --force` on push to `main` (paths-filtered), concurrency-serialized, `contents: write`. +- Adds `just docs-deploy` target driving the deploy via `uvx --with-requirements docs/requirements.txt mkdocs gh-deploy --force`. +- Adds `docs/CNAME` and `mkdocs.yml` `site_url` for the custom domain `faststream-outbox.modern-python.org`. +- Deletes `.readthedocs.yaml`; updates the four `readthedocs.io` links in `README.md` to `modern-python.org` equivalents. +- Bumps action pins in `ci.yml` and `publish.yml` to the same baseline the new `docs.yml` uses (`checkout@v6`, `setup-just@v4`, `setup-uv@v8.2.0`). + +Spec: `planning/specs/2026-06-09-mkdocs-github-pages-design.md` + +## Out-of-repo steps after merge + +1. DNS: CNAME `faststream-outbox.modern-python.org` → `modern-python.github.io`. +2. After the first workflow run creates `gh-pages`, set Settings → Pages → Source = `gh-pages` branch, `/` root. +3. Tick "Enforce HTTPS" once the cert provisions (~5 min). +4. Archive the `faststream-outbox` project on readthedocs.io. + +## Test plan + +- [ ] PR CI is green (`ci.yml` lint + pytest jobs run with bumped pins). +- [ ] After merge, the `Deploy Docs` workflow runs and creates the `gh-pages` branch. +- [ ] `https://faststream-outbox.modern-python.org/` returns the docs site once DNS + Pages settings are in place. +- [ ] All four `README.md` links resolve to working pages on the new domain. +- [ ] A subsequent push to `main` touching `docs/` re-deploys. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +Expected: prints the PR URL. + +--- + +## Self-review + +**Spec coverage:** + +| Spec section | Plan task | +| --- | --- | +| 1. `docs.yml` workflow | Task 4 | +| 2. `just docs-deploy` | Task 1 | +| 3. `docs/CNAME` | Task 2 | +| 4. `mkdocs.yml` `site_url` | Task 3 | +| 5. Delete `.readthedocs.yaml` | Task 5 | +| 6. README URL updates | Task 6 | +| 7. `ci.yml` pin bumps | Task 7 | +| 8. `publish.yml` pin bumps | Task 8 | +| Operations (out of repo) | PR description (Step 5 of Final verification) | +| Sequencing / rollback | Implicit — all 8 land as one PR; Tasks 1–4 add the new path, Task 5 removes RTD config, Task 6 swaps README links, Tasks 7–8 update pins. Reverting the PR restores RTD + old pins. | + +All eight spec changes have a dedicated task; out-of-repo ops are surfaced in the PR body so the merging maintainer sees them. + +**Placeholder check:** No TBDs, no "implement later", every code/YAML block is complete, every command has expected output. + +**Consistency check:** +- `docs-deploy` Justfile target name matches across Task 1, Task 4 (the workflow's `run: just docs-deploy` line), and the spec. +- Action pin versions match across Tasks 4, 7, 8 (`checkout@v6`, `setup-just@v4`, `setup-uv@v8.2.0`). +- The domain string `faststream-outbox.modern-python.org` matches across Tasks 2, 3, 6, and the PR body. diff --git a/planning/specs/2026-06-09-mkdocs-github-pages-design.md b/planning/specs/2026-06-09-mkdocs-github-pages-design.md new file mode 100644 index 0000000..d32652b --- /dev/null +++ b/planning/specs/2026-06-09-mkdocs-github-pages-design.md @@ -0,0 +1,269 @@ +# Design: Migrate docs hosting from Read the Docs to GitHub Pages + +**Date:** 2026-06-09 +**Status:** Approved +**Slug:** `mkdocs-github-pages` + +## Summary + +Move `faststream-outbox` docs hosting off Read the Docs and onto GitHub +Pages, mirroring the migration the sister `modern-di` project completed in +commit `61b9377`. Docs build via a new dedicated GH Actions workflow that +runs `mkdocs gh-deploy` on push to `main`, served at the custom domain +`faststream-outbox.modern-python.org`. Pin bumps for the two existing +workflows (`ci.yml`, `publish.yml`) come along for the ride so all three +workflows share the same action versions on landing. + +No runtime code, test code, or public API touched. + +## Motivation + +- **Operational simplicity.** Read the Docs requires a separate account + with its own settings, webhooks, and quirks (Python 3.10 pin in + `.readthedocs.yaml`, separate build infrastructure). GH Pages keeps the + docs pipeline in the same repo as code review. +- **Symmetry with `modern-di`.** The two repos already share Justfile + layout, lint pipeline, mkdocs theme, and PyPI publish flow. Sharing + docs hosting closes the last operational gap. +- **Action-version drift.** `ci.yml` and `publish.yml` pin `checkout@v4`, + `setup-just@v2`, `setup-uv@v3`. `modern-di` already bumped to `@v6`, + `@v4`, `@v8.2.0` (per its commit `4f1945c` — "drop Node.js 20"). Doing + the bump alongside the docs workflow keeps all three workflows on the + same baseline; landing it later would require a one-line follow-up PR. + +## Design + +### 1. New workflow `.github/workflows/docs.yml` + +Near-verbatim copy of `modern-di`'s `docs.yml`: + +```yaml +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@v6 + with: + fetch-depth: 0 + - uses: extractions/setup-just@v4 + - uses: astral-sh/setup-uv@v8.2.0 + - run: just docs-deploy +``` + +Load-bearing pieces: + +- **Paths filter.** Workflow fires only when docs sources, mkdocs config, + or the workflow itself change. CI runs that only touch code don't + trigger an unnecessary deploy. +- **`workflow_dispatch`.** Lets a maintainer force a redeploy from the + Actions UI if the live site falls out of sync (e.g. after a manual + `gh-pages` branch touch). +- **`concurrency: docs-deploy`, `cancel-in-progress: true`.** Serializes + deploys. A rapid sequence of merges to `main` doesn't race + force-pushes against the `gh-pages` branch. +- **`permissions: contents: write`.** Default `GITHUB_TOKEN` scope is + read-only; `mkdocs gh-deploy` needs write to push the `gh-pages` + branch. +- **`fetch-depth: 0`.** `mkdocs gh-deploy` reads git history to construct + the deploy commit; a shallow checkout breaks the push. + +### 2. Add `just docs-deploy` target + +Append to `Justfile`: + +```just +# 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 +``` + +Notes: + +- `uvx --with-requirements docs/requirements.txt` runs mkdocs in an + ephemeral environment derived from `docs/requirements.txt` + (`mkdocs`, `mkdocs-material`). The project's `.venv` (Docker-managed) + is not involved. +- `--force` matches `modern-di`. `gh-deploy` builds the site, commits it + to a synthetic `gh-pages` branch, and force-pushes. The comment in + the target body documents the "stale checkout rolls live back" gotcha + so a contributor reading the file sees it before invoking locally. + +### 3. New file `docs/CNAME` + +One line: + +``` +faststream-outbox.modern-python.org +``` + +mkdocs material copies any file under `docs/` into the build output, so +the `CNAME` survives every `gh-deploy --force` push. Without this file, +the force-push would wipe the custom domain on every deploy and GH Pages +would revert to the project-page URL. + +### 4. Update `mkdocs.yml` + +Add a `site_url:` line near the top, alongside `site_name:` / `repo_url:`: + +```yaml +site_url: https://faststream-outbox.modern-python.org/ +``` + +Used by mkdocs for: + +- Canonical link tags in every HTML page (SEO). +- The generated `sitemap.xml`. +- Material theme social cards (if enabled later). + +Currently absent from `mkdocs.yml` — generated pages have no canonical +URL. This is independently worth doing; bundling it with the migration +saves a follow-up. + +### 5. Delete `.readthedocs.yaml` + +Removes the RTD build configuration. After this lands and the new URL is +live, the RTD project at `faststream-outbox.readthedocs.io` should be +archived or deleted in the RTD UI (out-of-repo step; see +"Operations" below). + +### 6. Update `README.md` URLs + +Four RTD links replaced with their `modern-python.org` equivalents: + +- Three deep links of the form + `https://faststream-outbox.readthedocs.io/en/latest//` → + `https://faststream-outbox.modern-python.org//`. The `/en/latest/` + segment is RTD's version/locale routing; GH Pages doesn't use it. + Targets: relay tutorial, dead-letter queue, "How it works". +- One bare-domain link + `https://faststream-outbox.readthedocs.io` → + `https://faststream-outbox.modern-python.org` (the top-level + `## 📚 Documentation` header link). + +### 7. Bump action pins in `.github/workflows/ci.yml` + +Three pin upgrades, no structural changes: + +| Old | New | Occurrences | +| --- | --- | --- | +| `actions/checkout@v4` | `actions/checkout@v6` | 2 (one per job) | +| `extractions/setup-just@v2` | `extractions/setup-just@v4` | 1 (lint job only) | +| `astral-sh/setup-uv@v3` | `astral-sh/setup-uv@v8.2.0` | 2 (one per job) | + +`enable-cache: true` and `cache-dependency-glob: "**/pyproject.toml"` +both remain valid in `setup-uv@v8` — no `with:` block changes. + +Explicitly **not** in scope: restructuring into a reusable +`_checks.yml` workflow (modern-di's pattern). That's a separate refactor. + +### 8. Bump action pins in `.github/workflows/publish.yml` + +Same three upgrades, one occurrence each (single job). + +## Operations (out of repo) + +These steps cannot live in the spec/commit; they need a maintainer with +admin access: + +1. **DNS.** Add a CNAME record: + - Host: `faststream-outbox` + - Target: `modern-python.github.io` + - TTL: 1 hour (GitHub validates within minutes; short TTL is harmless). +2. **Trigger first build.** Either merge the PR (push to `main` runs the + workflow) or run `workflow_dispatch` from the Actions UI. The first + run creates the `gh-pages` branch. +3. **GH Pages source.** After the `gh-pages` branch exists: + Settings → Pages → Source = "Deploy from a branch", branch + `gh-pages`, folder `/` (root). The custom-domain field + auto-populates from `docs/CNAME`. +4. **HTTPS.** Wait ~5 minutes for GitHub to provision the Let's Encrypt + cert, then tick "Enforce HTTPS". +5. **Verify.** Hit + `https://faststream-outbox.modern-python.org/` and confirm the + relay/DLQ/how-it-works pages load. +6. **RTD teardown.** In the Read the Docs UI, archive or delete the + `faststream-outbox` project. The old `readthedocs.io` URL will + start returning 404; README links no longer point there, so user + breakage is bounded to external links / search-engine results + (which migrate over time). + +## Sequencing and rollback + +The workflow can land and run before DNS resolves. GH Pages serves at +`modern-python.github.io/faststream-outbox/` in the interim (with a +"custom domain not configured" notice in the Pages settings, harmless). +RTD continues to serve the old URL until step 6 above, so the docs are +never simultaneously down. + +Rollback path if something breaks mid-migration: + +- Revert the PR. RTD is still building (`.readthedocs.yaml` removal is + part of the same PR), so reverting re-enables the RTD pipeline. The + README points back at `readthedocs.io` after revert. +- The `gh-pages` branch can be left in place; deleting it requires no + follow-up. GH Pages settings can be reset to "Source = None". + +## Out of scope (deliberate) + +- **PR-time docs build check.** `modern-di` doesn't have one and the + symmetry is the whole point. Broken mkdocs config still fails the + deploy workflow on `main` — visible quickly. +- **Reusable `_checks.yml` workflow split.** Modern-di's structural + pattern; worth doing separately, not load-bearing for the docs move. +- **Theme / plugin additions.** No mkdocs plugins added (no + `mike` for versioning, no social cards, no privacy plugin). The + current setup is intentionally minimal; expansion is a separate + conversation. +- **Old-URL redirects.** GH Pages can't gracefully redirect from + `readthedocs.io`; RTD's URL behavior after archival is on RTD's side. + Accepted as a one-time bookmark/search-engine adjustment cost. + +## Testing + +The spec changes are configuration; correctness is observable on the +live site: + +- The first deploy workflow run completes green and creates the + `gh-pages` branch (visible in Actions UI + branch list). +- After DNS resolves and Pages config flips, the live URL returns the + expected docs (nav identical to current RTD site, theme intact, search + works). +- A second push to `main` that touches a doc page (or + `workflow_dispatch`) re-runs the workflow and updates the live site. + +No new pytest / lint hooks are added. The existing `just lint-ci` run on +PRs already validates YAML formatting via ruff's `EOF` fixer and +formatters. + +## Risk + +- **DNS misconfiguration** → custom domain doesn't resolve. Mitigated + by sequencing: workflow lands first and serves at the GH project URL; + DNS is fixed before README links are pointed at the new domain (or + the PR is split if needed). +- **Action pin upgrade regressions** → CI breaks for unrelated reasons. + Low risk: modern-di runs the same pins green; if `setup-uv@v8` does + drift, the fix is a one-line pin and rollback is trivial. +- **`docs-deploy` force-push from a stale clone** → site rolls back. + Mitigated by the comment on the `just docs-deploy` target plus the + fact that the canonical deploy is from CI; manual invocation is + documented as a debugging escape hatch, not a workflow.