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 6ae2133176d Add dry-run mode to teams sync (#72494)
6ae2133176d is described below

commit 6ae2133176d49fb8231092ba13e674761da8bd27
Author: SameerMesiah97 <[email protected]>
AuthorDate: Wed Sep 23 00:40:56 2026 +0100

    Add dry-run mode to teams sync (#72494)
    
    * Allow airflow teams sync --dry-run to preview missing teams anddefault 
team pools derived from the DAG bundle configuration without writing to the 
metadata database. Add tests covering dry-run behavior for missing teams, 
missing default team pools, and no-op sync scenarios.
    
    * Add no-op test coverage for teams sync dry-run.
    
    ---------
    
    Co-authored-by: Sameer Mesiah <[email protected]>
---
 airflow-core/newsfragments/72494.feature.rst       |   1 +
 airflow-core/src/airflow/cli/cli_config.py         |   7 +-
 .../src/airflow/cli/commands/team_command.py       |  47 ++++++---
 .../tests/unit/cli/commands/test_team_command.py   | 113 +++++++++++++++++++++
 4 files changed, 154 insertions(+), 14 deletions(-)

diff --git a/airflow-core/newsfragments/72494.feature.rst 
b/airflow-core/newsfragments/72494.feature.rst
new file mode 100644
index 00000000000..4406eb0e89a
--- /dev/null
+++ b/airflow-core/newsfragments/72494.feature.rst
@@ -0,0 +1 @@
+Add ``--dry-run`` to ``airflow teams sync`` so deployments can preview missing 
teams and default team pools without writing to the metadata database.
diff --git a/airflow-core/src/airflow/cli/cli_config.py 
b/airflow-core/src/airflow/cli/cli_config.py
index 0274fc4402e..1c0913db8c3 100644
--- a/airflow-core/src/airflow/cli/cli_config.py
+++ b/airflow-core/src/airflow/cli/cli_config.py
@@ -375,6 +375,11 @@ ARG_POOL = Arg(("--pool",), "Resource pool to use")
 
 # teams
 ARG_TEAM_NAME = Arg(("name",), help="Team name")
+ARG_TEAMS_DRY_RUN = Arg(
+    ("--dry-run",),
+    help="Show what would be synchronized without making changes.",
+    action="store_true",
+)
 
 # backfill
 ARG_BACKFILL_DAG = Arg(flags=("--dag-id",), help="The dag to backfill.", 
required=True)
@@ -1690,7 +1695,7 @@ TEAMS_COMMANDS = (
         help="Sync teams",
         description=("Sync missing teams from the dag bundle config into the 
database.\n"),
         func=lazy_load_command("airflow.cli.commands.team_command.team_sync"),
-        args=(ARG_VERBOSE,),
+        args=(ARG_TEAMS_DRY_RUN, ARG_VERBOSE),
     ),
     ActionCommand(
         name="verify",
diff --git a/airflow-core/src/airflow/cli/commands/team_command.py 
b/airflow-core/src/airflow/cli/commands/team_command.py
index 5f739e475ce..630141cfd86 100644
--- a/airflow-core/src/airflow/cli/commands/team_command.py
+++ b/airflow-core/src/airflow/cli/commands/team_command.py
@@ -234,23 +234,44 @@ def team_sync(args, *, session=NEW_SESSION):
             "Names already stored must be corrected before syncing."
         )
 
-    teams_added = 0
+    teams_to_add = sorted(dag_bundle_teams - existing_teams)
+    pools_to_add = []
+
+    for team_name in dag_bundle_teams:
+        pool = session.scalar(
+            select(Pool).where(
+                Pool.pool == Pool.get_default_team_pool_name(team_name),
+                Pool.team_name == team_name,
+            )
+        )
+
+        if pool is None:
+            pools_to_add.append(Pool.get_default_team_pool_name(team_name))
+
+    if args.dry_run:
+        if not teams_to_add and not pools_to_add:
+            print("No changes to sync.")
+            return
+
+        if teams_to_add:
+            print("Teams to add:")
+            for team_name in teams_to_add:
+                print(f"  - {team_name}")
+
+        if pools_to_add:
+            print("Default team pools to add:")
+            for pool_name in sorted(pools_to_add):
+                print(f"  - {pool_name}")
+
+        return
 
     try:
         for team_name in dag_bundle_teams:
-            if team_name not in existing_teams:
+            if team_name in teams_to_add:
                 session.add(Team(name=team_name))
                 session.flush()
-                teams_added += 1
-
-            pool = session.scalar(
-                select(Pool).where(
-                    Pool.pool == Pool.get_default_team_pool_name(team_name),
-                    Pool.team_name == team_name,
-                )
-            )
 
-            if pool is None:
+            if Pool.get_default_team_pool_name(team_name) in pools_to_add:
                 _create_default_team_pool(team_name=team_name, session=session)
 
         session.commit()
@@ -258,8 +279,8 @@ def team_sync(args, *, session=NEW_SESSION):
         session.rollback()
         raise SystemExit(f"Failed to sync teams: {e}")
 
-    if teams_added > 0:
-        print(f"{teams_added} teams added.")
+    if teams_to_add:
+        print(f"{len(teams_to_add)} teams added.")
 
 
 @cli_utils.action_cli
diff --git a/airflow-core/tests/unit/cli/commands/test_team_command.py 
b/airflow-core/tests/unit/cli/commands/test_team_command.py
index 4f1889dd383..c96b44b3c91 100644
--- a/airflow-core/tests/unit/cli/commands/test_team_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_team_command.py
@@ -599,6 +599,119 @@ class TestCliTeams:
 
         assert "Verification succeeded." in stdout.getvalue()
 
+    def test_team_sync_dry_run_reports_changes_without_writing(self, 
stdout_capture):
+        bundle_config = [
+            {
+                "name": "bundleone",
+                "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle",
+                "kwargs": {"path": "/dev/null", "refresh_interval": 0},
+                "team_name": "team1",
+            },
+            {
+                "name": "bundletwo",
+                "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle",
+                "kwargs": {"path": "/dev/null", "refresh_interval": 300},
+                "team_name": "team2",
+            },
+        ]
+
+        with conf_vars(
+            {
+                ("core", "multi_team"): "True",
+                ("dag_processor", "dag_bundle_config_list"): 
json.dumps(bundle_config),
+            }
+        ):
+            with stdout_capture as stdout:
+                team_command.team_sync(self.parser.parse_args(["teams", 
"sync", "--dry-run"]))
+
+        output = stdout.getvalue()
+
+        assert "Teams to add:" in output
+        assert "team1" in output
+        assert "team2" in output
+        assert "Default team pools to add:" in output
+        assert Pool.get_default_team_pool_name("team1") in output
+        assert Pool.get_default_team_pool_name("team2") in output
+
+        assert self.session.scalars(select(Team)).all() == []
+        assert (
+            self.session.scalar(select(Pool).where(Pool.pool == 
Pool.get_default_team_pool_name("team1")))
+            is None
+        )
+        assert (
+            self.session.scalar(select(Pool).where(Pool.pool == 
Pool.get_default_team_pool_name("team2")))
+            is None
+        )
+
+    def 
test_team_sync_dry_run_reports_missing_default_pool_without_writing(self, 
stdout_capture):
+        bundle_config = [
+            {
+                "name": "bundleone",
+                "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle",
+                "kwargs": {"path": "/dev/null", "refresh_interval": 0},
+                "team_name": "team1",
+            },
+        ]
+
+        self.session.add(Team(name="team1"))
+        self.session.commit()
+
+        with conf_vars(
+            {
+                ("core", "multi_team"): "True",
+                ("dag_processor", "dag_bundle_config_list"): 
json.dumps(bundle_config),
+            }
+        ):
+            with stdout_capture as stdout:
+                team_command.team_sync(self.parser.parse_args(["teams", 
"sync", "--dry-run"]))
+
+        output = stdout.getvalue()
+
+        assert "Teams to add:" not in output
+        assert "Default team pools to add:" in output
+        assert Pool.get_default_team_pool_name("team1") in output
+
+        assert self.session.scalar(select(Team).where(Team.name == "team1")) 
is not None
+        assert (
+            self.session.scalar(select(Pool).where(Pool.pool == 
Pool.get_default_team_pool_name("team1")))
+            is None
+        )
+
+    def test_team_sync_dry_run_reports_no_changes(self, stdout_capture):
+        bundle_config = [
+            {
+                "name": "bundleone",
+                "classpath": 
"airflow.dag_processing.bundles.local.LocalDagBundle",
+                "kwargs": {"path": "/dev/null", "refresh_interval": 0},
+                "team_name": "team1",
+            },
+        ]
+
+        self.session.add(Team(name="team1"))
+        self.session.commit()
+
+        self.session.add(
+            Pool(
+                pool=Pool.get_default_team_pool_name("team1"),
+                slots=128,
+                description="Default pool",
+                include_deferred=False,
+                team_name="team1",
+            )
+        )
+        self.session.commit()
+
+        with conf_vars(
+            {
+                ("core", "multi_team"): "True",
+                ("dag_processor", "dag_bundle_config_list"): 
json.dumps(bundle_config),
+            }
+        ):
+            with stdout_capture as stdout:
+                team_command.team_sync(self.parser.parse_args(["teams", 
"sync", "--dry-run"]))
+
+        assert "No changes to sync." in stdout.getvalue()
+
     def test_team_verify_missing_default_pool(self):
         self.session.add(Team(name="team1"))
         self.session.commit()

Reply via email to