- name
- upgrade-fab-provider
- description
- Upgrade the pinned Flask-AppBuilder (FAB) dependency in the Apache Airflow FAB provider (`providers/fab/`). Bumps the exact `flask-appbuilder==` pin and its mirror constant, regenerates `uv.lock`, drives the `test_fab_alignment.py` drift tripwire, reviews the vendored security-manager `override.py` against the new upstream FAB, and conditionally re-vendors static assets / DB migrations. Use when asked to "upgrade FAB", "bump flask-appbuilder", or move the FAB provider to a newer Flask-AppBuilder release.
- license
- Apache-2.0
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
# upgrade-fab-provider
Airflow's FAB provider is **tightly coupled** to a specific Flask-AppBuilder
release because it vendors-in and subclasses large parts of FAB's security
manager. A version bump is therefore never "just change the pin" — it must be
reconciled against the vendored code, and that reconciliation is enforced by a
pytest **alignment test**, not a prek hook.
The canonical reference for a real bump is PR **#66841** ("Bump flask-appbuilder
to 5.2.1 and mirror new auth event hooks") — commit `c72b6613fd`. Read its diff
first when in doubt: `git show c72b6613fd`.
## The coupling — why this is not a one-line change
- `providers/fab/pyproject.toml:75-80` explains it: Airflow vendored FAB's
security-manager code into
`providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py`
(~2700 lines) as `FabAirflowSecurityManagerOverride`. Every bump must review
that class against upstream FAB for new / changed / removed methods.
- `test_fab_alignment.py` mechanically detects drift between the **installed**
FAB package and the vendored override, and **fails CI** until the developer
reconciles it.
## Inputs
- **Target version** — the FAB version to move to. If not given, use the latest
release on PyPI (`https://pypi.org/pypi/flask-appbuilder/json` → `info.version`).
Confirm the target with the user if it is a **major or minor** bump (higher
reconciliation risk); a **patch** bump can proceed.
## The files a bump touches
**Always:**
1. `providers/fab/pyproject.toml` — line ~80, the `flask-appbuilder==X.Y.Z` pin
(the **only** real dependency pin in the repo).
2. `providers/fab/tests/unit/fab/auth_manager/security_manager/test_fab_alignment.py`
— `EXPECTED_FAB_VERSION = "X.Y.Z"` (line ~43). Must move in lockstep with the pin.
3. `providers/fab/docs/index.rst` — the dependency table row
``` ``flask-appbuilder`` ``==X.Y.Z`` ``` (line ~114).
4. `providers/fab/README.rst` — the Requirements table row (line ~60). **Do
not hand-edit** — it is auto-generated. Regenerate it from the bumped
`pyproject.toml` with the `sync-provider-readme` prek hook (Step 8); the hook
re-renders the table whenever `pyproject.toml` changes. (Pre-existing bumps
that predate this hook left it to release-time regeneration; today the hook is
per-commit, so CI flags the drift — run it.)
5. `uv.lock` — regenerated (see Step 4 for the pinned-uv caveat).
**Conditionally:**
6. `providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py`
— transplant any relevant upstream FAB changes (new auth hooks, changed
signatures, ported fixes). The reference bump added +37 lines here. **Note:**
a green alignment test does not prove the transplant is unnecessary — a
security fix may live inside a method Airflow vendors (see the 5.2.2 worked
example below).
7. `providers/fab/src/airflow/providers/fab/www/**` + `providers/fab/www-hash.txt`
— only if the new FAB ships changed static assets / templates that are
re-vendored (see Step 6).
8. `providers/fab/src/airflow/providers/fab/migrations/versions/**` — only if the
new FAB version ships DB migrations (see Step 7).
**Never:** no newsfragment, and do **not** hand-edit `providers/fab/docs/changelog.rst`
— providers are released from `main` and the release manager regenerates the
changelog from `git log` (per `providers/AGENTS.md`). The commit subject is the
changelog entry.
## Procedure
### Step 1 — Determine the target version and current state
- Read the current pin: `grep flask-appbuilder== providers/fab/pyproject.toml`.
- Resolve the target (PyPI latest, or the user's requested version).
- Classify the jump: patch / minor / major. For minor/major, skim the FAB
release notes (`https://github.com/dpgaspar/Flask-AppBuilder/releases`) for
security-manager / model / template changes before editing.
### Step 2 — Bump the three source pins
Edit in lockstep:
- `providers/fab/pyproject.toml` — the `flask-appbuilder==X.Y.Z` line (keep the
trailing `# Whenever updating the version, run test_fab_alignment.py to verify.`
comment).
- `test_fab_alignment.py` — `EXPECTED_FAB_VERSION = "X.Y.Z"`. It is the tripwire;
editing it now is fine — you will keep re-running the test until it and the
other three tests pass.
- `providers/fab/docs/index.rst` — the dependency-table `==X.Y.Z` row.
### Step 3 — Install the new FAB into the provider venv
`uv --project providers/fab sync` installs the new pin. Confirm the installed
version:
```bash
uv run --project providers/fab python -c \
"import importlib.metadata as m; print(m.version('flask-appbuilder'))"
```
### Step 4 — Regenerate uv.lock (pinned uv!)
The lock **must** be regenerated with the repo's pinned uv version, or it drifts
hundreds of unrelated marker lines:
```bash
AIRFLOW_UV_VERSION=$(grep -oE 'AIRFLOW_UV_VERSION=[0-9.]+' Dockerfile.ci | head -1 | cut -d= -f2)
uvx --from uv==$AIRFLOW_UV_VERSION uv lock
```
If a conflict is irrecoverable, delete `uv.lock` and re-run `uv lock` with the
pinned version. Confirm the diff is limited to the FAB bump, not a wholesale
marker rewrite.
### Step 5 — Run the alignment test and reconcile override.py
```bash
uv run --project providers/fab pytest \
providers/fab/tests/unit/fab/auth_manager/security_manager/test_fab_alignment.py -xvs
```
Under the host sandbox the test needs a writable `AIRFLOW_HOME` and the
rerun-failures socket disabled — if it errors on a socket `bind` or on
`~/airflow`, run it as:
```bash
AIRFLOW_HOME="$TMPDIR/fab_home" uv run --project providers/fab pytest \
providers/fab/tests/.../test_fab_alignment.py -q -p no:rerunfailures
```
The four tests and how to fix each:
1. **`test_fab_version_matches_expected`** — trips on the version mismatch. It
passes once `EXPECTED_FAB_VERSION` == installed version, but only after you
have done the review below.
2. **`test_no_unaudited_fab_methods`** — a new FAB public method exists that is
neither implemented in `override.py` nor listed in `AUDITED_EXCLUSIONS`.
Fix: either implement/override it in `override.py`, or add it to
`AUDITED_EXCLUSIONS` **with a justification comment**.
3. **`test_no_stale_exclusions`** — `AUDITED_EXCLUSIONS` lists a method the new
FAB no longer has. Fix: remove that entry.
4. **`test_shared_method_signatures_compatible`** — FAB changed a method
signature (new required param). Fix: update the `override.py` signature, or
add to `KNOWN_SIGNATURE_DEVIATIONS` if the divergence is intentional.
**The manual review that the test cannot fully automate:** diff the vendored
`override.py` against the new FAB's `flask_appbuilder/security/sqla/manager.py`
and `BaseSecurityManager` and **transplant behavioural changes** (bug fixes, new
auth event hooks), not just signatures — the test only checks method *presence*
and *required params*. Locate the installed source:
```bash
uv run --project providers/fab python -c \
"import flask_appbuilder.security.sqla.manager as m; print(m.__file__)"
```
### Step 6 — Re-vendor static assets / templates (only if changed)
If the new FAB changed frontend assets that Airflow vendors under
`providers/fab/src/airflow/providers/fab/www/` (templates in
`templates/appbuilder/`, static JS/CSS), re-vendor them, then regenerate the
fingerprint:
```bash
prek run compile-fab-assets --all-files
```
This runs `scripts/ci/prek/compile_provider_assets.py fab` (pnpm build over
`www/`) and rewrites `providers/fab/www-hash.txt`. A patch bump usually does
**not** touch assets — skip this step unless FAB's templates/static changed.
Commit the regenerated `www-hash.txt` if it changed.
### Step 7 — DB migrations (only if FAB ships them)
If the new FAB adds/changes security-model tables, add the corresponding
migration under `providers/fab/src/airflow/providers/fab/migrations/versions/`
and run:
```bash
prek run update-migration-references-fab check-revision-heads-map-fab --all-files
```
Patch bumps normally have no migrations — skip unless the release notes mention
schema changes.
### Step 8 — Static checks + tests
```bash
prek run --from-ref main --stage pre-commit
uv run --project providers/fab pytest providers/fab/tests/unit/fab/auth_manager -xvs
```
The pre-commit stage runs `sync-provider-readme` (regenerating `README.rst`) and
other FAB hooks. Re-run the alignment test until all four tests pass. The full
provider suite is `breeze testing providers-tests --test-type "Providers[fab]"`.
### Step 9 — Self-review and commit
- `git diff main...HEAD` — verify only the intended files changed, and `uv.lock`
is a clean FAB-scoped diff.
- Commit subject in imperative mood, plain prose, **no** Conventional-Commits
prefix, e.g. `Bump flask-appbuilder to X.Y.Z in FAB provider`. Body explains
*why* (what upstream changes were mirrored), not what.
- No newsfragment, no changelog edit (provider release manager regenerates from
git log).
- Prepend your PR to the history list in `providers/fab/CONTRIBUTING.rst` so the
FAB-upgrade record stays current.
- Push to `origin` and open the PR per the repo's PR conventions.
## Gotchas
- **`EXPECTED_FAB_VERSION` is a second pin.** Forgetting it makes
`test_fab_alignment.py` fail even when everything else is correct.
- **`uv.lock` marker drift.** Always use the pinned uv (Step 4) — a bare
`uv lock` rewrites hundreds of environment-marker lines and buries the real diff.
- **The alignment test uses AST, not import**, to read FAB's `SecurityManager`
(to avoid SQLAlchemy model-registry collisions with Airflow's vendored models).
A green test proves *structural* alignment; it does **not** prove behavioural
parity — Step 5's manual transplant review is still required.
- **`docs/index.rst` dependency table** may look auto-generated but the reference
PR edited it by hand; if a docs-regen prek hook rewrites it, let the hook win.
- **`README.rst` is generated, but the sync hook is per-commit.** Don't hand-edit
it; run `prek run sync-provider-readme` (or the full pre-commit stage) after
bumping `pyproject.toml` — CI fails on the drift otherwise.
- **Don't touch `providers/fab/docs/upgrading.rst`** — that is end-user guidance
for upgrading the *provider package* in a deployment, not the developer bump
workflow.
## Worked example — 5.2.1 to 5.2.2 (patch)
A patch bump that needed **no `override.py` change**, but only after a real
behavioural review — the green alignment test alone was not sufficient evidence:
- FAB 5.2.2 shipped three security-manager fixes: LDAP search-filter escaping,
OAuth email-allowlist regex anchoring (`email + "$"`), and API-login provider
validation.
- All three touch methods Airflow *vendors* (`_search_ldap`, `auth_user_ldap`,
`auth_user_oauth`) — so the alignment test passing did **not** mean "nothing to
do". Each had to be checked by hand:
- **LDAP escaping** — Airflow's vendored `_search_ldap` **already** escapes via
`ldap.filter.escape_filter_chars` (and adds filter-parenthesis validation); it
was ahead of FAB. No transplant.
- **OAuth allowlist anchoring** — the match lives in FAB's `AuthOAuthView`
(`views.py`), and Airflow's `CustomAuthOAuthView.oauth_authorized` delegates
via `super().oauth_authorized()`, so the fix is inherited from the installed
FAB 5.2.2. No transplant.
- **API-login validation / Azure-JWT warning / uuid4** — in FAB core Airflow
doesn't vendor. Inherited.
- Net change set: `pyproject.toml`, `test_fab_alignment.py`, `docs/index.rst`,
`README.rst` (via hook), `uv.lock`. Commit: `Bump flask-appbuilder to 5.2.2 in
FAB provider`.
The lesson the skill encodes: **for every security/behavioural fix in the FAB
release notes, locate the method and check whether Airflow vendors it** — the
alignment test guards structure, you guard behaviour.
View on GitHub