This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 602e150aace Allow building docs with a local sphinx_airflow_theme 
checkout (#75001)
602e150aace is described below

commit 602e150aace0ed5d4e22cd4df0cf49949bcd669d
Author: Shahar Epstein <[email protected]>
AuthorDate: Sat Oct 24 08:12:46 2026 +0300

    Allow building docs with a local sphinx_airflow_theme checkout (#75001)
---
 contributing-docs/11_documentation_building.rst    | 11 +++
 dev/breeze/doc/images/output_build-docs.svg        | 52 ++++++++----
 dev/breeze/doc/images/output_build-docs.txt        |  2 +-
 .../airflow_breeze/commands/developer_commands.py  | 27 ++++++
 .../commands/developer_commands_config.py          |  1 +
 .../src/airflow_breeze/params/shell_params.py      |  5 ++
 dev/breeze/src/airflow_breeze/utils/path_utils.py  |  3 +
 dev/breeze/tests/test_developer_commands.py        | 96 +++++++++++++++++-----
 dev/breeze/tests/test_shell_params.py              | 14 ++++
 docs/README.md                                     |  4 +-
 .../docker-compose/mount-sphinx-airflow-theme.yml} | 24 +++---
 scripts/in_container/run_docs_build.sh             |  6 ++
 12 files changed, 189 insertions(+), 56 deletions(-)

diff --git a/contributing-docs/11_documentation_building.rst 
b/contributing-docs/11_documentation_building.rst
index 1297b1048b8..b8e24ccbc4f 100644
--- a/contributing-docs/11_documentation_building.rst
+++ b/contributing-docs/11_documentation_building.rst
@@ -241,6 +241,17 @@ For example:
 
 Will build ``fab`` provider documentation and clean build artifacts before.
 
+To try a change to the HTML theme (``sphinx_airflow_theme`` from the
+`airflow-site <https://github.com/apache/airflow-site>`_ repository) on real 
documentation, build with the
+theme folder of an ``airflow-site`` checkout instead of the published wheel:
+
+.. code-block:: bash
+
+    breeze build-docs apache-airflow --sphinx-airflow-theme-path 
../airflow-site/sphinx_airflow_theme
+
+Run ``./site.sh build-landing-pages && ./site.sh prepare-theme`` in 
``airflow-site`` first to generate the
+theme's CSS/JS. Breeze mounts the folder read-only and installs the theme in 
the container before the build.
+
 Inventory cache handling
 ........................
 
diff --git a/dev/breeze/doc/images/output_build-docs.svg 
b/dev/breeze/doc/images/output_build-docs.svg
index 45d78b15e35..10d0e93d55f 100644
--- a/dev/breeze/doc/images/output_build-docs.svg
+++ b/dev/breeze/doc/images/output_build-docs.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1514.0" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 1611.6" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-build-docs-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="1463.0" />
+      <rect x="0" y="0" width="1463.0" height="1560.6" />
     </clipPath>
     <clipPath id="breeze-build-docs-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -222,9 +222,21 @@
 <clipPath id="breeze-build-docs-line-58">
     <rect x="0" y="1416.7" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-build-docs-line-59">
+    <rect x="0" y="1441.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-build-docs-line-60">
+    <rect x="0" y="1465.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-build-docs-line-61">
+    <rect x="0" y="1489.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-build-docs-line-62">
+    <rect x="0" y="1514.3" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1512" rx="8"/><text 
class="breeze-build-docs-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;build-docs</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1609.6" rx="8"/><text 
class="breeze-build-docs-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;build-docs</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -279,21 +291,25 @@
 </text><text class="breeze-build-docs-r5" x="0" y="1044.8" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-42)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1044.8" textLength="353.8" 
clip-path="url(#breeze-build-docs-line-42)">--include-removed-providers&#160;&#160;</text><text
 class="breeze-build-docs-r1" x="402.6" y="1044.8" textLength="561.2" 
clip-path="url(#breeze-build-docs-line-42)">Whether&#160;to&#160;include&#160;providers&#160;that&#160;are&#160;removed.</te
 [...]
 </text><text class="breeze-build-docs-r5" x="0" y="1069.2" textLength="1464" 
clip-path="url(#breeze-build-docs-line-43)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-build-docs-r1" x="1464" y="1069.2" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-43)">
 </text><text class="breeze-build-docs-r5" x="0" y="1093.6" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-44)">╭─</text><text 
class="breeze-build-docs-r5" x="24.4" y="1093.6" textLength="170.8" 
clip-path="url(#breeze-build-docs-line-44)">&#160;Misc&#160;options&#160;</text><text
 class="breeze-build-docs-r5" x="195.2" y="1093.6" textLength="1244.4" 
