Build the docs in CI, and check Read the Docs before publishing - #478
Merged
ThomasMBury merged 1 commit intoJul 30, 2026
Merged
Conversation
2.1.3 shipped while the docs build was red. Read the Docs had failed on the three preceding pushes to main, and the `stable` build triggered by the tag itself failed one minute before the upload. GitHub Actions was green the whole time, because RTD is a separate build system with separate failure modes. Two changes, aimed at different failure classes: - A `docs` job in tests.yml, mirroring .readthedocs.yaml exactly (ubuntu-24.04, Python 3.12, docs/requirements.txt plus the package, -W for fail_on_warning). This adds no new rule: RTD already fails on warnings. It moves that rule earlier, onto the PR. It would NOT have caught the July breakage -- that was RTD retiring its ubuntu-20.04 build image, which a GitHub-hosted build cannot see -- and the job says so in a comment rather than implying coverage it does not have. - A release gate that asks RTD directly, read-only and unauthenticated, so no credential is introduced. It polls for the build this release triggers and fails if that build failed. It deliberately does not key on the build's commit: RTD records an empty commit for builds that fail before checkout, which is precisely the case being caught. The gate can strand a release if RTD is down, so the escape hatch is documented in the workflow header: dispatch with target=pypi and skip_docs_gate=true. Also restrict both publish jobs to the upstream repository. A `v*` tag on any fork currently reaches the trusted-publishing handshake before failing; it should never get that far.
ThomasMBury
approved these changes
Jul 30, 2026
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.
This one's on me, so let me start with what happened.
2.1.3 went to PyPI while the Read the Docs build was red. RTD had failed on the three preceding pushes to
main, and thestablebuild triggered by thev2.1.3tag itself failed at 17:36 — the upload was at 17:37. I checked GitHub Actions (6/6 Python versions green) and a clean-venv install, called the release verified, and never looked at RTD. You found it the next morning.The environment fix was yours and is already in. This is the mechanism, so it can't happen quietly again.
1. A
docsjob intests.ymlMirrors
.readthedocs.yamldeliberately —ubuntu-24.04, Python 3.12,docs/requirements.txtplus the package, and-Wforfail_on_warning. A docs job that's greener than RTD would be worse than none, because it manufactures confidence.It introduces no new rule. RTD already fails on warnings; this just moves that rule earlier, onto the PR, next to the change that caused it. Takes about 40 seconds.
Two limits, both written into the workflow comments so nobody later mistakes this for full coverage:
ubuntu-20.04image — a property of their environment. A GitHub-hosted sphinx build passes fine. Only RTD can tell you about RTD, which is what part 2 is for.2. A release gate that asks RTD directly
Before building, on a tag, the release workflow polls the RTD API for the build this release triggers and fails if that build failed. Read-only and unauthenticated — RTD exposes build state for public projects without a token, and this workflow deliberately stores no long-lived credentials.
It does not key on the build's
commit. RTD records an empty commit string for builds that fail before checkout, which is exactly the case being caught, so commit-matching would silently never fire. It matches the neweststablebuild plus a recency check instead.A gate that can strand a release needs a way past it, so the header documents one: dispatch with
target=pypiandskip_docs_gate=true. It's meant for when RTD is the thing that's broken.3. Publish jobs restricted to this repository
A
v*tag pushed on any fork currently satisfies the publish condition and reaches the trusted-publishing handshake before failing. It shouldn't get that far. This also made it safe to test the above on my fork.Verification
All on my fork, so nothing here is asserted from having written it:
Build sdist and wheelskipped, publish skippedThe last one is the important one: it fails closed. Nothing was built and nothing was published.
I also ran the gate logic against the live API before wiring it up, including against the real failed
stablebuild from the 2.1.3 tag:which is the exact condition it exists to catch.
Separately
.github/workflows/draft-pdf.ymlfails on every push, including yours — it usesactions/upload-artifact@v1, which GitHub now auto-fails as deprecated (checkout@v2is on the same path). Unrelated to anything here and I've left it alone, but it's a third pipeline that's been quietly red. Happy to send a one-line bump if useful.As always, change or drop anything. Nothing here touches the package.