ethanstoner opened a new pull request, #72292: URL: https://github.com/apache/airflow/pull/72292
`croniter` accepts `?` in the day-of-month and day-of-week fields and expands it to `*` ([`croniter.py`](https://github.com/kiorky/croniter/blob/master/src/croniter/croniter.py): *"currently just trade `?` as `*`"*), so `0 0 ? * MON` schedules exactly like `0 0 * * MON`. Airflow's docs explicitly point users at croniter's extended syntax in [`cron.rst`](https://github.com/apache/airflow/blob/main/airflow-core/docs/authoring-and-scheduling/cron.rst). `CronMixin._describe_with_dom_dow_fix` compares the DOM/DOW fields against `"*"` only, so a `?` counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens. ### Reproduction (on `main`, before this change) ``` $ uv run --project airflow-core python -c '...' croniter 6.2.4 | cron-descriptor 2.1.0 expression : 0 0 ? * MON actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run expression : 0 0 1 * ? actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only description : 'At 00:00, on day 1 of the month (or) At 00:00' ``` The description is persisted as `DagModel.timetable_description` and surfaced in the UI, the REST API (`DAGDetailsResponse.timetable_description`) and `airflow dags details`, so the user is shown a schedule that contradicts what the scheduler actually does. ### After ``` '0 0 ? * MON' -> 'At 00:00, only on Monday' '0 0 1 * ?' -> 'At 00:00, on day 1 of the month' '0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday' ``` Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644. ### Testing Three parametrized cases added to `airflow-core/tests/unit/timetables/test_cron_mixin.py`, each asserting the `?` form describes identically to its `*` equivalent. They fail on unpatched `main`: ``` 3 failed, 3 passed FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month] FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week] FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both] ``` and pass with the fix. Full suite and static checks: - `uv run --project airflow-core pytest airflow-core/tests/unit/timetables/` — **256 passed** - `prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py` — all hooks pass - `prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py` — passed No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers. --- ##### Was generative AI tooling used to co-author this PR? - [X] Yes (please specify the tool below) Generated-by: Claude Code following [the guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions) The defect was found by auditing `airflow/timetables/`, reproduced against unmodified `main`, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