clip-path="url(#breeze-build-docs-line-44)">────────────────────────────────────────────────────────────────────────────────────────
 [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1118" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-45)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1118" textLength="244" 
clip-path="url(#breeze-build-docs-line-45)">--include-commits&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r1" x="341.6" y="1118" textLength="451.4" 
clip-path="url(#breeze-build-docs-line-45)">Include&#160;commits&#160;in&#160;the&#160;documentation.</text><text
 class="breeze-build-do [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1142.4" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-46)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1142.4" textLength="244" 
clip-path="url(#breeze-build-docs-line-46)">--github-repository&#160;</text><text
 class="breeze-build-docs-r7" x="292.8" y="1142.4" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-46)">-g</text><text 
class="breeze-build-docs-r1" x="341.6" y="1142.4" textLength="597.8" 
clip-path="ur [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1166.8" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-47)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1166.8" textLength="244" 
clip-path="url(#breeze-build-docs-line-47)">--builder&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r1" x="341.6" y="1166.8" textLength="768.6" 
clip-path="url(#breeze-build-docs-line-47)">Buildx&#160;builder&#160;used&#160;to&#160;per
 [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1191.2" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-48)">│</text><text 
class="breeze-build-docs-r6" x="341.6" y="1191.2" textLength="73.2" 
clip-path="url(#breeze-build-docs-line-48)">(TEXT)</text><text 
class="breeze-build-docs-r5" x="1451.8" y="1191.2" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-48)">│</text><text 
class="breeze-build-docs-r1" x="1464" y="1191.2" textLength="12.2" 
clip-path="url(#breeze-build-doc [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1215.6" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-49)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1215.6" textLength="244" 
clip-path="url(#breeze-build-docs-line-49)">--distributions-list</text><text 
class="breeze-build-docs-r1" x="341.6" y="1215.6" textLength="1098" 
clip-path="url(#breeze-build-docs-line-49)">Optional,&#160;contains&#160;space&#160;separated&#160;list&#160;of&#160;package&#160;ids&#160;that&#1
 [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1240" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-50)">│</text><text 
class="breeze-build-docs-r1" x="341.6" y="1240" textLength="1098" 
clip-path="url(#breeze-build-docs-line-50)">documentation&#160;building,&#160;and&#160;document&#160;publishing.&#160;It&#160;is&#160;an&#160;easier&#160;alternative&#160;to&#160;adding&#160;&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r5" x="1451.8" y="1240" textLength="12.2" clip [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1264.4" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-51)">│</text><text 
class="breeze-build-docs-r1" x="341.6" y="1264.4" textLength="1098" 
clip-path="url(#breeze-build-docs-line-51)">individual&#160;packages&#160;as&#160;arguments&#160;to&#160;every&#160;command.&#160;This&#160;overrides&#160;the&#160;packages&#160;passed&#160;as&#160;&#160;</text><text
 class="breeze-build-docs-r5" x="1451.8" y="1264.4" textLength="12.2" cli [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1288.8" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-52)">│</text><text 
class="breeze-build-docs-r1" x="341.6" y="1288.8" textLength="134.2" 
clip-path="url(#breeze-build-docs-line-52)">arguments.&#160;</text><text 
class="breeze-build-docs-r6" x="475.8" y="1288.8" textLength="73.2" 
clip-path="url(#breeze-build-docs-line-52)">(TEXT)</text><text 
class="breeze-build-docs-r5" x="1451.8" y="1288.8" textLength="12.2" 
clip-path="url( [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1313.2" textLength="1464" 
clip-path="url(#breeze-build-docs-line-53)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-build-docs-r1" x="1464" y="1313.2" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-53)">
-</text><text class="breeze-build-docs-r5" x="0" y="1337.6" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-54)">╭─</text><text 
class="breeze-build-docs-r5" x="24.4" y="1337.6" textLength="195.2" 
clip-path="url(#breeze-build-docs-line-54)">&#160;Common&#160;options&#160;</text><text
 class="breeze-build-docs-r5" x="219.6" y="1337.6" textLength="1220" 
