Skip to content

👌 register common CSS files deterministically - #1914

Merged
chrisjsewell merged 1 commit into
useblocks:masterfrom
AlexanderLanin:issue-1913-deterministic-css
Sep 8, 2026
Merged

chrisjsewell merged 1 commit into
useblocks:masterfrom
AlexanderLanin:issue-1913-deterministic-css

Conversation

@AlexanderLanin

Copy link
Copy Markdown
Contributor

Why

Common CSS files are discovered with Path.glob(), whose iteration order is not guaranteed. Because Sphinx emits same-priority stylesheets in registration order, this can make generated HTML and the CSS cascade vary between otherwise identical builds.

What changed

  • Sort common CSS paths by filename before registering them.
  • Add regression coverage asserting that registered common CSS files are ordered deterministically.

Fixes #1913

@AlexanderLanin
AlexanderLanin force-pushed the issue-1913-deterministic-css branch from 30bbb7c to bc4df08 Compare September 7, 2026 21:24
@github-actions github-actions Bot added the pkg: sphinx-needs The sphinx-needs distribution (packages/sphinx-needs): its code, tests and docs label Sep 7, 2026
@chrisjsewell chrisjsewell changed the title fix: register common CSS files deterministically 👌 register common CSS files deterministically Sep 8, 2026

@chrisjsewell chrisjsewell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a real one and the fix is the right shape.

Reproduced. With the environment.py change reverted, a build on macOS (APFS, which iterates directories in hash order like ext4) registers the common stylesheets as need_style, need_core, needstable, need_links, need_toggle, and the new assertion in test_file_registration fails on it. So the test is a genuine fence, not just a restatement of the fix.

Sorting is safe for the cascade. The five files under css/common/ hold 110 selectors between them and no selector appears in more than one file, so their relative order has no effect on what a page looks like. Alphabetical is also the order NTFS builds were already producing, so Windows users see no change at all.

Nothing else has the pattern. The other two glob calls in the package (the theme lookup in the same function, and clear_debug_dir) are a membership test and a delete loop, so registration order is only decided here.

For the next changelog: the common stylesheets are now registered in filename order, so identical builds produce identical HTML heads.

