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

pierrejeambrun 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 7ec8fc9ce5b Fix 'airflow dags next-execution --table' crash when no 
next run exists (#67642)
7ec8fc9ce5b is described below

commit 7ec8fc9ce5bbf91cc1c40d35b8642c01b00bac3c
Author: GayathriSrividya <[email protected]>
AuthorDate: Wed Jun 3 15:26:45 2026 +0530

    Fix 'airflow dags next-execution --table' crash when no next run exists 
(#67642)
    
    * Fix 'airflow dags next-execution --table' crash when no next run exists
    
    * Remove unnecessary newsfragment for next-execution table crash fix as per 
review
    
    ---------
    
    Co-authored-by: Gayathri Srividya Rajavarapu 
<[email protected]>
---
 .../src/airflow/cli/commands/dag_command.py        | 12 ++++++-
 .../tests/unit/cli/commands/test_dag_command.py    | 39 ++++++++++++++++++++++
 2 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/airflow-core/src/airflow/cli/commands/dag_command.py 
b/airflow-core/src/airflow/cli/commands/dag_command.py
index c0aa6e206b5..6818df4365e 100644
--- a/airflow-core/src/airflow/cli/commands/dag_command.py
+++ b/airflow-core/src/airflow/cli/commands/dag_command.py
@@ -444,7 +444,17 @@ def dag_next_execution(args) -> None:
         else:
             columns = ["logical_date", "data_interval.start", 
"data_interval.end", "run_after"]
         getters = [(c, operator.attrgetter(c)) for c in columns]
-        AirflowConsole().print_as_table([{n: f(o) for n, f in getters} for o 
in iter_next_dagrun_info()])
+        rows = []
+        for info in iter_next_dagrun_info():
+            if info is None:
+                print(
+                    "[WARN] No following schedule can be found. "
+                    "This DAG may have schedule interval '@once' or `None`.",
+                    file=sys.stderr,
+                )
+            else:
+                rows.append({n: f(info) for n, f in getters})
+        AirflowConsole().print_as_table(rows)
         return
 
     if args.field:
diff --git a/airflow-core/tests/unit/cli/commands/test_dag_command.py 
b/airflow-core/tests/unit/cli/commands/test_dag_command.py
index 37178a85ef5..69f463089ce 100644
--- a/airflow-core/tests/unit/cli/commands/test_dag_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_dag_command.py
@@ -274,6 +274,45 @@ class TestCliDags:
         clear_db_dags()
         parse_and_sync_to_db(os.devnull, include_examples=True)
 
+    @conf_vars({("core", "load_examples"): "false"})
+    @pytest.mark.parametrize(
+        ("dag_id", "schedule"),
+        [
+            pytest.param("table_none_schedule", "None", id="schedule_none"),
+            pytest.param("table_once_schedule", "'@once'", 
id="schedule_once_with_two_executions"),
+        ],
+    )
+    def test_next_execution_table_flag_with_no_next_run(
+        self, dag_id, schedule, tmp_path, stdout_capture, stderr_capture
+    ):
+        """Regression test for #67394: --table must not crash when schedule 
yields None."""
+        file_content = os.linesep.join(
+            [
+                "from airflow import DAG",
+                "from airflow.providers.standard.operators.empty import 
EmptyOperator",
+                "from datetime import timedelta; from pendulum import today",
+                f"dag = DAG('{dag_id}', start_date=today(tz='UTC') + 
timedelta(days=-5), schedule={schedule})",
+                "task = EmptyOperator(task_id='empty_task', dag=dag)",
+            ]
+        )
+        dag_file = tmp_path / f"{dag_id}.py"
+        dag_file.write_text(file_content)
+
+        with time_machine.travel(DEFAULT_DATE):
+            clear_db_dags()
+            parse_and_sync_to_db(tmp_path, include_examples=False)
+
+        args = self.parser.parse_args(["dags", "next-execution", dag_id, 
"--table", "--num-executions", "2"])
+        # Must not raise AttributeError on None DagRunInfo
+        with stdout_capture:
+            with stderr_capture as temp_stderr:
+                dag_command.dag_next_execution(args)
+        assert "No following schedule" in temp_stderr.getvalue()
+
+        # Rebuild Test DB for other tests
+        clear_db_dags()
+        parse_and_sync_to_db(os.devnull, include_examples=True)
+
     @conf_vars({("core", "load_examples"): "true"})
     def test_cli_report(self, stdout_capture):
         args = self.parser.parse_args(["dags", "report", "--output", "json"])

Reply via email to