clip-path="url(#breeze-build-docs-line-54)">────────────────────────────────────────────────────────────────────────────────────────
 [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1362" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-55)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1362" textLength="109.8" 
clip-path="url(#breeze-build-docs-line-55)">--dry-run</text><text 
class="breeze-build-docs-r7" x="158.6" y="1362" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-55)">-D</text><text 
class="breeze-build-docs-r1" x="207.4" y="1362" textLength="719.8" 
clip-path="url(#breeze-build-docs-l [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1386.4" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-56)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1386.4" textLength="109.8" 
clip-path="url(#breeze-build-docs-line-56)">--verbose</text><text 
class="breeze-build-docs-r7" x="158.6" y="1386.4" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-56)">-v</text><text 
class="breeze-build-docs-r1" x="207.4" y="1386.4" textLength="585.6" 
clip-path="url(#breeze-buil [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1410.8" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-57)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1410.8" textLength="109.8" 
clip-path="url(#breeze-build-docs-line-57)">--answer&#160;</text><text 
class="breeze-build-docs-r7" x="158.6" y="1410.8" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-57)">-a</text><text 
class="breeze-build-docs-r1" x="207.4" y="1410.8" textLength="329.4" 
clip-path="url(#breeze [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1435.2" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-58)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1435.2" textLength="109.8" 
clip-path="url(#breeze-build-docs-line-58)">--help&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r7" x="158.6" y="1435.2" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-58)">-h</text><text 
class="breeze-build-docs-r1" x="207.4" y="1435.2" textLength="329.4" 
clip-path="u [...]
-</text><text class="breeze-build-docs-r5" x="0" y="1459.6" textLength="1464" 
clip-path="url(#breeze-build-docs-line-59)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-build-docs-r1" x="1464" y="1459.6" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-59)">
+</text><text class="breeze-build-docs-r5" x="0" y="1118" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-45)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1118" textLength="329.4" 
clip-path="url(#breeze-build-docs-line-45)">--include-commits&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r1" x="427" y="1118" textLength="451.4" 
clip-path="url(#breeze-build-docs-line-45)">Include&#160;commits&#160;in&#160;the&#160;documen
 [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1142.4" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-46)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1142.4" textLength="329.4" 
clip-path="url(#breeze-build-docs-line-46)">--github-repository&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r7" x="378.2" y="1142.4" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-46)">-g</text><text 
class="breeze-build-docs-r1" x="427" y [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1166.8" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-47)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1166.8" textLength="329.4" 
clip-path="url(#breeze-build-docs-line-47)">--builder&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r1" x="427" y="1166.8" textLength="768.6" 
clip-path="url(#breeze-build-docs-line-47)">Buil [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1191.2" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-48)">│</text><text 
class="breeze-build-docs-r5" x="427" y="1191.2" textLength="134.2" 
clip-path="url(#breeze-build-docs-line-48)">autodetect]</text><text 
class="breeze-build-docs-r6" x="573.4" y="1191.2" textLength="73.2" 
clip-path="url(#breeze-build-docs-line-48)">(TEXT)</text><text 
class="breeze-build-docs-r5" x="1451.8" y="1191.2" textLength="12.2" 
clip-path="url(#breeze [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1215.6" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-49)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1215.6" textLength="329.4" 
clip-path="url(#breeze-build-docs-line-49)">--distributions-list&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r1" x="427" y="1215.6" textLength="1012.6" 
clip-path="url(#breeze-build-docs-line-49)">Optional,&#160;contains&#160;space&#160;separated&#160;lis
 [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1240" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-50)">│</text><text 
class="breeze-build-docs-r1" x="427" y="1240" textLength="1012.6" 
clip-path="url(#breeze-build-docs-line-50)">documentation&#160;building,&#160;and&#160;document&#160;publishing.&#160;It&#160;is&#160;an&#160;easier&#160;alternative&#160;to&#160;&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r5" x="1451.8" y="1240" textLength="12.2" 
clip-path="url(# [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1264.4" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-51)">│</text><text 
class="breeze-build-docs-r1" x="427" y="1264.4" textLength="1012.6" 
clip-path="url(#breeze-build-docs-line-51)">adding&#160;individual&#160;packages&#160;as&#160;arguments&#160;to&#160;every&#160;command.&#160;This&#160;overrides&#160;the&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r5" x="1451.8" y="1264.4" textLength="1 [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1288.8" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-52)">│</text><text 
class="breeze-build-docs-r1" x="427" y="1288.8" textLength="366" 
clip-path="url(#breeze-build-docs-line-52)">packages&#160;passed&#160;as&#160;arguments.&#160;</text><text
 class="breeze-build-docs-r6" x="793" y="1288.8" textLength="73.2" 
clip-path="url(#breeze-build-docs-line-52)">(TEXT)</text><text 
class="breeze-build-docs-r5" x="1451.8" y="1288.8" textL [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1313.2" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-53)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1313.2" textLength="329.4" 
clip-path="url(#breeze-build-docs-line-53)">--sphinx-airflow-theme-path</text><text
 class="breeze-build-docs-r1" x="427" y="1313.2" textLength="1012.6" 
clip-path="url(#breeze-build-docs-line-53)">Build&#160;the&#160;docs&#160;with&#160;the&#160;`sphinx_airflow_theme`&#160;sub-folder&#160;
 [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1337.6" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-54)">│</text><text 
class="breeze-build-docs-r1" x="427" y="1337.6" textLength="1012.6" 
clip-path="url(#breeze-build-docs-line-54)">checkout&#160;(e.g.&#160;`../airflow-site/sphinx_airflow_theme`,&#160;not&#160;the&#160;repository&#160;root),&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r5" x="1451.8" y="1337.6" textLength="12.2" 
clip-path="url(#breeze-build-docs-l [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1362" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-55)">│</text><text 
class="breeze-build-docs-r1" x="427" y="1362" textLength="1012.6" 
clip-path="url(#breeze-build-docs-line-55)">after&#160;`./site.sh&#160;build-landing-pages&#160;&amp;&amp;&#160;./site.sh&#160;prepare-theme`,&#160;instead&#160;of&#160;the&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r5" x="1451.8" y="1362" textLength="12.2" 
clip-path="url(#breeze- [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1386.4" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-56)">│</text><text 
class="breeze-build-docs-r1" x="427" y="1386.4" textLength="207.4" 
clip-path="url(#breeze-build-docs-line-56)">published&#160;theme.&#160;</text><text
 class="breeze-build-docs-r6" x="634.4" y="1386.4" textLength="134.2" 
clip-path="url(#breeze-build-docs-line-56)">(DIRECTORY)</text><text 
class="breeze-build-docs-r5" x="1451.8" y="1386.4" textLength="12.2"  [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1410.8" textLength="1464" 
clip-path="url(#breeze-build-docs-line-57)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-build-docs-r1" x="1464" y="1410.8" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-57)">
+</text><text class="breeze-build-docs-r5" x="0" y="1435.2" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-58)">╭─</text><text 
class="breeze-build-docs-r5" x="24.4" y="1435.2" textLength="195.2" 
clip-path="url(#breeze-build-docs-line-58)">&#160;Common&#160;options&#160;</text><text
 class="breeze-build-docs-r5" x="219.6" y="1435.2" textLength="1220" 
