Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ jobs:
- run: uv python pin 3.13
- run: just install lint-ci

docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: extractions/setup-just@v4
- uses: astral-sh/setup-uv@v8.2.0
- run: just docs-build

pytest:
runs-on: ubuntu-latest
strategy:
Expand Down
4 changes: 2 additions & 2 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
mkdocs
mkdocs-material
mkdocs>=1.6,<2
mkdocs-material>=9,<10
8 changes: 6 additions & 2 deletions faststream_outbox/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,12 @@ def make_outbox_table(metadata: "MetaData", table_name: str = "outbox") -> Table
The user wires the returned table into their own SQLAlchemy ``MetaData`` so it is
discovered by Alembic's autogenerate. They are responsible for the actual migration.

Raises ``ValueError`` if *table_name* would push the NOTIFY channel
``outbox_<table_name>`` past Postgres' 63-byte identifier limit (NAMEDATALEN-1).
Raises ``ValueError`` if *table_name* would push any derived identifier past
Postgres' 63-byte limit (NAMEDATALEN-1). The binding identifier is the longest
one derived — usually an index/constraint name (``<table_name>_pending_idx``,
``<table_name>_timer_id_uq``), which is longer than the NOTIFY channel
``outbox_<table_name>``, so a name that fits the channel can still overflow an
index name. See ``validate_table_identifiers``.
"""
validate_table_identifiers(table_name)
table = Table(
Expand Down
6 changes: 6 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ theme:
icon: material/brightness-4
name: Switch to system preference

validation:
omitted_files: warn
absolute_links: warn
unrecognized_links: warn
anchors: warn

markdown_extensions:
- admonition
- toc:
Expand Down
151 changes: 151 additions & 0 deletions planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
summary: Made mkdocs --strict a PR gate (parallel docs job) with native link/anchor validation, pinned docs deps (<2 / <10), and fixed the make_outbox_table 63-byte docstring; README audit found no drift (clean).
---

# Design: Docs CI drift guard

## Summary

Add a PR-time guard against the class of documentation drift that a 2026-07-05
docs audit (`docs/audit-drift-fixes`, PR #125) surfaced. Today `mkdocs build
--strict` runs **only** in `docs.yml` on push to `main` (for deploy), so a PR
that breaks an internal link or orphans a page is not caught until after merge;
broken `#anchor` fragments are not caught at all. This change makes the strict
build a parallel PR check, turns on mkdocs' native link/anchor validation, pins
the (currently unpinned) docs toolchain, fixes one source docstring that
carries the same wording bug the audit fixed in the docs, and does a one-time
correctness pass over `README.md` (which lives outside `docs_dir` and is
therefore invisible to mkdocs).

Deliberately **excluded**: any validation of the Python inside `docs/` code
fences (syntax-parse, type-check, or execute). That was considered and rejected
for cost / false-positive reasons — recorded in
[`../../decisions/2026-07-05-no-doc-code-fence-validation.md`](../../decisions/2026-07-05-no-doc-code-fence-validation.md).

## Motivation

The docs audit (PR #125) found, among ~20 fixes: three broken code samples,
a migration snippet missing a load-bearing CHECK, several stale numbers, and a
set of `#anchor` links that happened to resolve. CI caught **none** of it,
because:

- `mkdocs build --strict` — which fails on broken internal links and orphaned
pages — is not part of the PR `checks` workflow (`_checks.yml`). It runs only
in `docs.yml`, which triggers on push to `main` with `paths: docs/**`. So
link/orphan breakage fails the **deploy** after merge, not the PR.
- mkdocs does not validate `#anchor` fragments by default; the audit relied on a
throwaway script to check them.
- `docs/requirements.txt` pins nothing (`mkdocs`, `mkdocs-material`). The
audit's strict build already emitted the upstream "MkDocs 2.0 will break all
plugins/themes" warning — an unpinned major bump would break the build with no
notice.

The cheapest durable guards are all in-tool: promote the existing strict build
to a PR gate and switch on mkdocs' own link/anchor validation. No new script to
own.

## Non-goals

- No validation of Python code fences in `docs/` (syntax/type/execute) — see the
decision file. This is the single biggest scope cut and the reason the change
stays small.
- No ongoing link-checking of `README.md`. Its links are predominantly stable
external badges/URLs; an external link-checker is flaky and low-value. README
is fixed once here (§4) and not added to a recurring guard.
- No change to what `docs.yml` does at deploy time; the PR job simply runs the
same command earlier.

## Design

### 1. Strict docs build as a parallel PR job

Add a `docs` job to `.github/workflows/_checks.yml` (the reusable workflow that
`ci.yml` calls on every PR and push to `main`). It runs the **existing**
recipe, identical to the deploy build:

```yaml
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: extractions/setup-just@v4
- uses: astral-sh/setup-uv@v8.2.0
- run: just docs-build
```

Parallel with `lint` and `pytest`; isolated from the lint runner; the same
`just docs-build` command PR-time and deploy-time, so they cannot diverge.

### 2. Native link + anchor validation in mkdocs

Add a `validation:` block to `mkdocs.yml` (supported since mkdocs 1.6; the repo
resolves 1.6.1):

```yaml
validation:
omitted_files: warn
absolute_links: warn
unrecognized_links: warn
anchors: warn
```

Under `--strict`, every `warn` is promoted to an error, so a broken `#anchor`
fragment or an unrecognized internal link now fails the build. This subsumes the
audit's throwaway anchor script with zero maintained code.

### 3. `make_outbox_table` docstring fix (folds in #2)

`faststream_outbox/schema.py`'s `make_outbox_table` docstring says it raises
`ValueError` when `table_name` pushes the NOTIFY channel `outbox_<table_name>`
past Postgres' 63-byte limit. The guard (`validate_table_identifiers`) actually
binds on the **longest derived identifier** — an index/constraint name such as
`<table_name>_pending_idx` / `<table_name>_timer_id_uq`, which is longer than
the channel prefix. This is the exact wording the audit corrected in
`docs/operations/checklist.md`; fixing the docstring closes it at the source so
docs and code agree. One-line prose change; no behavior change.

### 4. One-time README audit (folds in #3)

`README.md` is outside `docs_dir`, so neither the strict build nor mkdocs
validation ever touches it. Do a one-time correctness/consistency pass with the
same rigor as the docs audit: verify every code sample's imports/symbols/kwargs
against source, check stated numbers/claims, and check links. Fix confident
drift in place; surface any judgment calls. Findings are recorded inline in the
implementing PR (a full `planning/audits/` report is overkill for a single
file); if the pass turns up something structural, it spawns its own change.

### 5. Pin the docs toolchain

`docs/requirements.txt` gets upper-bounded pins so the strict build (now a PR
gate) is reproducible and immune to a breaking major:

```
mkdocs>=1.6,<2
mkdocs-material>=9,<10
```

(Exact lower bound for `mkdocs-material` set to the resolved version's series at
implementation time.)

## Testing

- **Negative-to-positive on the guard:** on the branch, temporarily introduce a
broken `#anchor` link and a broken relative `.md` link in a docs page and
confirm `just docs-build` now **fails**; revert. Confirm a clean tree
**passes**. This proves §1+§2 actually gate.
- **README:** re-run the audit checks (imports/symbols resolve, links resolve)
after fixes; `git diff` review.
- **Docstring:** `just lint-ci` (ruff + ty) stays green; no runtime surface.
- **Planning:** `just check-planning` validates this bundle + the decision file.
- **Full CI on the PR:** lint, the new docs job, and the pytest matrix all green.

## Risk

- **Low — new PR gate turns red on pre-existing latent breakage.** If any
current docs link/anchor is already broken under the stricter validation, the
first run fails. Mitigation: PR #125 already left links/anchors clean; the
branch build is run before opening the PR.
- **Low — pinning drifts from upstream.** Upper bounds mean a manual bump to
adopt mkdocs-material 10 / mkdocs 2. Acceptable and intended (that is the
point of the pin); the bound documents the tested range.
- **Negligible — docstring/README are prose-only**, no behavior change.
194 changes: 194 additions & 0 deletions planning/changes/2026-07-05.01-docs-ci-drift-guard/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# docs-ci-drift-guard — 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:** Make `mkdocs build --strict` a PR gate with native link/anchor
validation, pin the docs toolchain, fix the `make_outbox_table` 63-byte
docstring, and one-time-audit `README.md`.

**Spec:** [`design.md`](./design.md)

**Branch:** `chore/docs-ci-drift-guard`

**Commit strategy:** Per-task commits; all ship in one PR alongside this bundle.

---

### Task 1: Enable mkdocs link/anchor validation

**Files:**
- Modify: `mkdocs.yml`

Add the `validation:` block so `--strict` fails on broken internal links and
`#anchor` fragments.

- [ ] **Step 1: Add the validation block**

Insert a top-level `validation:` key (mkdocs 1.6+):
```yaml
validation:
omitted_files: warn
absolute_links: warn
unrecognized_links: warn
anchors: warn
```

- [ ] **Step 2: Prove it gates (negative test)**

Temporarily append a broken anchor link (e.g. `[x](usage/basic.md#no-such-anchor)`)
to a docs page, run `just docs-build`, confirm it **fails** with an anchor
warning-as-error, then remove the temporary link.

- [ ] **Step 3: Confirm clean tree passes**

`just docs-build` → "Documentation built"; no warnings-as-errors.

- [ ] **Step 4: Commit**

```bash
git add mkdocs.yml
git commit -m "docs: validate internal links and anchors under mkdocs --strict"
```

---

### Task 2: Pin the docs toolchain

**Files:**
- Modify: `docs/requirements.txt`

Upper-bound the docs deps so the (now gating) strict build is reproducible.

- [ ] **Step 1: Resolve current mkdocs-material series**

`uvx --with-requirements docs/requirements.txt mkdocs --version` and note the
installed `mkdocs-material` major (for the lower bound).

- [ ] **Step 2: Pin with upper bounds**

```
mkdocs>=1.6,<2
mkdocs-material>=<resolved-major>,<<next-major>
```

- [ ] **Step 3: Verify build still passes on the pins**

`just docs-build` → passes.

- [ ] **Step 4: Commit**

```bash
git add docs/requirements.txt
git commit -m "docs: pin mkdocs and mkdocs-material with upper bounds"
```

---

### Task 3: Add the strict docs build as a parallel PR job

**Files:**
- Modify: `.github/workflows/_checks.yml`

Add a `docs` job running `just docs-build`, parallel with `lint`/`pytest`.

- [ ] **Step 1: Add the job**

Mirror the `lint` job's setup steps; final step `- run: just docs-build`.

- [ ] **Step 2: Sanity-check YAML**

`uv run python -c "import yaml,sys; yaml.safe_load(open('.github/workflows/_checks.yml'))"`
(or `just` if a lint target covers workflows) → no parse error.

- [ ] **Step 3: Commit**

```bash
git add .github/workflows/_checks.yml
git commit -m "ci: run mkdocs --strict as a PR check"
```

---

### Task 4: Fix the `make_outbox_table` docstring (#2)

**Files:**
- Modify: `faststream_outbox/schema.py`

Correct the 63-byte-guard wording: the binding identifier is the longest
derived index/constraint name, not the NOTIFY channel.

- [ ] **Step 1: Edit the docstring**

Reword the `Raises ValueError ...` line in `make_outbox_table` to say the
guard trips when the **longest derived identifier** (e.g. `<table>_pending_idx`)
exceeds 63 bytes — longer than the `outbox_<table>` channel.

- [ ] **Step 2: Lint stays green**

`just lint-ci` → ruff + ty pass.

- [ ] **Step 3: Commit**

```bash
git add faststream_outbox/schema.py
git commit -m "docs: correct make_outbox_table 63-byte guard docstring"
```

---

### Task 5: One-time README audit (#3)

**Files:**
- Modify: `README.md` (as findings require)

Audit `README.md` against source + `docs/`; fix confident drift, surface
judgment calls.

- [ ] **Step 1: Verify code samples**

For every code block in `README.md`, confirm imports/symbols/kwargs resolve
against `faststream_outbox/` (grep `__init__.py` exports; check signatures).

- [ ] **Step 2: Verify claims, numbers, links**

Check stated defaults/numbers against source; check every link (badges,
docs-site links, GitHub URLs) resolves.

- [ ] **Step 3: Apply fixes; surface judgment calls**

Fix confident drift in place. Any ambiguous/design-choice item → raise to
maintainer, do not decide unilaterally.

- [ ] **Step 4: Re-verify**

Re-run the import/symbol/link checks on the edited README.

- [ ] **Step 5: Commit**

```bash
git add README.md
git commit -m "docs: fix drift in README"
```

---

### Task 6: Finalize the bundle and open the PR

**Files:**
- Modify: `planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md` (finalize `summary:`)

- [ ] **Step 1: Validate planning**

`just check-planning` → passes (bundle + decision frontmatter, lane, spec link).

- [ ] **Step 2: Full local gate**

`just lint-ci` and `just docs-build` both green.

- [ ] **Step 3: Push + open PR; watch CI**

Push the branch, open the PR, and watch lint + the new `docs` job + the pytest
matrix to green.
Loading