@chrisjsewell
chrisjsewell merged commit aaa815d into useblocks:master Sep 8, 2026
31 checks passed
chrisjsewell added a commit that referenced this pull request Oct 14, 2026
Six members of the workspace released together, in one pull request and
three tag waves: sphinx-needs 9.0.0 (the Sphinx half of the
derived-values lockstep with ubCode's next release) and the five
packages around it. Everything mechanical is `uv run poe bump`'s, one
commit per member; the prose, the gap bullets and the version-marker
sweep are by hand, each claim measured against the tree.

### What this releases

| member | from → to | level | why |
|---|---|---|---|
| sphinx-ai-index | 0.1.0 → **1.0.0** | major | the `ai_docs_index.json`
it writes has been the format's `"1.0"` since the first release, and
ubCode reads it in production — that is the stability 1.0 declares
(#2034); this is also its first release from this repository, on the
workspace's Python 3.11 / Sphinx 7.4–9 / docutils 0.21 floors |
| sphinx-mounts | 0.2.0 → **0.3.0** | minor (0.x) | variant data from
`[variants]` through ub-project, with new refusals and two new
`-W`-failing warnings; Python 3.11 again; docutils 0.21; first release
from this repository |
| sphinx-needs | 8.5.0 → **9.0.0** | major | computed values (derived
fields, dependency order), `needextend` filters and the needs.json
readers can change need data in certain corner cases; `[[…]]` in content
and `need_func` removed; Python 3.11; `[variants]`; `needstable.js` |
| ub-test-reports | 1.0.0 → **1.1.0** | minor | parser and converter
fixes that add `ReportReadError` and change which exceptions escape and
what a nested-suite report converts to |
| sphinx-codelinks | 1.4.0 → **2.0.0** | major | requires Sphinx-Needs
9; `get_rst` / `[analyse.marked_rst]` refused, the `@rst` blocks
replaced by multi-line needs; first release from this repository |
| sphinx-test-reports | 2.0.0 → **3.0.0** | major | the Sphinx-free half
is ub-test-reports; the import name moves to `sphinx_test_reports`;
requires Sphinx-Needs 9 and ub-test-reports 1.1 |

ub-project stays at 1.1.0: nothing to release.

The six versions, the `__version__` literals, the floors
(`sphinx-ai-index>=1.0.0,<2` in four `docs` extras;
`sphinx-needs>=9.0.0,<10` in codelinks and test-reports;
`ub-test-reports>=1.1.0,<2` in test-reports), the docker fallback
(`sphinx-needs-v9.0.0`), the lock and the six changelog headings are `uv
run poe bump`'s, one commit per member in the planner's order.

### What was added by hand

- **sphinx-needs 9.0.0**: bullets for the changes the entry had not
recorded — ‼️ `[[…]]` in need content is plain text and the `need_func`
role is removed (#1919); ✨ the needtable script (`needstable.js`,
`needs_table_page_size` / `needs_table_page_sizes`, Excel / PDF export
and column reordering removed; #1920); ✨ `[variants]` read through
ub-project, a new dependency, with two new warning types (#1996, #1998);
⬆️ docutils 0.21 (#1977); ⬆️ jsonschema-rs up to 0.58 (#1851, #1980); 👌
deterministic CSS order (#1914); 🔧 the uv workspace, the
`#subdirectory=packages/sphinx-needs` git install and the
`sphinx-needs-v<version>` tags (#1839, #1846), in a new "Internal
changes" section; 🔧 the sdist without `tests/` / `performance/` (#1905);
🐛 needgantt's integer completion (#1870); and the derived-fields bullet
cites #2204 and #2205.
- **sphinx-mounts 0.3.0**: the move bullet (#1850).
- **sphinx-codelinks 2.0.0**: 🐛 `get_current_rev` reads `packed-refs`
(#1871); the ‼️ floor bullet says Sphinx-Needs 9.0 / `>=9.0.0,<10`;
`codelinks write rst` "will be removed in 3.0.0"; the documentation is
served by Read the Docs; the `ingest_need_record` note trimmed.
- **sphinx-test-reports 3.0.0**: the floors it ships
(`sphinx-needs>=9.0.0,<10`, ub-test-reports 1.1.0).
- **ub-test-reports 1.1.0**: the UTF-8 bullet links #2024.
- **The six summary paragraphs**, one per entry (sphinx-ai-index's
replaces its old lead).
- **Pages outside the changelogs** (each states what this release
ships):
- sphinx-codelinks' `codelinks write rst` deprecation
(`WRITE_RST_DEPRECATED` in `cmd.py`, and `docs/components/write.rst`)
now promises removal in 3.0.0, not 2.0.0, and is dated 2.0.0, not an
uncut 1.5.0: 2.0.0 ships with the command.
- `docs/basics/introduction.rst` says `src-trace` renders multi-line
needs.
- sphinx-needs' `docs/directives/choose.rst` says `versionadded::
9.0.0`, not an uncut 8.6.0, and `docs/configuration.rst` says
sphinx-mounts reads `[variants]` "from 0.3.0", not "from its next
release".
- sphinx-test-reports' `docs/install.rst` names the Sphinx-Needs 9 floor
(`>=9.0.0,<10`).
- Comments and agent notes naming the old `>=8.5.0,<9` floor
(`compat-requirements.txt`, two `AGENTS.md`, `src_trace.py`) name 9.0.0.
- A sweep of all 216 version markers (`versionadded` / `versionchanged`
/ `deprecated`) in the six members' docs and sources, against each
member's released versions, found 13 more that named a release never
cut. Each now names the release that shipped it: mounts 0.2.0 ×2,
sphinx-needs 3.0.0 ×7, 4.0.0 ×1 and 7.0.0 ×1, test-reports 1.1.0 ×2.

### Merge and tag order

Merge RIGHT BEFORE tagging. **Re-stamp `:Released:` on the merge day
first** (it is stamped 2026-10-14, the earliest tag day); one line per
file, each in its own format:

| file | line | format |
|---|---|---|
| `packages/sphinx-ai-index/docs/changelog.rst` | l.11 `:Released:
2026-10-14` | ISO `YYYY-MM-DD` |
| `packages/sphinx-mounts/docs/changelog.rst` | l.11 `:Released:
2026-10-14` | ISO |
| `packages/sphinx-needs/docs/changelog.rst` | l.12 `:Released:
14.10.2026` | `DD.MM.YYYY` |
| `packages/ub-test-reports/docs/changelog.rst` | l.11 `:Released:
2026-10-14` | ISO |
| `packages/sphinx-codelinks/docs/changelog.rst` | l.11 `:Released:
14.10.2026` | `DD.MM.YYYY` |
| `packages/sphinx-test-reports/docs/changelog.rst` | l.11 `:Released:
14.10.2026` | `DD.MM.YYYY` |

Then tag the squash commit `<sha>` explicitly (so nothing merged later
can slip into a release), in three waves, each after the previous wave's
`publish` jobs are green — the plan job's check 4 refuses a member whose
workspace dependencies (extras included) are not on PyPI at the version
this tree builds:

```bash
# wave 1 — dependencies already on PyPI (ub-project 1.1.0) or none
git tag sphinx-ai-index-v1.0.0 <sha> && git push origin sphinx-ai-index-v1.0.0
git tag ub-test-reports-v1.1.0 <sha> && git push origin ub-test-reports-v1.1.0
# wave 2 — once sphinx-ai-index 1.0.0 is on PyPI
git tag sphinx-mounts-v0.3.0 <sha> && git push origin sphinx-mounts-v0.3.0
git tag sphinx-needs-v9.0.0 <sha> && git push origin sphinx-needs-v9.0.0
# wave 3 — once sphinx-needs 9.0.0 and ub-test-reports 1.1.0 are on PyPI
git tag sphinx-codelinks-v2.0.0 <sha> && git push origin sphinx-codelinks-v2.0.0
git tag sphinx-test-reports-v3.0.0 <sha> && git push origin sphinx-test-reports-v3.0.0
```

The bare `9.0.0` tag is pushed by `release.yaml`'s last step for
sphinx-needs ("Push the bare companion tag"); it is what moves Read the
Docs' `stable` and keeps `git+…@<version>` pins working. Only if that
step fails: `git tag 9.0.0 sphinx-needs-v9.0.0 && git push origin
refs/tags/9.0.0`. A tag pushed too early fails only its plan job, with
nothing published: re-run it once the dependency is up. After a green
`publish`, a red later job is re-run with **Re-run failed jobs**, never
*Re-run all jobs*.

### Before wave 1

Five of these PyPI projects release through `release.yaml` for the first
time — sphinx-ai-index, sphinx-mounts, sphinx-needs (8.5.0 went out
through the old token-based workflow), sphinx-codelinks and
sphinx-test-reports. Each must list the trusted publisher **owner
`useblocks`, repository `sphinx-needs`, workflow `release.yaml`,
environment `pypi-<dist>`**. The GitHub side is ready (the five
`pypi-<dist>` environments exist, each with the tag policy `<dist>-v*`);
the PyPI side can only be checked by a project owner. A wrong publisher
fails `publish` with nothing uploaded — fix it and re-run from the same
tag — but sphinx-ai-index's is the very first tag, so it would stall the
whole train.

### Rehearsal

Two rehearsals can pass before the merge, from this branch:

```bash
gh workflow run release.yaml --ref release/9.0.0-train -f tag=sphinx-ai-index-v1.0.0
gh workflow run release.yaml --ref release/9.0.0-train -f tag=ub-test-reports-v1.1.0
```

Dispatched from this branch at `9a2dd5ec`: run
[38479550037](https://github.com/useblocks/sphinx-needs/actions/runs/38479550037)
(`sphinx-ai-index-v1.0.0`) and run
[38479553168](https://github.com/useblocks/sphinx-needs/actions/runs/38479553168)
(`ub-test-reports-v1.1.0`). A dispatch runs `plan` and `build` (the
resolver gate and the compat cell) and never `publish`. It must name
this branch: at `master` the tag's version is not the tree's (check 2).
The other four cannot pass yet, because check 4 stays fatal in a
rehearsal: sphinx-mounts and sphinx-needs need sphinx-ai-index 1.0.0 on
PyPI (their `docs` extras), sphinx-codelinks and sphinx-test-reports
need sphinx-needs 9.0.0 (and test-reports ub-test-reports 1.1.0). They
can be rehearsed from `master` once the wave before them has published.

### What goes red and why

- **Docs-Linkcheck** (not a required check): the new
`8.5.0...sphinx-needs-v9.0.0` compare link 404s until the
`sphinx-needs-v9.0.0` tag exists.
- **`uv run poe import-check-codelinks` / `import-check-reports`**: red
until wave 2 publishes — "only sphinx-needs<=8.5.0 is available and
sphinx-codelinks==2.0.0 depends on sphinx-needs>=9.0.0,<10". Run them on
`master` between waves 2 and 3: red then would be a real missing symbol.
The other five import checks are green on this branch.
- **Docker**: the fallback ref becomes `sphinx-needs-v9.0.0`, so the
`docker/**` pull-request and manual-dispatch path, red since the move,
goes green once that tag exists. This PR does not run it (its trigger is
`docker/**` only), and the tag push builds the image from the tag
itself.

### For a human (outside the repository)

- **PyPI trusted publishers**: the five checks under "Before wave 1".
- **codelinks.useblocks.com deep links**: the old GitHub Pages site
served pages at the root (`…/components/analyse.html`). Under Read the
Docs they 404 and live under `/en/latest/`. A Read the Docs page
redirect (old path → `/en/latest/<path>`) on the `sphinx-codelinks`
project would keep bookmarks and inbound links working.
- **Read the Docs `stable` for sphinx-mounts and sphinx-codelinks**
serves the Sphinx-Needs 8.5.0 documentation today (both projects build
from this repository, whose highest PEP 440 tag is sphinx-needs' bare
`8.5.0`). After this train it follows the bare `9.0.0` tag, then every
later sphinx-needs release, and never a mounts- or codelinks-only one.
Consider deactivating `stable`, or making `latest` the default version,
on those two projects.

### ubCode

ubCode's next release names sphinx-needs 9.0.0 as the version its
derived-values reading pairs with. This changelog names no ubCode
version, by design: it keeps the entry's "phase-2 release" wording. The
ubCode release pull request is prepared separately.

Closes #2034
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: sphinx-needs The sphinx-needs distribution (packages/sphinx-needs): its code, tests and docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make common CSS file registration deterministic

2 participants