Conversation
check_docs_build.py runs the API reference site build, streams its log and reads every line. Zensical's --strict fails on broken links and anchors but not on the Griffe and mkdocstrings warnings about docstrings, which it prints as bare lines while the build exits 0; the gate fails on those too. It exits 1 on a failed build or any warning line, and 2 when the build did not run to the end: the command could not start, a signal stopped it, or it wrote no index.html. Its tests run in the Audit Script Tests job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs job installs the locked docs group and builds the site through check_docs_build.py, so a pull request fails CI when the site build fails, logs a docstring warning or has a broken link. CI now needs 11 jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs-deploy.yml runs when a release is published, except a prerelease, and when started by hand; never on a push. Its build job installs the docs group and builds the site through check_docs_build.py with the same two steps as the docs job in test.yml, which the gate's tests check, then uploads it. Only the deploy job holds pages: write and id-token: write, in the github-pages environment. The Pages actions are pinned to the commits of their release tags, and the build uses no cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zensical, mkdocstrings and Griffe updates go to a docs-tools group, as the lint tools do, so a release that breaks the site build does not hold up the runtime floor bumps in minor-and-patch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API reference site (PER-16775) renders the SDK with mkdocstrings, which reads it through Griffe. scripts/docs_griffe_extension.py adjusts what Griffe reads: - A name bound in both branches of `if TYPE_CHECKING: ... else: ...` is documented as the `if` branch binds it. Each blocking API class is then the stub class from permit/_sync_types.pyi, with blocking signatures, matched by the full path of the name: permit.pdp_api's SyncRoleAssignmentsApi is the stub's SyncPdpRoleAssignmentsApi, not the REST API's SyncRoleAssignmentsApi. - A function or class decorated with permit.utils.deprecation.deprecated or a PEP 702 deprecated gets a `deprecated` label, and its docstring ends with the decorator's message. Stub methods take the deprecation of the async method they are generated from. - A pydantic field's Field(description=...) becomes its docstring, and its default becomes its value. griffelib joins the dev group, so mypy checks the extension and its offline tests run with the suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API reference site includes MIGRATION.md as a page, where a link relative to the repository root points at a page that does not exist, as it does in the sdist, which ships MIGRATION.md but not skills/. An absolute GitHub URL, as README.md uses for the same folder, works in all three places. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The site that https://permitio.github.io/permit-python/ will serve (PER-16775), built with Zensical 0.0.65 from mkdocs.yml, so that MkDocs with Material stays a fallback while Zensical is in alpha: - Home is README.md and "Upgrading to 3.0" is MIGRATION.md, both included rather than copied. A short page says which client to use. - The reference: the two clients, configuration, exceptions, the enforcement types, one page per permit.api API with its async class and then its blocking twin, the PDP API, Elements, the deprecated flat methods, and a models page. - The models page lists only the 84 models of permit.api.models that a public method takes or returns, with their fields, and links to the REST API reference for the rest. A test fails when a method starts or stops using one, and names it. - Every page links to docs.permit.io for guides, from the navigation. The docs dependency group pins the tools exactly. `uv run --locked --group docs zensical build --strict --clean` builds the site into site/ with no warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CONTRIBUTING.md says how to build and preview the site, what fails the build and why the build runs with --clean, how to add a page, an API class or a model, and that only a release or a manual run deploys it. README.md links to the site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The site section now builds through .github/scripts/check_docs_build.py, as the docs job does, says what its exit codes mean, and names docs-deploy.yml, its prerelease skip and the v* tag rule of the github-pages environment. The CI scripts' tests section lists the gate's tests, as the Audit Script Tests job runs them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uv run --locked exits 1, not 2, when uv.lock is out of date, so a uv error does not always read as "did not run". Say that such an error comes before the gate and prints no verdict, and that CI installs the docs group in its own step, which is where such an error fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs group's comment gives the gate's command, the one CI runs, and mkdocs.yml says what else has to change with site_dir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs group pins it directly, and a new release of it can change how the site's pages render, so its bumps belong in the docs-tools PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The models page lists the models that public methods take or return and links to the REST API reference for the rest, so "every model" in the README line and the site description was wrong. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zensical sets up no logging handler, so Python printed a record logged by any logger other than Griffe's and mkdocstrings' as its bare message. The gate could not tell that line from the rest of the log, and the build passed. The gate's default build now runs Zensical's command line with this script's interpreter under a root handler that prints each record of level WARNING and up as `LEVEL:logger:message`, which the gate already fails on. When zensical is not installed for that interpreter, the gate exits 2 (did not run) with the command to use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a plugin or the Griffe extension raised, the gate exited 1 but its verdict listed only knock-on lines, such as links to the page that failed. The exception was only in the middle of the log. The gate now reads each Python traceback in the log and lists the exception it ends with first, before the warnings. That is Zensical's `RuntimeError: Python error: <exception>` line for a plugin error. A traceback in a build that exited 0 also fails the gate. Zensical's own "Aborted because --strict flag is set" is left out, since the verdict lists the diagnostics that caused it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zensical checks the links of the Markdown pages under docs/ before it renders them. It does not see the links that rendering adds: those in docstrings, and in README.md and MIGRATION.md, which the home page and the migration guide include. A broken link or anchor there passed the gate. After a build that passed, the gate now reads every page of the built site (except 404.html, which the server returns for any missing URL) and fails with exit 1 on each relative href or src that reaches no page or file of the site, or whose #fragment is no id on the page it reaches. Each broken link is listed once per page, with the built page that has it. Links with a scheme or host and absolute paths are not checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Permit.wait_for_sync() is a generator function decorated with contextlib.contextmanager. The site showed its annotation, Generator[Self, None, None], on the async and the blocking client pages, but calling it returns a context manager: a type checker sees contextlib._GeneratorContextManager[Self, None, None]. The Griffe extension now shows the return type of a @contextmanager function as contextlib.AbstractContextManager, that class's public base, of the type the generator yields. It parses the docstring first, so a Yields item without a type still names the yielded type. A return annotation that does not name the yielded type fails the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The extension matched the test of an `if TYPE_CHECKING:` block by its text, as `TYPE_CHECKING` or `typing.TYPE_CHECKING`, so it skipped the `if _typing.TYPE_CHECKING:` blocks of permit/api/models.py and permit/__init__.py. The test of the EmailStr binding in models.py then passed without reaching the check that keeps a runtime import, and no test covered the check that only the `if` branch is read. The extension now takes any `if` whose test is the name TYPE_CHECKING or an attribute of that name. The site does not change: the bindings those blocks make are kept as before. New tests visit small packages for each case: the plain and `_typing.` spellings, an `elif`, a runtime import that stays, an import in the runtime branch that is not read, and an `if not TYPE_CHECKING:` that is not read. The EmailStr test now checks the import's target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploy Docs ran on `release: published` and skipped prereleases with a job condition. Changing a prerelease to a release fires only the `released` activity, so such a release never deployed the site, and the site stayed on the previous release until someone ran Deploy Docs by hand. The workflow now runs on `released`, which fires when a release that is not a prerelease is published and when a prerelease is changed to a release, and never for a prerelease, so the job condition is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment said the step fails when Pages does not publish from GitHub Actions. configure-pages v6.0.0 with enablement off only reads the repository's Pages site and fails when there is none; it does not check the build type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploy Docs and the publish workflow run separately on a release. If the publish workflow's Security Gate or its upload fails, the site still deploys the release's API. CONTRIBUTING now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parity tests compared only the install and build steps of the docs job and the deploy's build job. A change to one workflow's Python version or uv version file would have let the deploy build with a different interpreter, and nothing would have failed. A new test reads the "Install uv" step of both jobs and checks that they use the same action, version-file and python-version, and that the deploy keeps the cache off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The site section's lead-in still said the gate fails on warnings in the build log only; it also fails on a broken link in the built site. The exit-code paragraph is rewrapped to the file's line length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependency Security AuditScanned: pyproject.toml dependencies + dev group, resolved at Python 3.10 (the current resolution, and the lowest versions the published specs permit under each pydantic major) ✅ No known vulnerabilities found. Both the resolved dependency set and the lowest versions the published specs permit are clean at HIGH and CRITICAL. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear issues
Stacked on #151 (docstring fixes), which is stacked on #150 and #144.
Why
permit-python has no API reference of its own; docs.permit.io carries the guides. This adds a reference-only site generated from the SDK's docstrings and type annotations. It shows the signatures a type checker sees, links to docs.permit.io for guides and to the REST API reference for full model schemas, builds on every PR, fails on any docs warning or broken link, and deploys only on release or by hand. It has one version until a second major exists.
What changed
Site (
mkdocs.yml,docs/)mkdocs.yml(Material theme), with mkdocstrings 1.0.6, mkdocstrings-python 2.0.9 on griffelib 2.3.0, and pymdown-extensions 12.1 (highlight, magiclink, snippets, superfences). Each is pinned exactly in a newdocsdependency group, anduv.lockis resolved under the repo's 7-day cooldown. griffelib is also pinned indev, at the same version, for mypy and the offline tests.README.md), "Upgrading to 3.0" (includesMIGRATION.md), "Async or blocking client", and 29 reference pages: the two clients, configuration, exceptions, enforcement types, an index and one page perpermit.apiAPI (the async class, then its blocking twin), the deprecated methods, the PDP API, Elements, and Models. The models page lists the 84 models that public methods take or return, and links to https://api.permit.io/scalar for the rest. A navigation entry links every page to docs.permit.io.Griffe extension (
scripts/docs_griffe_extension.py)if TYPE_CHECKING:(or<module>.TYPE_CHECKING) and rebound at runtime by a class or an assignment, the page shows the type checker's binding, matched by full path. So the blocking API classes,SyncElementsApiandSyncEnforcercome frompermit/_sync_types.pyi, and the PDPSyncRoleAssignmentsApigets the PDP stub class, not the REST one. Runtime imports are kept.deprecatedor a PEP 702deprecated, and appends the decorator's message. Stub methods take the deprecation of the async method they are generated from.@contextmanagerfunction ascontextlib.AbstractContextManager[<yielded type>](Permit.wait_for_sync()). Its Yields section still names the yielded type.Field(description=...)as its docstring and its default as its value.tests/test_docs_griffe_extension.py: 25 offline tests (0.3 s), including the PDP/REST full-path regression and a check that the models page lists exactly the models of public signatures.Build gate (
.github/scripts/check_docs_build.py, stdlib only)zensical build --strict --cleanwith its own interpreter, under a root logging handler, so every record of level WARNING and up prints asLEVEL:logger:message. It streams the build log as it runs.hreforsrcin the built site reaches no page, file or anchor (404.html and absolute links are skipped). That covers links in docstrings and in the includedREADME.mdandMIGRATION.md, which Zensical's own check does not see.site/index.html..github/scripts/test_check_docs_build.py: 45 tests, run in the Audit Script Tests job.CI and deploy
test.yml: a newdocsjob (15-minute timeout,contents: read, SHA-pinned actions,persist-credentials: false) builds the site through the gate.ci.needsincludes it, andEXPECTED_JOBSis 11..github/workflows/docs-deploy.yml(Deploy Docs) runs onrelease: releasedandworkflow_dispatch. Its build job uses the same uv setup, install step and build step as CI (tests keep them identical), with no cache, thenactions/configure-pagesv6.0.0 andactions/upload-pages-artifactv5.0.0. Its deploy job has onlypages: writeandid-token: write, runs inenvironment: github-pagesand usesactions/deploy-pagesv5.0.1. The workflow setsconcurrency: { group: pages, cancel-in-progress: false }, top-levelpermissions: contents: readand 15-minute timeouts. Each action is pinned to its release commit SHA.docs-toolsgroup (zensical, mkdocstrings*, griffe*, pymdown-extensions), listed beforeminor-and-patch.python-sdk-publish.ymlis not changed.Docs
CONTRIBUTING.md: a new section, "The API reference site". It covers building and previewing the site, the gate and its exit codes, adding a page, an API class or a model, and when and how the site deploys.README.md: one sentence linking to the site.MIGRATION.md: the two links to the migration skill are now absolute GitHub URLs, so they work on the site and on PyPI.Behaviour changes
docsjob, so a PR fails CI when the site build fails, logs a warning, or has a broken link or anchor.How it was tested
Args:entry for a missing parameter: exit 1, listing the 2 Griffe lines;MIGRATION.md: exit 1, each listed with its page;docs/: exit 1 (Zensical diagnostic);logginglogger in the extension: exit 1;site_dirpointed at another directory: exit 2;permit.apipages, the blocking class has methods, none labelled async and none with Coroutine or Awaitable types, and the async class's methods are labelled async.SyncRoleAssignmentsApishows onlylist(), withresource_instance_key.RoleRead.nameshows "The name of the role", and noField(call appears on any page.DeprecatedApi.get_user,SyncDeprecatedApi.get_user,TenantsApi.add_user,SyncTenantsApi.add_userandPermitExceptioncarry the deprecated label and message.wait_for_sync()returnsAbstractContextManager[Self]on both client pages, and its See Also URL is a link.uv run pre-commit run --all-files: pass (ruff, mypy, typos,uv lock --check).pytest -m "not e2e"): 1943 passed, 3 skipped, in each of the pydantic 2 and pydantic 1 lanes..github/scriptstests (includingtest_ci_checks.pyandtest_check_docs_build.py): 318 passed. Migration skill tests: 86 passed, 1 skipped.docsjob and Deploy Docs.Owner actions before merge
released, so a prerelease does not deploy and a prerelease later changed to a release does. To deploy prereleases as well, change it totypes: [published].github-pagesenvironment accepts deploys frommainandv*tags only, so tag the releasev3.1.0, as 3.0.0 was taggedv3.0.0. A tag without thevpublishes to PyPI but cannot deploy the site.Docs, so the docs build blocks merges only once the ruleset requiresCIalone (the owner action in Add an aggregate CI check and run every PR job from test.yml #150).main(or wait for the next release) for the first deploy. Check that configure-pages works withpages: readand that the environment protection lets deploy-pages through.🤖 Generated with Claude Code