This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-2-test by this push:
new 3d903c9e22d [v3-2-test] Fix 'airflow dags next-execution --table'
crash when no next run exists (#67642) (#67949)
3d903c9e22d is described below
commit 3d903c9e22d10e8519ce5827bdb701c3b91312bc
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Jun 3 13:33:11 2026 +0200
[v3-2-test] Fix 'airflow dags next-execution --table' crash when no next
run exists (#67642) (#67949)
* Fix 'airflow dags next-execution --table' crash when no next run exists
* Remove unnecessary newsfragment for next-execution table crash fix as per
review
---------
(cherry picked from commit 7ec8fc9ce5bbf91cc1c40d35b8642c01b00bac3c)
Co-authored-by: GayathriSrividya <[email protected]>
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 6572f467ff3..4e13d136927 100644
--- a/airflow-core/src/airflow/cli/commands/dag_command.py
+++ b/airflow-core/src/airflow/cli/commands/dag_command.py
@@ -363,7 +363,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 cb59144f107..670b4f0703c 100644
--- a/airflow-core/tests/unit/cli/commands/test_dag_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_dag_command.py
@@ -272,6 +272,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"])