clip-path="url(#breeze-build-docs-line-58)">────────────────────────────────────────────────────────────────────────────────────────
 [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1459.6" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-59)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1459.6" textLength="109.8" 
clip-path="url(#breeze-build-docs-line-59)">--dry-run</text><text 
class="breeze-build-docs-r7" x="158.6" y="1459.6" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-59)">-D</text><text 
class="breeze-build-docs-r1" x="207.4" y="1459.6" textLength="719.8" 
clip-path="url(#breeze-buil [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1484" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-60)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1484" textLength="109.8" 
clip-path="url(#breeze-build-docs-line-60)">--verbose</text><text 
class="breeze-build-docs-r7" x="158.6" y="1484" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-60)">-v</text><text 
class="breeze-build-docs-r1" x="207.4" y="1484" textLength="585.6" 
clip-path="url(#breeze-build-docs-l [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1508.4" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-61)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1508.4" textLength="109.8" 
clip-path="url(#breeze-build-docs-line-61)">--answer&#160;</text><text 
class="breeze-build-docs-r7" x="158.6" y="1508.4" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-61)">-a</text><text 
class="breeze-build-docs-r1" x="207.4" y="1508.4" textLength="329.4" 
clip-path="url(#breeze [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1532.8" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-62)">│</text><text 
class="breeze-build-docs-r4" x="24.4" y="1532.8" textLength="109.8" 
clip-path="url(#breeze-build-docs-line-62)">--help&#160;&#160;&#160;</text><text
 class="breeze-build-docs-r7" x="158.6" y="1532.8" textLength="24.4" 
clip-path="url(#breeze-build-docs-line-62)">-h</text><text 
class="breeze-build-docs-r1" x="207.4" y="1532.8" textLength="329.4" 
clip-path="u [...]
+</text><text class="breeze-build-docs-r5" x="0" y="1557.2" textLength="1464" 
clip-path="url(#breeze-build-docs-line-63)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-build-docs-r1" x="1464" y="1557.2" textLength="12.2" 
clip-path="url(#breeze-build-docs-line-63)">
 </text>
     </g>
     </g>
diff --git a/dev/breeze/doc/images/output_build-docs.txt 
b/dev/breeze/doc/images/output_build-docs.txt
index 5105c9d092b..fca340f3bd2 100644
--- a/dev/breeze/doc/images/output_build-docs.txt
+++ b/dev/breeze/doc/images/output_build-docs.txt
@@ -1 +1 @@
-585c88f391fe92933f3d30f4bff0be5b
+64d40b641cf8161bf9acf27704df91da
diff --git a/dev/breeze/src/airflow_breeze/commands/developer_commands.py 
b/dev/breeze/src/airflow_breeze/commands/developer_commands.py
index e7ba6eefbd5..a320cb633ab 100644
--- a/dev/breeze/src/airflow_breeze/commands/developer_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/developer_commands.py
@@ -901,6 +901,7 @@ def _build_python_docs(
     package_filter: tuple[str, ...],
     distributions_list: str,
     doc_packages: tuple[str, ...],
+    sphinx_airflow_theme_path: str,
 ):
     # Docs are always built on the default Python. The Sphinx configuration 
mocks third-party
     # modules, and what that mocking does depends on the interpreter - on 3.12 
functools copies
@@ -964,6 +965,7 @@ def _build_python_docs(
         github_repository=github_repository,
         python=DEFAULT_PYTHON_MAJOR_MINOR_VERSION,
         mount_sources=MOUNT_ALL,
+        sphinx_airflow_theme_path=sphinx_airflow_theme_path,
     )
     result = execute_command_in_shell(shell_params, 
project_name="breeze-docs", command=cmd)
     fix_ownership_using_docker()
@@ -976,6 +978,16 @@ def _build_python_docs(
     return sphinx_returncode
 
 
+def _validate_sphinx_airflow_theme_path(ctx: click.Context, param: 
click.Parameter, value: str | None):
+    if value and not Path(value, "sphinx_airflow_theme/static/_gen").is_dir():
+        raise click.BadParameter(
+            f"{value} has no sphinx_airflow_theme/static/_gen. It must be the 
`sphinx_airflow_theme` "
+            "sub-folder of an airflow-site checkout (not the repository root), 
"
+            "after `./site.sh build-landing-pages && ./site.sh prepare-theme`."
+        )
+    return value
+
+
 @main.command(name="build-docs")
 @option_builder
 @click.option(
@@ -1035,6 +1047,14 @@ def _build_python_docs(
     is_flag=True,
     help="Only build SDK docs. Requires at least one --sdk value to be 
useful.",
 )
[email protected](
+    "--sphinx-airflow-theme-path",
+    type=click.Path(exists=True, file_okay=False, resolve_path=True),
+    callback=_validate_sphinx_airflow_theme_path,
+    help="Build the docs with the `sphinx_airflow_theme` sub-folder of an 
airflow-site checkout "
+    "(e.g. `../airflow-site/sphinx_airflow_theme`, not the repository root), 
after "
+    "`./site.sh build-landing-pages && ./site.sh prepare-theme`, instead of 
the published theme.",
+)
 @option_verbose
 @option_answer
 @argument_doc_packages
@@ -1053,11 +1073,17 @@ def build_docs(
     distributions_list: str,
     sdk: tuple[str, ...],
     sdk_docs_only: bool,
+    sphinx_airflow_theme_path: str | None,
     doc_packages: tuple[str, ...],
 ):
     """
     Build documents.
     """
+    if sdk_docs_only and sphinx_airflow_theme_path:
+        raise click.UsageError(
+            "--sphinx-airflow-theme-path cannot be used with --sdk-docs-only: "
+            "the SDK docs do not use the Sphinx theme."
+        )
     perform_environment_checks()
     fix_ownership_using_docker()
     cleanup_python_generated_files()
@@ -1080,6 +1106,7 @@ def build_docs(
             package_filter=package_filter,
             distributions_list=distributions_list,
             doc_packages=doc_packages,
+            sphinx_airflow_theme_path=sphinx_airflow_theme_path or "",
         )
     # Every requested SDK is built even if an earlier one failed, so a single 
run reports
     # all the broken toolchains rather than only the first.
diff --git 
a/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py 
b/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
index 97e85a7d312..16ddb36c357 100644
--- a/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
@@ -380,6 +380,7 @@ DEVELOPER_PARAMETERS: dict[str, list[dict[str, str | 
list[str]]]] = {
                 "--github-repository",
                 "--builder",
                 "--distributions-list",
+                "--sphinx-airflow-theme-path",
             ],
         },
     ],
diff --git a/dev/breeze/src/airflow_breeze/params/shell_params.py 
b/dev/breeze/src/airflow_breeze/params/shell_params.py
index a692289494a..6d117084825 100644
--- a/dev/breeze/src/airflow_breeze/params/shell_params.py
+++ b/dev/breeze/src/airflow_breeze/params/shell_params.py
@@ -99,6 +99,7 @@ from airflow_breeze.utils.path_utils import (
     SCRIPTS_CI_DOCKER_COMPOSE_INTEGRATION_KERBEROS_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_ALL_SOURCES_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_YAML_PATH,
+    SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_SPHINX_AIRFLOW_THEME_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UI_DIST_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_MYPY_PATH,
@@ -241,6 +242,7 @@ class ShellParams:
     skip_image_upgrade_check: bool = False
     skip_provider_dependencies_check: bool = False
     skip_ssh_setup: bool = os.environ.get("SKIP_SSH_SETUP", "false") == "true"
+    sphinx_airflow_theme_path: str = ""
     standalone_dag_processor: bool = False
     start_airflow: bool = False
     test_type: str | None = None
@@ -442,6 +444,8 @@ class ShellParams:
             
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_REMOVE_SOURCES_PATH)
         if self.mount_ui_dist:
             
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UI_DIST_PATH)
+        if self.sphinx_airflow_theme_path:
+            
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_SPHINX_AIRFLOW_THEME_PATH)
         if self.forward_credentials:
             
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_FORWARD_CREDENTIALS_PATH)
         if self.include_mypy_volume:
