Skip to content

Build the docs in CI, and check Read the Docs before publishing - #478

Merged
ThomasMBury merged 1 commit into
ThomasMBury:mainfrom
energyscholar:ci/docs-build-and-release-gate
Jul 30, 2026
Merged

Build the docs in CI, and check Read the Docs before publishing#478
ThomasMBury merged 1 commit into
ThomasMBury:mainfrom
energyscholar:ci/docs-build-and-release-gate

Conversation

@energyscholar

Copy link
Copy Markdown
Collaborator

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 the stable build triggered by the v2.1.3 tag 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 docs job in tests.yml

Mirrors .readthedocs.yaml deliberately — ubuntu-24.04, Python 3.12, docs/requirements.txt plus the package, and -W for fail_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:

  • It would not have caught the July breakage. That was RTD retiring its ubuntu-20.04 image — 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.
  • RTD also builds pdf and epub. This builds HTML only.

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 newest stable build 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=pypi and skip_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:

docs job green (sphinx 9.1.0, py3.12, RTD's pins) 30578249029
docs job red on a deliberate broken reference, while all 6 test jobs stayed green 30578426187
mismatched tag → existing version guard still fires, publish jobs skipped 30578612771
manual dispatch → gate queries RTD, passes on a green build, publish skipped by the fork guard 30578665903
matching tag → gate blocks, Build sdist and wheel skipped, publish skipped 30578775677

The 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 stable build from the 2.1.3 tag:

newest stable build: id=33821471 state=finished success=False age=1596.2min
::error::Read the Docs stable build 33821471 FAILED -- .../builds/33821471/

which is the exact condition it exists to catch.

Separately

.github/workflows/draft-pdf.yml fails on every push, including yours — it uses actions/upload-artifact@v1, which GitHub now auto-fails as deprecated (checkout@v2 is 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.

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
ThomasMBury merged commit 5e7b950 into ThomasMBury:main Jul 30, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants