This is an automated email from the ASF dual-hosted git repository.
potiuk 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 285978e2d7b Breeze: shim fallback outside worktrees + publish-docs
workflow ref (#68192)
285978e2d7b is described below
commit 285978e2d7b10dfa1605af7c201d0b23aaf8b034
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon Jun 8 10:27:21 2026 +0200
Breeze: shim fallback outside worktrees + publish-docs workflow ref (#68192)
* Breeze: shim fallback outside worktrees + publish-docs workflow ref
- shim falls back to its install-dir dev/breeze when run outside a worktree
(e.g. the asf-dist SVN tree during a provider release)
- publish-docs --workflow-branch now defaults to --ref so the dispatched
workflow version matches the tag being built
* fixup! Breeze: shim fallback outside worktrees + publish-docs workflow ref
---
dev/breeze/README.md | 41 +++-
...017-use-uvx-to-run-breeze-from-local-sources.md | 56 ++++--
.../images/output_workflow-run_publish-docs.svg | 58 +++---
.../images/output_workflow-run_publish-docs.txt | 2 +-
.../airflow_breeze/commands/workflow_commands.py | 14 +-
dev/breeze/src/airflow_breeze/utils/path_utils.py | 152 ++++++++++++++-
dev/breeze/tests/test_shim_version_check.py | 212 +++++++++++++++++++++
scripts/tools/setup_breeze | 45 ++++-
8 files changed, 521 insertions(+), 59 deletions(-)
diff --git a/dev/breeze/README.md b/dev/breeze/README.md
index d0e7244a3ad..f7cc35c1d59 100644
--- a/dev/breeze/README.md
+++ b/dev/breeze/README.md
@@ -46,6 +46,15 @@ worktree's sources. Because the shim is a real file on
`PATH`, subprocesses (pre
hooks, CI scripts, dev tools) see it just like a `uv tool`-installed binary.
See
[ADR 0017](doc/adr/0017-use-uvx-to-run-breeze-from-local-sources.md) for the
rationale.
+When invoked from outside any Airflow worktree — for example from an SVN
release checkout
+(`asf-dist`) during a provider release — the shim falls back to, in order: the
worktree
+pointed at by `$AIRFLOW_REPO_ROOT` (which the release docs export to the repo
root, so breeze
+resolves the same way across every release process), then the `dev/breeze` of
the worktree it
+was installed from (baked in at install time). This keeps release commands
such as
+`breeze release-management clean-old-provider-artifacts --directory
<asf-dist>` working
+from the SVN tree. The fallbacks never override a real worktree, so
per-worktree isolation is
+preserved wherever it matters.
+
The `scripts/tools/setup_breeze` script installs the shim for you. If you
previously
installed breeze globally via `uv tool install -e ./dev/breeze` or `pipx
install -e ./dev/breeze`,
remove that install first — both write to `~/.local/bin/breeze` and would
conflict:
@@ -60,25 +69,37 @@ To install the shim manually, write this file to
`~/.local/bin/breeze` and `chmo
#!/usr/bin/env bash
# Apache Airflow breeze shim — managed by scripts/tools/setup_breeze (ADR
0017).
set -e
-repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
- echo "breeze: not inside a git repository — cd into an Airflow worktree
first" >&2
- exit 1
-}
-if [ ! -d "${repo_root}/dev/breeze" ]; then
- echo "breeze: ${repo_root} is not an Airflow worktree (no dev/breeze)" >&2
+# Install-time fallback: the Airflow sources 'scripts/tools/setup_breeze' was
run
+# from. Used only when the current directory is not an Airflow worktree.
+fallback_root="/abs/path/to/airflow" # baked in by setup_breeze (= the
worktree it ran from)
+repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || repo_root=""
+if [ -n "${repo_root}" ] && [ -d "${repo_root}/dev/breeze" ]; then
+ breeze_root="${repo_root}"
+elif [ -n "${AIRFLOW_REPO_ROOT:-}" ] && [ -d "${AIRFLOW_REPO_ROOT}/dev/breeze"
]; then
+ breeze_root="${AIRFLOW_REPO_ROOT}"
+elif [ -d "${fallback_root}/dev/breeze" ]; then
+ breeze_root="${fallback_root}"
+else
+ echo "breeze: not inside an Airflow worktree, AIRFLOW_REPO_ROOT is unset
or not an Airflow worktree, and the install-time fallback
'${fallback_root}/dev/breeze' is missing — re-run scripts/tools/setup_breeze"
>&2
exit 1
fi
-exec env AIRFLOW_ROOT_PATH="${repo_root}" SKIP_BREEZE_SELF_UPGRADE_CHECK=1 \
- uvx --from "${repo_root}/dev/breeze" --quiet breeze "$@"
+exec env AIRFLOW_ROOT_PATH="${breeze_root}" SKIP_BREEZE_SELF_UPGRADE_CHECK=1 \
+ uvx --from "${breeze_root}/dev/breeze" --quiet breeze "$@"
```
-Then `breeze` invoked from any Airflow checkout uses that checkout's source.
The first call in
-a fresh worktree pays a one-time `uvx` resolve/install; subsequent calls hit
the cache.
+Then `breeze` invoked from any Airflow checkout uses that checkout's source,
and from
+anywhere else it uses `$AIRFLOW_REPO_ROOT` or the baked-in fallback. The first
call in a
+fresh worktree pays a one-time `uvx` resolve/install; subsequent calls hit the
cache.
The legacy global-install path (`uv tool install -e ./dev/breeze --force` or
`pipx install -e ./dev/breeze --force`) still works for users who explicitly
want a single
shared install, but it is no longer the recommended approach.
+The shim carries a `# breeze-shim-version: N` marker. On startup breeze
compares it with the
+version the current sources would install and, if your installed shim is older
(or you are still
+on a legacy global install), prints a warning telling you to re-run
`scripts/tools/setup_breeze`
+(after uninstalling the global install, if any).
+
You can read more about Breeze in the
[documentation](https://github.com/apache/airflow/blob/main/dev/breeze/doc/README.rst)
This README file contains automatically generated hash of the `pyproject.toml`
files that were
diff --git
a/dev/breeze/doc/adr/0017-use-uvx-to-run-breeze-from-local-sources.md
b/dev/breeze/doc/adr/0017-use-uvx-to-run-breeze-from-local-sources.md
index de24c41da05..017fd6d1d18 100644
--- a/dev/breeze/doc/adr/0017-use-uvx-to-run-breeze-from-local-sources.md
+++ b/dev/breeze/doc/adr/0017-use-uvx-to-run-breeze-from-local-sources.md
@@ -90,17 +90,32 @@ The recommended way to run breeze is via a small **shim
script** at
# Runs breeze from the dev/breeze folder of the current git worktree via 'uvx',
# so each worktree (e.g. parallel agentic runs) gets its own
ephemerally-installed
# breeze tied to that worktree's source.
+#
+# Resolution order for the Airflow sources breeze runs from:
+# 1. the current git worktree (per-worktree isolation — see above);
+# 2. $AIRFLOW_REPO_ROOT, if exported and pointing at an Airflow worktree —
the
+# release docs export this, so breeze resolves the same way across every
+# release process regardless of where the shim was installed from;
+# 3. the install-time fallback baked in below (the worktree setup_breeze ran
from).
+# Steps 2 and 3 apply only when the current directory is not an Airflow
worktree,
+# so the fallbacks never override a real worktree and isolation is preserved.
set -e
-repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
- echo "breeze: not inside a git repository — cd into an Airflow worktree
first" >&2
- exit 1
-}
-if [ ! -d "${repo_root}/dev/breeze" ]; then
- echo "breeze: ${repo_root} is not an Airflow worktree (no dev/breeze)" >&2
+# Install-time fallback: the Airflow sources 'scripts/tools/setup_breeze' was
run
+# from. Used only when the current directory is not an Airflow worktree.
+fallback_root="/abs/path/to/airflow" # baked in by setup_breeze (=
AIRFLOW_SOURCES)
+repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || repo_root=""
+if [ -n "${repo_root}" ] && [ -d "${repo_root}/dev/breeze" ]; then
+ breeze_root="${repo_root}"
+elif [ -n "${AIRFLOW_REPO_ROOT:-}" ] && [ -d "${AIRFLOW_REPO_ROOT}/dev/breeze"
]; then
+ breeze_root="${AIRFLOW_REPO_ROOT}"
+elif [ -d "${fallback_root}/dev/breeze" ]; then
+ breeze_root="${fallback_root}"
+else
+ echo "breeze: not inside an Airflow worktree, AIRFLOW_REPO_ROOT is unset
or not an Airflow worktree, and the install-time fallback
'${fallback_root}/dev/breeze' is missing — re-run scripts/tools/setup_breeze"
>&2
exit 1
fi
-exec env AIRFLOW_ROOT_PATH="${repo_root}" SKIP_BREEZE_SELF_UPGRADE_CHECK=1 \
- uvx --from "${repo_root}/dev/breeze" --quiet breeze "$@"
+exec env AIRFLOW_ROOT_PATH="${breeze_root}" SKIP_BREEZE_SELF_UPGRADE_CHECK=1 \
+ uvx --from "${breeze_root}/dev/breeze" --quiet breeze "$@"
```
``scripts/tools/setup_breeze`` writes this file (replacing any previous
@@ -150,6 +165,13 @@ behaviour, but they are no longer the recommended path.
* **Subprocess-safe.** The shim is a real binary on ``PATH``, so anything that
shells out to ``breeze`` — pre-commit hooks, CI helpers, dev scripts —
resolves it exactly like a ``uv tool`` install did.
+* **Self-detecting staleness.** The shim carries a ``# breeze-shim-version: N``
+ marker that ``setup_breeze`` bumps whenever the shim body changes. On startup
+ breeze compares the installed shim's version against the version the current
+ sources would install and warns the user to re-run ``setup_breeze`` if the
+ installed shim is older (or predates versioning). The same startup check also
+ detects a leftover legacy global ``uv tool`` / ``pipx`` install and nudges
the
+ user to migrate to the shim.
**Costs**
@@ -160,11 +182,19 @@ behaviour, but they are no longer the recommended path.
runs ``git rev-parse`` and ``uvx`` for every invocation. Negligible at the
command line, but noticeable inside tight loops or shell completion that
re-invokes ``breeze`` many times.
-* **Requires a git checkout.** ``breeze`` invoked outside a git tree errors
- out with a clear message rather than running. This matches actual usage —
- breeze is meaningless outside an Airflow source tree — but is a behavioural
- change from the global install, which would silently run against whatever
- tree it was last installed from.
+* **Resolution is current-worktree-first, with two fallbacks.** ``breeze``
+ invoked from inside an Airflow worktree runs that worktree's breeze. Invoked
+ from anywhere else (a non-Airflow git tree, or no git tree at all — e.g. an
+ ``asf-dist`` SVN release checkout), it falls back to, in order: the worktree
+ pointed at by ``$AIRFLOW_REPO_ROOT`` (which the release docs export to the
+ repo root, so breeze resolves the same way across every release process),
then
+ the ``dev/breeze`` of the worktree ``setup_breeze`` was last run from, baked
+ into the shim at install time. This keeps release commands such as
+ ``breeze release-management clean-old-provider-artifacts --directory
<asf-dist>``
+ working from the SVN tree. Only if the current worktree,
``$AIRFLOW_REPO_ROOT``,
+ and the baked-in fallback are all missing ``dev/breeze`` does the shim error
+ out with a clear message. The fallbacks never override a real worktree, so
+ per-worktree isolation is preserved wherever it matters.
* **One-time migration.** Users who previously installed breeze with
``uv tool install`` need to ``uv tool uninstall apache-airflow-breeze``
before installing the shim, otherwise both write to ``~/.local/bin/breeze``
diff --git a/dev/breeze/doc/images/output_workflow-run_publish-docs.svg
b/dev/breeze/doc/images/output_workflow-run_publish-docs.svg
index 6a8e55584df..48033b138d1 100644
--- a/dev/breeze/doc/images/output_workflow-run_publish-docs.svg
+++ b/dev/breeze/doc/images/output_workflow-run_publish-docs.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1148.0"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 1221.1999999999998"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -45,7 +45,7 @@
<defs>
<clipPath id="breeze-workflow-run-publish-docs-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="1097.0" />
+ <rect x="0" y="0" width="1463.0" height="1170.1999999999998" />
</clipPath>
<clipPath id="breeze-workflow-run-publish-docs-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -179,9 +179,18 @@
<clipPath id="breeze-workflow-run-publish-docs-line-43">
<rect x="0" y="1050.7" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-workflow-run-publish-docs-line-44">
+ <rect x="0" y="1075.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-workflow-run-publish-docs-line-45">
+ <rect x="0" y="1099.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-workflow-run-publish-docs-line-46">
+ <rect x="0" y="1123.9" 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="1146" rx="8"/><text
class="breeze-workflow-run-publish-docs-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: workflow-run publish-docs</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="1219.2" rx="8"/><text
class="breeze-workflow-run-publish-docs-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: workflow-run publish-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"/>
@@ -214,28 +223,31 @@
</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="508"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-20)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="61" y="508" textLength="256.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-20)">--skip-tag-validation</text><text
class="breeze-workflow-run-publish-docs-r1" x="341.6" y="508"
textLength="1000.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-20)">Skip validati
[...]
</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="532.4"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-21)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="61" y="532.4" textLength="256.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-21)">--apply-commits      </text><text
class="breeze-workflow-run-publish-docs-r1" x="341.6" y="532.4"
textLength="1098" clip-path="url(#breeze-workflow-run-publish [...]
</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="556.8"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-22)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="341.6" y="556.8"
textLength="341.6"
clip-path="url(#breeze-workflow-run-publish-docs-line-22)">separated list of commits). </text><text
class="breeze-workflow-run-publish-docs-r8" x="683.2" y="556.8"
textLength="73.2" clip-path="url(#breeze-workflow-run-publish [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="581.2"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-23)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="61" y="581.2" textLength="256.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-23)">--workflow-branch    </text><text
class="breeze-workflow-run-publish-docs-r1" x="341.6" y="581.2"
textLength="622.2" clip-path="url(#breeze-workflow-run-publish-docs-lin [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="605.6"
textLength="1464"
clip-path="url(#breeze-workflow-run-publish-docs-line-24)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-workflow-run-publish-docs-r1" x="1464" y="605.6"
textLength="12.2" clip-path="url(#breeze-workflow-run-publish-docs-line-24)">
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="630"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-25)">╭─</text><text
class="breeze-workflow-run-publish-docs-r5" x="24.4" y="630" textLength="451.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-25)"> Optional airflow versions to build. </text><text
class="breeze-workflow-run-publish-docs-r5" x="475.8" y="630"
textLength="963.8" clip-path="url(#breeze-workfl [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="654.4"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-26)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="654.4"
textLength="268.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-26)">--airflow-version     </text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="654.4"
textLength="1122.4" clip-path="url(#breeze-workflow-run-publish [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="678.8"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-27)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="678.8"
textLength="1122.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-27)">from the ref. If only base version is provided, it will be set to the same as the base 
[...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="703.2"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-28)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="703.2"
textLength="109.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-28)">version. </text><text
class="breeze-workflow-run-publish-docs-r8" x="427" y="703.2"
textLength="73.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-28)">(TEXT)</text><text c
[...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="727.6"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-29)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="727.6"
textLength="268.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-29)">--airflow-base-version</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="727.6"
textLength="1122.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-29)">Override& [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="752"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-30)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="752"
textLength="1122.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-30)">extracted from the ref. If airflow-version is provided, the base version of the version     </text>
[...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="776.4"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-31)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="776.4"
textLength="622.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-31)">provided (i.e. stripped pre-/post-/dev- suffixes). </text><text
class="breeze-workflow-run-publish-docs-r8" x="939.4" y="776.4"
textLength="73.2" clip-path="url( [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="800.8"
textLength="1464"
clip-path="url(#breeze-workflow-run-publish-docs-line-32)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-workflow-run-publish-docs-r1" x="1464" y="800.8"
textLength="12.2" clip-path="url(#breeze-workflow-run-publish-docs-line-32)">
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="825.2"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-33)">╭─</text><text
class="breeze-workflow-run-publish-docs-r5" x="24.4" y="825.2" textLength="488"
clip-path="url(#breeze-workflow-run-publish-docs-line-33)"> Select docs to exclude and destination </text><text
class="breeze-workflow-run-publish-docs-r5" x="512.4" y="825.2"
textLength="927.2" clip-path="url(#b [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="849.6"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-34)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="849.6"
textLength="353.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-34)">--exclude-docs               </text><text
class="breeze-workflow-run-publish-docs-r1" x="402.6" y="849.6" textLe [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="874"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-35)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="402.6" y="874"
textLength="378.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-35)">(example: apache.druid,google) </text><text
class="breeze-workflow-run-publish-docs-r8" x="780.8" y="874"
textLength="73.2" clip-path="url(#breeze-workflow-run-publish-docs-line-35 [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="898.4"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-36)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="898.4"
textLength="353.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-36)">--site-env                   </text><text
class="breeze-workflow-run-publish-docs-r1" x="402 [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="922.8"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-37)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="922.8"
textLength="353.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-37)">--skip-write-to-stable-folder</text><text
class="breeze-workflow-run-publish-docs-r1" x="402.6" y="922.8"
textLength="366"
clip-path="url(#breeze-workflow-run-publish-docs-line-37)">Skip& [...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="947.2"
textLength="1464"
clip-path="url(#breeze-workflow-run-publish-docs-line-38)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-workflow-run-publish-docs-r1" x="1464" y="947.2"
textLength="12.2" clip-path="url(#breeze-workflow-run-publish-docs-line-38)">
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="971.6"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-39)">╭─</text><text
class="breeze-workflow-run-publish-docs-r5" x="24.4" y="971.6" textLength="244"
clip-path="url(#breeze-workflow-run-publish-docs-line-39)"> Inventory handling </text><text
class="breeze-workflow-run-publish-docs-r5" x="268.4" y="971.6"
textLength="1171.2" clip-path="url(#breeze-workflow-run-publish-docs-line-39
[...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="996"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-40)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="996" textLength="341.6"
clip-path="url(#breeze-workflow-run-publish-docs-line-40)">--ignore-missing-inventories</text><text
class="breeze-workflow-run-publish-docs-r1" x="390.4" y="996"
textLength="695.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-40)">Do no [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="581.2"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-23)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="61" y="581.2" textLength="256.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-23)">--workflow-branch    </text><text
class="breeze-workflow-run-publish-docs-r1" x="341.6" y="581.2"
textLength="829.6" clip-path="url(#breeze-workflow-run-publish-docs-lin [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="605.6"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-24)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="341.6" y="605.6"
textLength="1098"
clip-path="url(#breeze-workflow-run-publish-docs-line-24)">version matches the content being built (running main's workflow against an older tag     </text>
[...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="630"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-25)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="341.6" y="630" textLength="1098"
clip-path="url(#breeze-workflow-run-publish-docs-line-25)">breaks when inputs/jobs have since changed). Pass an explicit ref to override (e.g.       </text
[...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="654.4"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-26)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="341.6" y="654.4"
textLength="109.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-26)">'main'). </text><text
class="breeze-workflow-run-publish-docs-r8" x="451.4" y="654.4"
textLength="73.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-26)">(TEXT)</ [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="678.8"
textLength="1464"
clip-path="url(#breeze-workflow-run-publish-docs-line-27)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-workflow-run-publish-docs-r1" x="1464" y="678.8"
textLength="12.2" clip-path="url(#breeze-workflow-run-publish-docs-line-27)">
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="703.2"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-28)">╭─</text><text
class="breeze-workflow-run-publish-docs-r5" x="24.4" y="703.2"
textLength="451.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-28)"> Optional airflow versions to build. </text><text
class="breeze-workflow-run-publish-docs-r5" x="475.8" y="703.2"
textLength="963.8" clip-path="url(#breeze- [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="727.6"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-29)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="727.6"
textLength="268.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-29)">--airflow-version     </text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="727.6"
textLength="1122.4" clip-path="url(#breeze-workflow-run-publish [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="752"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-30)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="752"
textLength="1122.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-30)">from the ref. If only base version is provided, it will be set to the same as the base  &
[...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="776.4"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-31)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="776.4"
textLength="109.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-31)">version. </text><text
class="breeze-workflow-run-publish-docs-r8" x="427" y="776.4"
textLength="73.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-31)">(TEXT)</text><text c
[...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="800.8"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-32)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="800.8"
textLength="268.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-32)">--airflow-base-version</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="800.8"
textLength="1122.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-32)">Override& [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="825.2"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-33)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="825.2"
textLength="1122.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-33)">extracted from the ref. If airflow-version is provided, the base version of the version     </t
[...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="849.6"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-34)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="317.2" y="849.6"
textLength="622.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-34)">provided (i.e. stripped pre-/post-/dev- suffixes). </text><text
class="breeze-workflow-run-publish-docs-r8" x="939.4" y="849.6"
textLength="73.2" clip-path="url( [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="874"
textLength="1464"
clip-path="url(#breeze-workflow-run-publish-docs-line-35)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-workflow-run-publish-docs-r1" x="1464" y="874" textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-35)">
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="898.4"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-36)">╭─</text><text
class="breeze-workflow-run-publish-docs-r5" x="24.4" y="898.4" textLength="488"
clip-path="url(#breeze-workflow-run-publish-docs-line-36)"> Select docs to exclude and destination </text><text
class="breeze-workflow-run-publish-docs-r5" x="512.4" y="898.4"
textLength="927.2" clip-path="url(#b [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="922.8"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-37)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="922.8"
textLength="353.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-37)">--exclude-docs               </text><text
class="breeze-workflow-run-publish-docs-r1" x="402.6" y="922.8" textLe [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="947.2"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-38)">│</text><text
class="breeze-workflow-run-publish-docs-r1" x="402.6" y="947.2"
textLength="378.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-38)">(example: apache.druid,google) </text><text
class="breeze-workflow-run-publish-docs-r8" x="780.8" y="947.2"
textLength="73.2" clip-path="url(#breeze-workflow-run-publish-docs-l [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="971.6"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-39)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="971.6"
textLength="353.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-39)">--site-env                   </text><text
class="breeze-workflow-run-publish-docs-r1" x="402 [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="996"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-40)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="996" textLength="353.8"
clip-path="url(#breeze-workflow-run-publish-docs-line-40)">--skip-write-to-stable-folder</text><text
class="breeze-workflow-run-publish-docs-r1" x="402.6" y="996" textLength="366"
clip-path="url(#breeze-workflow-run-publish-docs-line-40)">Skip w [...]
</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1020.4"
textLength="1464"
clip-path="url(#breeze-workflow-run-publish-docs-line-41)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-workflow-run-publish-docs-r1" x="1464" y="1020.4"
textLength="12.2" clip-path="url(#breeze-workflow-run-publish-docs-line-41)">
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1044.8"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-42)">╭─</text><text
class="breeze-workflow-run-publish-docs-r5" x="24.4" y="1044.8"
textLength="195.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-42)"> Common options </text><text
class="breeze-workflow-run-publish-docs-r5" x="219.6" y="1044.8"
textLength="1220" clip-path="url(#breeze-workflow-run-publish-docs-line-42)
[...]
-</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1069.2"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-43)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="1069.2"
textLength="73.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-43)">--help</text><text
class="breeze-workflow-run-publish-docs-r9" x="122" y="1069.2"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-43)">-h</text><text
class="breez [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1044.8"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-42)">╭─</text><text
class="breeze-workflow-run-publish-docs-r5" x="24.4" y="1044.8"
textLength="244"
clip-path="url(#breeze-workflow-run-publish-docs-line-42)"> Inventory handling </text><text
class="breeze-workflow-run-publish-docs-r5" x="268.4" y="1044.8"
textLength="1171.2" clip-path="url(#breeze-workflow-run-publish-docs-line [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1069.2"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-43)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="1069.2"
textLength="341.6"
clip-path="url(#breeze-workflow-run-publish-docs-line-43)">--ignore-missing-inventories</text><text
class="breeze-workflow-run-publish-docs-r1" x="390.4" y="1069.2"
textLength="695.4" clip-path="url(#breeze-workflow-run-publish-docs-line-43)">D
[...]
</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1093.6"
textLength="1464"
clip-path="url(#breeze-workflow-run-publish-docs-line-44)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-workflow-run-publish-docs-r1" x="1464" y="1093.6"
textLength="12.2" clip-path="url(#breeze-workflow-run-publish-docs-line-44)">
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1118"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-45)">╭─</text><text
class="breeze-workflow-run-publish-docs-r5" x="24.4" y="1118"
textLength="195.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-45)"> Common options </text><text
class="breeze-workflow-run-publish-docs-r5" x="219.6" y="1118"
textLength="1220"
clip-path="url(#breeze-workflow-run-publish-docs-line-45)">──── [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1142.4"
textLength="12.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-46)">│</text><text
class="breeze-workflow-run-publish-docs-r4" x="24.4" y="1142.4"
textLength="73.2"
clip-path="url(#breeze-workflow-run-publish-docs-line-46)">--help</text><text
class="breeze-workflow-run-publish-docs-r9" x="122" y="1142.4"
textLength="24.4"
clip-path="url(#breeze-workflow-run-publish-docs-line-46)">-h</text><text
class="breez [...]
+</text><text class="breeze-workflow-run-publish-docs-r5" x="0" y="1166.8"
textLength="1464"
clip-path="url(#breeze-workflow-run-publish-docs-line-47)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-workflow-run-publish-docs-r1" x="1464" y="1166.8"
textLength="12.2" clip-path="url(#breeze-workflow-run-publish-docs-line-47)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_workflow-run_publish-docs.txt
b/dev/breeze/doc/images/output_workflow-run_publish-docs.txt
index 1b59a845f02..bcdbd541efa 100644
--- a/dev/breeze/doc/images/output_workflow-run_publish-docs.txt
+++ b/dev/breeze/doc/images/output_workflow-run_publish-docs.txt
@@ -1 +1 @@
-8c78dbba18e40bb21c4e40543d4c3108
+01f2b301d8965951b49e2c8cd114700a
diff --git a/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
b/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
index 1620a09c90b..2330edfcba6 100644
--- a/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
@@ -98,8 +98,11 @@ def workflow_run_group():
)
@click.option(
"--workflow-branch",
- help="Branch to run the workflow on. Defaults to 'main'.",
- default="main",
+ help="Git ref the workflow DEFINITION runs from. Defaults to the value of
--ref, so the "
+ "workflow version matches the content being built (running main's workflow
against an "
+ "older tag breaks when inputs/jobs have since changed). Pass an explicit
ref to override "
+ "(e.g. 'main').",
+ default=None,
type=str,
)
@click.option(
@@ -118,9 +121,14 @@ def workflow_run_publish(
airflow_version: str | None = None,
airflow_base_version: str | None = None,
apply_commits: str | None = None,
- workflow_branch: str = "main",
+ workflow_branch: str | None = None,
ignore_missing_inventories: bool = False,
):
+ # Default the workflow-definition ref to the ref being built, so the
workflow version
+ # matches the content. Running main's workflow against an older tag breaks
when the
+ # workflow's inputs/jobs have changed since that tag (e.g. a newly
required input).
+ if workflow_branch is None:
+ workflow_branch = ref
if len(doc_packages) == 0:
console_print(
"[red]Error: No doc packages provided. Please provide at least one
doc package.[/red]",
diff --git a/dev/breeze/src/airflow_breeze/utils/path_utils.py
b/dev/breeze/src/airflow_breeze/utils/path_utils.py
index b703575fc02..533e741c61e 100644
--- a/dev/breeze/src/airflow_breeze/utils/path_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/path_utils.py
@@ -37,6 +37,14 @@ from airflow_breeze.utils.shared_options import get_verbose,
set_forced_answer
PYPROJECT_TOML_FILE = "pyproject.toml"
+# Location and markers of the breeze shim installed by
scripts/tools/setup_breeze
+# (ADR 0017). Used to detect a stale installed shim and tell the user to re-run
+# the setup script.
+BREEZE_SHIM_PATH = Path.home() / ".local" / "bin" / "breeze"
+BREEZE_SHIM_MARKER = "Apache Airflow breeze shim — managed by
scripts/tools/setup_breeze"
+BREEZE_SHIM_VERSION_PREFIX = "# breeze-shim-version:"
+SETUP_BREEZE_SHIM_VERSION_PREFIX = "SHIM_VERSION="
+
def search_upwards_for_airflow_root_path(start_from: Path) -> Path | None:
root = Path(start_from.root)
@@ -183,6 +191,139 @@ def get_used_airflow_sources() -> Path:
return current_sources
+def _parse_shim_version(shim_text: str) -> int | None:
+ """Read the ``# breeze-shim-version: N`` marker from an installed shim, if
present."""
+ for line in shim_text.splitlines():
+ stripped = line.strip()
+ if stripped.startswith(BREEZE_SHIM_VERSION_PREFIX):
+ value = stripped[len(BREEZE_SHIM_VERSION_PREFIX) :].strip()
+ try:
+ return int(value)
+ except ValueError:
+ return None
+ return None
+
+
+def get_expected_shim_version(airflow_sources: Path) -> int | None:
+ """Read ``SHIM_VERSION`` that the current sources' ``setup_breeze`` would
install."""
+ setup_script = airflow_sources / "scripts" / "tools" / "setup_breeze"
+ try:
+ for line in setup_script.read_text().splitlines():
+ stripped = line.strip()
+ if stripped.startswith(SETUP_BREEZE_SHIM_VERSION_PREFIX):
+ value = stripped[len(SETUP_BREEZE_SHIM_VERSION_PREFIX)
:].strip().strip("\"'")
+ try:
+ return int(value)
+ except ValueError:
+ return None
+ except OSError:
+ return None
+ return None
+
+
+def warn_if_shim_outdated(airflow_sources: Path, shim_text: str | None = None)
-> bool:
+ """
+ Warn if the installed breeze shim is older than the one the current
sources would install.
+
+ The shim at ``~/.local/bin/breeze`` (see ADR 0017) carries a ``#
breeze-shim-version: N``
+ marker. When the shim body changes, ``scripts/tools/setup_breeze`` bumps
that version, so an
+ installed shim with a lower (or missing) version means the user is running
a stale shim and
+ should re-run the setup script. Only our own managed shim is inspected — a
``uv tool`` / ``pipx``
+ install or a user's own script at that path is left alone.
+
+ :param airflow_sources: Airflow sources breeze is operating on.
+ :param shim_text: contents of the installed shim, if already read by the
caller.
+ :return: True if a warning was printed.
+ """
+ if shim_text is None:
+ try:
+ shim_text = BREEZE_SHIM_PATH.read_text() if
BREEZE_SHIM_PATH.is_file() else ""
+ except OSError:
+ return False
+ if BREEZE_SHIM_MARKER not in shim_text:
+ # Not our managed shim (uv tool / pipx install, or the user's own
script).
+ return False
+ expected_version = get_expected_shim_version(airflow_sources)
+ if expected_version is None:
+ return False
+ installed_version = _parse_shim_version(shim_text)
+ if installed_version is not None and installed_version >= expected_version:
+ return False
+ setup_script = airflow_sources / "scripts" / "tools" / "setup_breeze"
+ installed_text = installed_version if installed_version is not None else
"unknown (pre-versioning)"
+ console_print(
+ f"\n[warning]Your breeze shim at {BREEZE_SHIM_PATH} is out of date "
+ f"(installed: {installed_text}, current: {expected_version}).[/]\n"
+ "[warning]Re-run the setup script to refresh it:[/]\n\n"
+ f" {setup_script}\n"
+ )
+ return True
+
+
+def detect_legacy_global_breeze_install() -> str | None:
+ """
+ Detect a legacy global breeze install superseded by the shim (ADR 0017).
+
+ :return: ``"uv"`` or ``"pipx"`` if breeze is installed as a global ``uv
tool`` / ``pipx``
+ install, otherwise ``None``.
+ """
+ try:
+ result = subprocess.run(["uv", "tool", "list"], text=True,
capture_output=True, check=False)
+ if result.returncode == 0 and "apache-airflow-breeze" in result.stdout:
+ return "uv"
+ except FileNotFoundError:
+ pass
+ try:
+ result = subprocess.run(["pipx", "list", "--short"], text=True,
capture_output=True, check=False)
+ if result.returncode == 0 and "apache-airflow-breeze" in result.stdout:
+ return "pipx"
+ except FileNotFoundError:
+ pass
+ return None
+
+
+def warn_if_breeze_launcher_outdated(airflow_sources: Path) -> bool:
+ """
+ Warn when the breeze launcher in use is not the current recommended shim
(ADR 0017).
+
+ Covers both ways an installation can be out of date:
+
+ * the managed shim at ``~/.local/bin/breeze`` exists but is older than the
one the current
+ sources would install (e.g. ``$AIRFLOW_REPO_ROOT`` support was added) —
re-run the setup
+ script to refresh it;
+ * breeze is still installed as a legacy global ``uv tool`` / ``pipx``
install (no shim) — that
+ install keeps working but is no longer recommended; point the user at
migrating to the shim.
+
+ :param airflow_sources: Airflow sources breeze is operating on.
+ :return: True if a warning was printed.
+ """
+ try:
+ shim_text = BREEZE_SHIM_PATH.read_text() if BREEZE_SHIM_PATH.is_file()
else ""
+ except OSError:
+ shim_text = ""
+ if BREEZE_SHIM_MARKER in shim_text:
+ # Our managed shim is installed — only check that it is current.
+ return warn_if_shim_outdated(airflow_sources, shim_text=shim_text)
+ legacy = detect_legacy_global_breeze_install()
+ if legacy is None:
+ return False
+ uninstall_cmd = (
+ "uv tool uninstall apache-airflow-breeze"
+ if legacy == "uv"
+ else "pipx uninstall apache-airflow-breeze"
+ )
+ setup_script = airflow_sources / "scripts" / "tools" / "setup_breeze"
+ console_print(
+ f"\n[warning]Breeze is installed as a legacy global '{legacy}'
install, which still works "
+ "but is no longer the recommended setup (see ADR 0017).[/]\n"
+ "[warning]Migrate to the per-worktree uvx shim by uninstalling the
global install and "
+ "running the setup script:[/]\n\n"
+ f" {uninstall_cmd}\n"
+ f" {setup_script}\n"
+ )
+ return True
+
+
@clearable_cache
def find_airflow_root_path_to_operate_on() -> Path:
"""
@@ -209,7 +350,13 @@ def find_airflow_root_path_to_operate_on() -> Path:
"""
sources_root_from_env = os.getenv("AIRFLOW_ROOT_PATH", None)
if sources_root_from_env:
- return Path(sources_root_from_env)
+ # Set by the shim (ADR 0017) — the global self-upgrade checks below
are skipped in this
+ # path (the shim also exports SKIP_BREEZE_SELF_UPGRADE_CHECK), so
check here that the
+ # installed shim itself is not stale. Ignore SKIP here on purpose: the
shim always sets it.
+ airflow_sources_from_env = Path(sources_root_from_env)
+ if not in_autocomplete() and not in_help() and not hasattr(sys,
"_called_from_test"):
+ warn_if_breeze_launcher_outdated(airflow_sources_from_env)
+ return airflow_sources_from_env
installation_airflow_sources = get_installation_airflow_sources()
if installation_airflow_sources is None and not
skip_breeze_self_upgrade_check():
console_print(
@@ -226,6 +373,9 @@ def find_airflow_root_path_to_operate_on() -> Path:
# only print warning and sleep if not producing complete results
reinstall_if_different_sources(airflow_sources)
reinstall_if_setup_changed()
+ # Not invoked via the shim (no AIRFLOW_ROOT_PATH). If breeze is still
on a legacy global
+ # uv tool / pipx install, nudge the user to migrate to the recommended
shim (ADR 0017).
+ warn_if_breeze_launcher_outdated(airflow_sources)
os.chdir(airflow_sources.as_posix())
airflow_home_dir = Path(os.environ.get("AIRFLOW_HOME", (Path.home() /
"airflow").resolve().as_posix()))
if airflow_sources.resolve() == airflow_home_dir.resolve():
diff --git a/dev/breeze/tests/test_shim_version_check.py
b/dev/breeze/tests/test_shim_version_check.py
new file mode 100644
index 00000000000..b027955a1d4
--- /dev/null
+++ b/dev/breeze/tests/test_shim_version_check.py
@@ -0,0 +1,212 @@
+# 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
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import subprocess
+from pathlib import Path
+from unittest import mock
+
+import pytest
+
+from airflow_breeze.utils import path_utils
+from airflow_breeze.utils.path_utils import (
+ BREEZE_SHIM_MARKER,
+ _parse_shim_version,
+ detect_legacy_global_breeze_install,
+ get_expected_shim_version,
+ warn_if_breeze_launcher_outdated,
+ warn_if_shim_outdated,
+)
+
+ACTUAL_AIRFLOW_SOURCES = Path(__file__).parents[3].resolve()
+
+
+def _managed_shim(version_line: str | None) -> str:
+ lines = ["#!/usr/bin/env bash", BREEZE_SHIM_MARKER]
+ if version_line is not None:
+ lines.append(version_line)
+ lines.append("set -e")
+ return "\n".join(lines) + "\n"
+
+
[email protected](
+ ("version_line", "expected"),
+ [
+ ("# breeze-shim-version: 1", 1),
+ ("# breeze-shim-version: 42", 42),
+ (" # breeze-shim-version: 3 ", 3),
+ ("# breeze-shim-version: not-a-number", None),
+ ("# something-else: 1", None),
+ (None, None),
+ ],
+)
+def test_parse_shim_version(version_line, expected):
+ assert _parse_shim_version(_managed_shim(version_line)) == expected
+
+
+def test_get_expected_shim_version_reads_real_setup_script():
+ # The real setup_breeze in the sources is the source of truth — keep this
test in sync
+ # with the SHIM_VERSION it declares.
+ assert get_expected_shim_version(ACTUAL_AIRFLOW_SOURCES) == 1
+
+
+def test_get_expected_shim_version_from_fake_sources(tmp_path):
+ setup_script = tmp_path / "scripts" / "tools" / "setup_breeze"
+ setup_script.parent.mkdir(parents=True)
+ setup_script.write_text('SHIM_DIR="x"\nSHIM_VERSION="7"\n')
+ assert get_expected_shim_version(tmp_path) == 7
+
+
+def test_get_expected_shim_version_missing_script(tmp_path):
+ assert get_expected_shim_version(tmp_path) is None
+
+
+def _fake_sources_with_version(tmp_path: Path, version: int) -> Path:
+ setup_script = tmp_path / "scripts" / "tools" / "setup_breeze"
+ setup_script.parent.mkdir(parents=True)
+ setup_script.write_text(f'SHIM_VERSION="{version}"\n')
+ return tmp_path
+
+
+def test_warn_if_shim_outdated_no_shim_file(tmp_path, monkeypatch, capsys):
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", tmp_path / "breeze")
+ sources = _fake_sources_with_version(tmp_path, 1)
+ assert warn_if_shim_outdated(sources) is False
+ assert capsys.readouterr().out == ""
+
+
+def test_warn_if_shim_outdated_foreign_script(tmp_path, monkeypatch, capsys):
+ shim = tmp_path / "breeze"
+ shim.write_text("#!/usr/bin/env bash\n# some user's own script\n")
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", shim)
+ sources = _fake_sources_with_version(tmp_path, 1)
+ assert warn_if_shim_outdated(sources) is False
+ assert capsys.readouterr().out == ""
+
+
+def test_warn_if_shim_outdated_up_to_date(tmp_path, monkeypatch, capsys):
+ shim = tmp_path / "breeze"
+ shim.write_text(_managed_shim("# breeze-shim-version: 1"))
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", shim)
+ sources = _fake_sources_with_version(tmp_path, 1)
+ assert warn_if_shim_outdated(sources) is False
+ assert capsys.readouterr().out == ""
+
+
+def test_warn_if_shim_outdated_newer_installed(tmp_path, monkeypatch, capsys):
+ shim = tmp_path / "breeze"
+ shim.write_text(_managed_shim("# breeze-shim-version: 5"))
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", shim)
+ sources = _fake_sources_with_version(tmp_path, 2)
+ assert warn_if_shim_outdated(sources) is False
+ assert capsys.readouterr().out == ""
+
+
+def test_warn_if_shim_outdated_older_installed(tmp_path, monkeypatch, capsys):
+ shim = tmp_path / "breeze"
+ shim.write_text(_managed_shim("# breeze-shim-version: 1"))
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", shim)
+ sources = _fake_sources_with_version(tmp_path, 2)
+ assert warn_if_shim_outdated(sources) is True
+ output = capsys.readouterr().out
+ assert "out of date" in output
+ assert "setup_breeze" in output
+
+
+def test_warn_if_shim_outdated_pre_versioning_shim(tmp_path, monkeypatch,
capsys):
+ # A managed shim with no version marker predates versioning — treat it as
outdated.
+ shim = tmp_path / "breeze"
+ shim.write_text(_managed_shim(None))
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", shim)
+ sources = _fake_sources_with_version(tmp_path, 1)
+ assert warn_if_shim_outdated(sources) is True
+ output = capsys.readouterr().out
+ assert "out of date" in output
+ assert "pre-versioning" in output
+
+
+def _run_result(stdout: str, returncode: int = 0):
+ return subprocess.CompletedProcess(args=[], returncode=returncode,
stdout=stdout, stderr="")
+
+
+def test_detect_legacy_global_breeze_install_uv():
+ with mock.patch("airflow_breeze.utils.path_utils.subprocess.run") as run:
+ run.return_value = _run_result("apache-airflow-breeze v3.0.0\n")
+ assert detect_legacy_global_breeze_install() == "uv"
+
+
+def test_detect_legacy_global_breeze_install_pipx():
+ def fake_run(cmd, *args, **kwargs):
+ if cmd[:2] == ["uv", "tool"]:
+ return _run_result("some-other-tool\n")
+ return _run_result("apache-airflow-breeze\n")
+
+ with mock.patch("airflow_breeze.utils.path_utils.subprocess.run",
side_effect=fake_run):
+ assert detect_legacy_global_breeze_install() == "pipx"
+
+
+def test_detect_legacy_global_breeze_install_none():
+ with mock.patch("airflow_breeze.utils.path_utils.subprocess.run") as run:
+ run.return_value = _run_result("")
+ assert detect_legacy_global_breeze_install() is None
+
+
+def test_detect_legacy_global_breeze_install_tools_missing():
+ with mock.patch("airflow_breeze.utils.path_utils.subprocess.run",
side_effect=FileNotFoundError):
+ assert detect_legacy_global_breeze_install() is None
+
+
+def test_launcher_check_prefers_shim_version(tmp_path, monkeypatch, capsys):
+ # When our managed shim is installed, the launcher check does the version
check and never
+ # shells out to detect a legacy install.
+ shim = tmp_path / "breeze"
+ shim.write_text(_managed_shim("# breeze-shim-version: 1"))
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", shim)
+ sources = _fake_sources_with_version(tmp_path, 2)
+ with
mock.patch("airflow_breeze.utils.path_utils.detect_legacy_global_breeze_install")
as detect:
+ assert warn_if_breeze_launcher_outdated(sources) is True
+ detect.assert_not_called()
+ output = capsys.readouterr().out
+ assert "out of date" in output
+
+
[email protected](
+ ("legacy", "uninstall_cmd"),
+ [
+ ("uv", "uv tool uninstall apache-airflow-breeze"),
+ ("pipx", "pipx uninstall apache-airflow-breeze"),
+ ],
+)
+def test_launcher_check_warns_on_legacy_install(tmp_path, monkeypatch, capsys,
legacy, uninstall_cmd):
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", tmp_path / "breeze")
# no shim file
+ sources = _fake_sources_with_version(tmp_path, 1)
+ with mock.patch(
+ "airflow_breeze.utils.path_utils.detect_legacy_global_breeze_install",
return_value=legacy
+ ):
+ assert warn_if_breeze_launcher_outdated(sources) is True
+ output = capsys.readouterr().out
+ assert "legacy global" in output
+ assert uninstall_cmd in output
+ assert "setup_breeze" in output
+
+
+def test_launcher_check_silent_without_shim_or_legacy(tmp_path, monkeypatch,
capsys):
+ monkeypatch.setattr(path_utils, "BREEZE_SHIM_PATH", tmp_path / "breeze")
# no shim file
+ sources = _fake_sources_with_version(tmp_path, 1)
+ with
mock.patch("airflow_breeze.utils.path_utils.detect_legacy_global_breeze_install",
return_value=None):
+ assert warn_if_breeze_launcher_outdated(sources) is False
+ assert capsys.readouterr().out == ""
diff --git a/scripts/tools/setup_breeze b/scripts/tools/setup_breeze
index f47ccfb905a..4ae4ab2c25e 100755
--- a/scripts/tools/setup_breeze
+++ b/scripts/tools/setup_breeze
@@ -35,26 +35,55 @@ SHIM_PATH="${SHIM_DIR}/breeze"
# foreign breeze binary (e.g. left over from `uv tool install`).
SHIM_MARKER="# Apache Airflow breeze shim — managed by
scripts/tools/setup_breeze (ADR 0017)."
+# Shim body version, stamped into the shim as a "# breeze-shim-version: N"
line.
+# Bump this whenever the shim body below changes. On startup breeze reads this
+# value from the sources it operates on and compares it with the version baked
+# into the installed shim; if the installed shim is older it warns the user to
+# re-run this script. See warn_if_shim_outdated() in
+# dev/breeze/src/airflow_breeze/utils/path_utils.py.
+SHIM_VERSION="1"
+
# The shim itself. Runs breeze via 'uvx' against the dev/breeze folder of the
# *current* git worktree, so multiple checkouts / agentic worktrees never
# share a single global install. See ADR 0017.
+#
+# When invoked outside any Airflow worktree (e.g. from an SVN release checkout
+# such as asf-dist during a provider release), the shim falls back to, in
order:
+# $AIRFLOW_REPO_ROOT (exported by the release docs) if it points at a worktree,
+# then the worktree this shim was installed from (AIRFLOW_SOURCES, baked in
below).
read -r -d '' BREEZE_SHIM_BODY <<BREEZE_SHIM || true
#!/usr/bin/env bash
${SHIM_MARKER}
+# breeze-shim-version: ${SHIM_VERSION}
# Runs breeze from the dev/breeze folder of the current git worktree via 'uvx',
# so each worktree (e.g. parallel agentic runs) gets its own
ephemerally-installed
# breeze tied to that worktree's source.
+#
+# Resolution order for the Airflow sources breeze runs from:
+# 1. the current git worktree (per-worktree isolation — see ADR 0017);
+# 2. \$AIRFLOW_REPO_ROOT, if exported and pointing at an Airflow worktree —
the
+# release docs export this, so breeze resolves the same way across every
+# release process regardless of where the shim was installed from;
+# 3. the install-time fallback baked in below (the worktree setup_breeze ran
from).
+# Steps 2 and 3 apply only when the current directory is not an Airflow
worktree,
+# so the fallbacks never override a real worktree and isolation is preserved.
set -e
-repo_root=\$(git rev-parse --show-toplevel 2>/dev/null) || {
- echo "breeze: not inside a git repository — cd into an Airflow worktree
first" >&2
- exit 1
-}
-if [ ! -d "\${repo_root}/dev/breeze" ]; then
- echo "breeze: \${repo_root} is not an Airflow worktree (no dev/breeze)" >&2
+# Install-time fallback: the Airflow sources 'scripts/tools/setup_breeze' was
run
+# from. Used only when the current directory is not an Airflow worktree.
+fallback_root="${AIRFLOW_SOURCES}"
+repo_root=\$(git rev-parse --show-toplevel 2>/dev/null) || repo_root=""
+if [ -n "\${repo_root}" ] && [ -d "\${repo_root}/dev/breeze" ]; then
+ breeze_root="\${repo_root}"
+elif [ -n "\${AIRFLOW_REPO_ROOT:-}" ] && [ -d
"\${AIRFLOW_REPO_ROOT}/dev/breeze" ]; then
+ breeze_root="\${AIRFLOW_REPO_ROOT}"
+elif [ -d "\${fallback_root}/dev/breeze" ]; then
+ breeze_root="\${fallback_root}"
+else
+ echo "breeze: not inside an Airflow worktree, AIRFLOW_REPO_ROOT is unset
or not an Airflow worktree, and the install-time fallback
'\${fallback_root}/dev/breeze' is missing — re-run scripts/tools/setup_breeze"
>&2
exit 1
fi
-exec env AIRFLOW_ROOT_PATH="\${repo_root}" SKIP_BREEZE_SELF_UPGRADE_CHECK=1 \\
- uvx --from "\${repo_root}/dev/breeze" --quiet breeze "\$@"
+exec env AIRFLOW_ROOT_PATH="\${breeze_root}" SKIP_BREEZE_SELF_UPGRADE_CHECK=1
\\
+ uvx --from "\${breeze_root}/dev/breeze" --quiet breeze "\$@"
BREEZE_SHIM
function manual_instructions() {