@@ -755,6 +759,7 @@ services:
         _set_var(_env, "SQLITE_URL", self.sqlite_url)
         _set_var(_env, "SSH_PORT", None, SSH_PORT)
         _set_var(_env, "SKIP_ASSETS_COMPILATION", self.skip_assets_compilation)
+        _set_var(_env, "SPHINX_AIRFLOW_THEME_PATH", 
self.sphinx_airflow_theme_path)
         _set_var(_env, "STANDALONE_DAG_PROCESSOR", 
self.standalone_dag_processor)
         _set_var(_env, "START_AIRFLOW", self.start_airflow)
         _set_var(_env, "TERMINAL_MULTIPLEXER", self.terminal_multiplexer)
diff --git a/dev/breeze/src/airflow_breeze/utils/path_utils.py 
b/dev/breeze/src/airflow_breeze/utils/path_utils.py
index 3ae47757459..34b2b571a6a 100644
--- a/dev/breeze/src/airflow_breeze/utils/path_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/path_utils.py
@@ -527,6 +527,9 @@ SCRIPTS_CI_DOCKER_COMPOSE_INTEGRATION_KERBEROS_PATH = (
 )
 SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_ALL_SOURCES_PATH = 
SCRIPTS_CI_DOCKER_COMPOSE_PATH / "local-all-sources.yml"
 SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_YAML_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH / 
"local.yml"
+SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_SPHINX_AIRFLOW_THEME_PATH = (
+    SCRIPTS_CI_DOCKER_COMPOSE_PATH / "mount-sphinx-airflow-theme.yml"
+)
 SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UI_DIST_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH 
/ "mount-ui-dist.yml"
 SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH 
/ "mount-uv-lock.yml"
 SCRIPTS_CI_DOCKER_COMPOSE_MYPY_PATH = SCRIPTS_CI_DOCKER_COMPOSE_PATH / 
"mypy.yml"
diff --git a/dev/breeze/tests/test_developer_commands.py 
b/dev/breeze/tests/test_developer_commands.py
index 4064f16697a..b9c603f6da7 100644
--- a/dev/breeze/tests/test_developer_commands.py
+++ b/dev/breeze/tests/test_developer_commands.py
@@ -17,13 +17,14 @@
 
 from __future__ import annotations
 
-from unittest.mock import patch
+from unittest.mock import create_autospec, patch
 
 import click
 import pytest
 from click import UsageError
 from click.testing import CliRunner
 
+from airflow_breeze.commands import developer_commands
 from airflow_breeze.commands.developer_commands import build_docs, down, run
 from airflow_breeze.global_constants import DEFAULT_PYTHON_MAJOR_MINOR_VERSION
 
