From 8c0c37cd2b3b5ccbe85fd547dc3c2c44dd734115 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sun, 5 Jul 2026 21:23:19 -0400 Subject: [PATCH 1/2] Deduplicate HTTP route index entries across netapi doc pages The rest_cherrypy, rest_tornado and rest_wsgi doc pages document overlapping HTTP routes, so each shared route registered multiple httpdomain index entries. sphinxcontrib-httpdomain only detects the duplicates in merge_domaindata, which runs when sphinx -j parallel reader chunks are merged, so the -W builds in tools docs (Prepare Release, Documentation) fail intermittently depending on where the chunk boundary lands: WARNING: duplicate HTTP post method definition / in doc/ref/netapi/all/salt.netapi.rest_tornado.rst, other instance is in doc/ref/netapi/all/salt.netapi.rest_wsgi.rst When it hits, the release patch artifact is never produced and every downstream build job in the run fails with 'Artifact not found'. Mark the tornado and wsgi copies of the shared routes with :noindex: so each route is registered exactly once, by the canonical rest_cherrypy reference. noindex'd directives never enter the domain data, so the merge collision is impossible under any chunking. Page content is unchanged and no :http: cross-references exist that could be affected. This also makes the HTTP routing index deterministic; it previously pointed at whichever page the readers processed last. Fixes #69724 --- changelog/69724.fixed.md | 1 + salt/netapi/rest_tornado/saltnado.py | 10 ++++++++++ salt/netapi/rest_wsgi.py | 1 + 3 files changed, 12 insertions(+) create mode 100644 changelog/69724.fixed.md diff --git a/changelog/69724.fixed.md b/changelog/69724.fixed.md new file mode 100644 index 000000000000..ea56ced3d3a2 --- /dev/null +++ b/changelog/69724.fixed.md @@ -0,0 +1 @@ +Fixed the intermittent ``duplicate HTTP post method definition`` failure in the -W parallel docs builds (Prepare Release and Documentation jobs) by marking the HTTP routes documented on the rest_tornado and rest_wsgi pages with ``:noindex:``, leaving rest_cherrypy as the single indexed instance of each shared route. diff --git a/salt/netapi/rest_tornado/saltnado.py b/salt/netapi/rest_tornado/saltnado.py index 99971b778b9d..079662ead805 100644 --- a/salt/netapi/rest_tornado/saltnado.py +++ b/salt/netapi/rest_tornado/saltnado.py @@ -633,6 +633,7 @@ def get(self): # pylint: disable=arguments-differ All logins are done over post, this is a parked endpoint .. http:get:: /login + :noindex: :status 401: |401| :status 406: |406| @@ -672,6 +673,7 @@ def post(self): # pylint: disable=arguments-differ :ref:`Authenticate ` against Salt's eauth system .. http:post:: /login + :noindex: :reqheader X-Auth-Token: |req_token| :reqheader Accept: |req_accept| @@ -803,6 +805,7 @@ def get(self): # pylint: disable=arguments-differ An endpoint to determine salt-api capabilities .. http:get:: / + :noindex: :reqheader Accept: |req_accept| @@ -841,6 +844,7 @@ def post(self): # pylint: disable=arguments-differ Send one or more Salt commands (lowstates) in the request body .. http:post:: / + :noindex: :reqheader X-Auth-Token: |req_token| :reqheader Accept: |req_accept| @@ -1219,6 +1223,7 @@ def get(self, mid=None): # pylint: disable=W0221 details .. http:get:: /minions/(mid) + :noindex: :reqheader X-Auth-Token: |req_token| :reqheader Accept: |req_accept| @@ -1266,6 +1271,7 @@ def post(self): Start an execution command and immediately return the job id .. http:post:: /minions + :noindex: :reqheader X-Auth-Token: |req_token| :reqheader Accept: |req_accept| @@ -1345,6 +1351,7 @@ def get(self, jid=None): # pylint: disable=W0221 the return from a single job .. http:get:: /jobs/(jid) + :noindex: List jobs or show a single job from the job cache. @@ -1445,6 +1452,7 @@ def post(self): ` .. http:post:: /run + :noindex: This entry point is primarily for "one-off" commands. Each request must pass full Salt authentication credentials. Otherwise this URL @@ -1519,6 +1527,7 @@ def get(self): event is formatted as JSON. .. http:get:: /events + :noindex: :status 200: |200| :status 401: |401| @@ -1678,6 +1687,7 @@ def post(self, tag_suffix=None): # pylint: disable=W0221 Fire an event in Salt with a custom event tag and data .. http:post:: /hook + :noindex: :status 200: |200| :status 401: |401| diff --git a/salt/netapi/rest_wsgi.py b/salt/netapi/rest_wsgi.py index 50dfabe23c1e..1f448be4447d 100644 --- a/salt/netapi/rest_wsgi.py +++ b/salt/netapi/rest_wsgi.py @@ -65,6 +65,7 @@ ============== .. http:post:: / + :noindex: **Example request** for a basic ``test.ping``:: From 6be78b752d883eaf8a6f81e5dcc103a04d283e5e Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sun, 5 Jul 2026 22:51:38 -0400 Subject: [PATCH 2/2] Keep HTML anchors on noindex'd httpdomain directives sphinxcontrib-httpdomain's add_target_and_index always appends the signature anchor and only gates the global route registration behind :noindex:, but Sphinx's ObjectDescription.run skips the whole method when noindex is set, so the previous commit's dedup also dropped the per-page anchors and permalinks from the rest_tornado and rest_wsgi endpoint signatures. Anchors are per-document HTML ids and cannot collide across pages; only the global registration can produce the parallel-merge duplicate warnings. Add a small extension that hides the noindex option from Sphinx's outer gate and re-presents it to httpdomain's inner gate, restoring the anchors and permalinks (existing deep links into those pages keep working) while the routes stay out of the domain data, so the duplicate-route collision remains impossible under any chunking. --- doc/_ext/salthttpanchors.py | 45 +++++++++++++++++++++++++++++++++++++ doc/conf.py | 1 + 2 files changed, 46 insertions(+) create mode 100644 doc/_ext/salthttpanchors.py diff --git a/doc/_ext/salthttpanchors.py b/doc/_ext/salthttpanchors.py new file mode 100644 index 000000000000..5822384a2a43 --- /dev/null +++ b/doc/_ext/salthttpanchors.py @@ -0,0 +1,45 @@ +""" +Keep HTML anchors on ``:noindex:``'d httpdomain directives. + +sphinxcontrib-httpdomain's ``add_target_and_index`` intentionally splits its +two jobs: it always appends the ``#--`` anchor to the signature +node, and only gates the global route *registration* behind ``:noindex:``. +Sphinx's ``ObjectDescription.run`` however skips the whole method when +``noindex`` is set, so the anchor (and its permalink) is lost along with the +index entry. Anchors are per-page HTML ids and cannot collide across pages, +so restoring them is safe; only the global registration can produce the +parallel-build duplicate-route warnings. + +Hide the option from Sphinx's outer gate and re-present it to httpdomain's +inner gate, so ``:noindex:`` means what httpdomain meant it to mean: no index +entry, anchor kept. +""" + +from sphinxcontrib.httpdomain import HTTPDomain + + +def _make_anchored(cls): + class AnchoredHTTPResource(cls): + def run(self): + self._salt_noindex = "noindex" in self.options + self.options.pop("noindex", None) + return super().run() + + def add_target_and_index(self, name_cls, sig, signode): + if self._salt_noindex: + self.options["noindex"] = None + try: + super().add_target_and_index(name_cls, sig, signode) + finally: + if self._salt_noindex: + self.options.pop("noindex", None) + + AnchoredHTTPResource.__name__ = f"Anchored{cls.__name__}" + return AnchoredHTTPResource + + +def setup(app): + app.setup_extension("sphinxcontrib.httpdomain") + for name, cls in list(HTTPDomain.directives.items()): + app.add_directive_to_domain("http", name, _make_anchored(cls), override=True) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/doc/conf.py b/doc/conf.py index 64d38b0e7106..0450729512cc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -176,6 +176,7 @@ def _safe_urlsplit(url, scheme="", allow_fragments=True): "sphinx.ext.imgconverter", "sphinx.ext.intersphinx", "sphinxcontrib.httpdomain", + "salthttpanchors", "saltrepo", "myst_parser", #'saltautodoc', # Must be AFTER autodoc