@@ -115,6 +116,28 @@ def 
test_down_lists_projects_left_in_other_worktrees(runner, tmp_path, args, lis
         assert "/work/other" not in output
 
 
[email protected]
+def docker_mocks(monkeypatch, tmp_path):
+    monkeypatch.setenv("SKIP_SAVING_CHOICES", "true")
+    for name in ("BREEZE_SDK", "DISTRIBUTIONS_LIST"):
+        monkeypatch.delenv(name, raising=False)
+    # The real _build_python_docs deletes generated/**/apis under 
AIRFLOW_ROOT_PATH.
+    monkeypatch.setattr(developer_commands, "AIRFLOW_ROOT_PATH", tmp_path)
+    mocks = {}
+    for name in (
+        "perform_environment_checks",
+        "fix_ownership_using_docker",
+        "cleanup_python_generated_files",
+        "build_ci_image_if_needed",
+        "execute_command_in_shell",
+    ):
+        mocks[name] = create_autospec(getattr(developer_commands, name))
+        monkeypatch.setattr(developer_commands, name, mocks[name])
+    mocks["execute_command_in_shell"].return_value.returncode = 0
+    return mocks
+
+
[email protected]("docker_mocks")
 class TestBuildDocsPythonVersion:
     """`breeze build-docs` always documents on the default Python.
 
@@ -123,30 +146,16 @@ class TestBuildDocsPythonVersion:
     selected - see the comment in ``_build_python_docs``.
     """
 
-    @pytest.fixture(autouse=True)
-    def _no_docker(self, monkeypatch):
-        monkeypatch.setenv("SKIP_SAVING_CHOICES", "true")
-        for name in (
-            "perform_environment_checks",
-            "fix_ownership_using_docker",
-            "cleanup_python_generated_files",
-        ):
-            
monkeypatch.setattr(f"airflow_breeze.commands.developer_commands.{name}", 
lambda *a, **kw: None)
-
-    def _invoke(self, runner: CliRunner, args: list[str], env: dict[str, str] 
| None = None):
-        with (
-            
patch("airflow_breeze.commands.developer_commands.build_ci_image_if_needed") as 
mock_build,
-            
patch("airflow_breeze.commands.developer_commands.execute_command_in_shell") as 
mock_shell,
-        ):
-            mock_shell.return_value.returncode = 0
-            runner.invoke(build_docs, args, env=env, catch_exceptions=False)
-        return mock_build, mock_shell
-
-    def test_environment_python_does_not_change_the_docs_build(self, runner):
+    def test_environment_python_does_not_change_the_docs_build(self, runner, 
docker_mocks):
         # PYTHON_MAJOR_MINOR_VERSION is set on every job of the docs 
publishing workflow, so an
         # option reading it silently decided what the docs were built with.
-        mock_build, mock_shell = self._invoke(runner, [], 
env={"PYTHON_MAJOR_MINOR_VERSION": "3.12"})
+        result = runner.invoke(
+            build_docs, [], env={"PYTHON_MAJOR_MINOR_VERSION": "3.12"}, 
catch_exceptions=False
+        )
 
+        assert result.exit_code == 0
+        mock_build = docker_mocks["build_ci_image_if_needed"]
+        mock_shell = docker_mocks["execute_command_in_shell"]
         assert mock_build.call_args.kwargs["command_params"].python == 
DEFAULT_PYTHON_MAJOR_MINOR_VERSION
         assert mock_shell.call_args.args[0].python == 
DEFAULT_PYTHON_MAJOR_MINOR_VERSION
 
@@ -157,6 +166,49 @@ class TestBuildDocsPythonVersion:
         assert "no such option" in result.output.lower()
 
 
+class TestBuildDocsSphinxAirflowTheme:
+    @pytest.mark.parametrize("pass_option", [True, False])
+    def test_theme_path_is_passed_to_shell_params(
+        self, runner, docker_mocks, tmp_path, monkeypatch, pass_option
+    ):
+        (tmp_path / "theme" / "sphinx_airflow_theme" / "static" / 
"_gen").mkdir(parents=True)
+        monkeypatch.chdir(tmp_path)
+
+        result = runner.invoke(
+            build_docs,
+            ["--sphinx-airflow-theme-path", "theme"] if pass_option else [],
+            catch_exceptions=False,
+        )
+
+        assert result.exit_code == 0
+        shell_params = 
docker_mocks["execute_command_in_shell"].call_args.args[0]
+        assert shell_params.sphinx_airflow_theme_path == (str(tmp_path / 
"theme") if pass_option else "")
+
+    def test_theme_without_generated_assets_is_rejected(self, runner, 
docker_mocks, tmp_path):
+        theme_checkout = tmp_path / "airflow-site"
+        (theme_checkout / "sphinx_airflow_theme").mkdir(parents=True)
+
+        result = runner.invoke(build_docs, ["--sphinx-airflow-theme-path", 
str(theme_checkout)])
+
+        assert result.exit_code == 2
+        assert "sphinx_airflow_theme/static/_gen" in " 
".join(click.unstyle(result.output).split())
+        for mock in docker_mocks.values():
+            mock.assert_not_called()
+
+    def test_theme_with_sdk_docs_only_is_rejected(self, runner, docker_mocks, 
tmp_path):
+        theme_checkout = tmp_path / "airflow-site"
+        (theme_checkout / "sphinx_airflow_theme" / "static" / 
"_gen").mkdir(parents=True)
+
+        result = runner.invoke(
+            build_docs, ["--sdk-docs-only", "--sphinx-airflow-theme-path", 
str(theme_checkout)]
+        )
+
+        assert result.exit_code == 2
+        assert "--sdk-docs-only:" in click.unstyle(result.output)
+        for mock in docker_mocks.values():
+            mock.assert_not_called()
+
+
 class TestRunIncludeMypyVolume:
     @pytest.fixture(autouse=True)
     def _no_docker(self, monkeypatch):
diff --git a/dev/breeze/tests/test_shell_params.py 
b/dev/breeze/tests/test_shell_params.py
index b6d0b6d77a9..78e3d56d691 100644
--- a/dev/breeze/tests/test_shell_params.py
+++ b/dev/breeze/tests/test_shell_params.py
@@ -34,6 +34,7 @@ from airflow_breeze.params.shell_params import ShellParams
 from airflow_breeze.utils.path_utils import (
     SCRIPTS_CI_DOCKER_COMPOSE_BASE_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_LOCAL_YAML_PATH,
+    SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_SPHINX_AIRFLOW_THEME_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_UV_LOCK_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_PYCACHE_PATH,
@@ -411,3 +412,16 @@ def test_uv_lock_is_mounted_only_by_its_own_compose_file():
     assert uv_lock_compose_file["services"]["airflow"]["volumes"] == [
         {"type": "bind", "source": "../../../uv.lock", "target": 
"/opt/airflow/uv.lock"}
     ]
+
+
[email protected](
+    ("theme_path", "expected_count"), 
[("/tmp/airflow-site/sphinx_airflow_theme", 1), ("", 0)]
+)
+def test_sphinx_airflow_theme_is_mounted_only_when_a_path_is_given(theme_path: 
str, expected_count: int):
+    shell_params = ShellParams(sphinx_airflow_theme_path=theme_path)
+    compose_files = shell_params.compose_file.split(os.pathsep)
+    assert (
+        
compose_files.count(str(SCRIPTS_CI_DOCKER_COMPOSE_MOUNT_SPHINX_AIRFLOW_THEME_PATH))
 == expected_count
+    )
+    # Compose interpolates the bind source from this variable, so it must be 
exported together with the file.
+    assert 
shell_params.env_variables_for_docker_commands["SPHINX_AIRFLOW_THEME_PATH"] == 
theme_path
diff --git a/docs/README.md b/docs/README.md
index 21533eec881..e76925d3fd6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -198,7 +198,9 @@ Such a main build also publishes the latest 
"sphinx-airflow-theme" package to Gi
 of documentation can automatically pick it up from there. This means that if 
you want to make changes to
 `javascript` or `css` that are part of the theme, you need to do it in 
`airflow-site` repository and
 merge it to `main` branch in order to be able to run the documentation build 
in `apache-airflow` repository
-and pick up the latest version of the theme.
+and pick up the latest version of the theme. To preview an unreleased theme 
change, see the
+`--sphinx-airflow-theme-path` option in
+[Building documentation in 
Breeze](../contributing-docs/11_documentation_building.rst#building-documentation-in-breeze).
 
 The version of sphinx theme is fixed in both repositories:
 
diff --git a/scripts/in_container/run_docs_build.sh 
b/scripts/ci/docker-compose/mount-sphinx-airflow-theme.yml
old mode 100755
new mode 100644
similarity index 58%
copy from scripts/in_container/run_docs_build.sh
copy to scripts/ci/docker-compose/mount-sphinx-airflow-theme.yml
index bacfb75efe9..626a07d801f
--- a/scripts/in_container/run_docs_build.sh
+++ b/scripts/ci/docker-compose/mount-sphinx-airflow-theme.yml
@@ -1,4 +1,3 @@
-#!/usr/bin/env bash
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
@@ -15,16 +14,13 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-# shellcheck source=scripts/in_container/_in_container_script_init.sh
-. "$( dirname "${BASH_SOURCE[0]}" )/_in_container_script_init.sh"
-
-git config --global --add safe.directory /opt/airflow
-
-cd "${AIRFLOW_SOURCES}" || exit 1
-python -m docs.build_docs "${@}"
-
-# Copy the generated documentation to the mounted volume
-if [[ ( ${CI:="false"} == "true" || ${CI} == "True" ) && -d 
"${AIRFLOW_SOURCES}/generated/_build/docs/" ]]; then
-    rm -rf "/files/documentation"
-    cp -r "${AIRFLOW_SOURCES}/generated/_build" "/files/documentation"
-fi
+---
+services:
+  airflow:
+    # Added by `breeze build-docs --sphinx-airflow-theme-path`. The mount is 
read-only so the
+    # in-container install cannot leave root-owned build output in the 
airflow-site checkout.
+    volumes:
+      - type: bind
+        source: ${SPHINX_AIRFLOW_THEME_PATH}
+        target: /opt/sphinx_airflow_theme
+        read_only: true
diff --git a/scripts/in_container/run_docs_build.sh 
b/scripts/in_container/run_docs_build.sh
index bacfb75efe9..1cdfbca8b67 100755
--- a/scripts/in_container/run_docs_build.sh
+++ b/scripts/in_container/run_docs_build.sh
@@ -21,6 +21,12 @@
 git config --global --add safe.directory /opt/airflow
 
 cd "${AIRFLOW_SOURCES}" || exit 1
+
+# Mounted by `breeze build-docs --sphinx-airflow-theme-path`.
+if [[ -n "${SPHINX_AIRFLOW_THEME_PATH:-}" ]]; then
+    uv pip install --reinstall-package sphinx-airflow-theme 
/opt/sphinx_airflow_theme
+fi
+
 python -m docs.build_docs "${@}"
 
 # Copy the generated documentation to the mounted volume

Reply via email to