This is an automated email from the ASF dual-hosted git repository.
henry3260 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-3-test by this push:
new 4c91c4ee51f [v3-3-test] Fix airflow config lint staying silent on
conditional removal rules (#70977) (#71651)
4c91c4ee51f is described below
commit 4c91c4ee51f7c7ac8eeac0dfefd17ebc72531558
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Aug 15 22:27:48 2026 +0800
[v3-3-test] Fix airflow config lint staying silent on conditional removal
rules (#70977) (#71651)
(cherry picked from commit 39d9abaa3bf45fe043fd04eaabe2c029556c3e3a)
Co-authored-by: Y-C <[email protected]>
---
airflow-core/newsfragments/70977.bugfix.rst | 1 +
.../src/airflow/cli/commands/config_command.py | 20 ++++---
.../tests/unit/cli/commands/test_config_command.py | 62 ++++++++++++++++++++++
3 files changed, 77 insertions(+), 6 deletions(-)
diff --git a/airflow-core/newsfragments/70977.bugfix.rst
b/airflow-core/newsfragments/70977.bugfix.rst
new file mode 100644
index 00000000000..0a3f85b63c4
--- /dev/null
+++ b/airflow-core/newsfragments/70977.bugfix.rst
@@ -0,0 +1 @@
+Fixed ``airflow config lint`` silently skipping removal rules that only apply
to a specific value.
diff --git a/airflow-core/src/airflow/cli/commands/config_command.py
b/airflow-core/src/airflow/cli/commands/config_command.py
index 5f087fdfa4c..e464fa1e80b 100644
--- a/airflow-core/src/airflow/cli/commands/config_command.py
+++ b/airflow-core/src/airflow/cli/commands/config_command.py
@@ -139,12 +139,12 @@ class ConfigChange:
f"`{self.config.option}` configuration parameter renamed to
`{self.renamed_to.option}` "
f"in the `{self.config.section}` section."
)
- if self.was_removed and not self.remove_if_equals:
- return (
- f"Removed{' deprecated' if self.was_deprecated else ''}
`{self.config.option}` configuration parameter "
- f"from `{self.config.section}` section. "
- f"{self.suggestion}"
- )
+ if self.was_removed:
+ if self.remove_if_equals is None:
+ return self._removed_message
+ # Only the exact value is dropped, so an unreadable option must
never count as a match.
+ if conf.get(self.config.section, self.config.option,
fallback=None) == str(self.remove_if_equals):
+ return self._removed_message
if self.is_invalid_if is not None:
value = conf.get(self.config.section, self.config.option)
if value == self.is_invalid_if:
@@ -154,6 +154,14 @@ class ConfigChange:
)
return None
+ @property
+ def _removed_message(self) -> str:
+ return (
+ f"Removed{' deprecated' if self.was_deprecated else ''}
`{self.config.option}` configuration parameter "
+ f"from `{self.config.section}` section. "
+ f"{self.suggestion}"
+ )
+
CONFIGS_CHANGES = [
# admin
diff --git a/airflow-core/tests/unit/cli/commands/test_config_command.py
b/airflow-core/tests/unit/cli/commands/test_config_command.py
index ecbb31fbb19..c6345b96434 100644
--- a/airflow-core/tests/unit/cli/commands/test_config_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_config_command.py
@@ -531,6 +531,68 @@ class TestConfigLint:
assert "Invalid value" not in normalized_output
+ @pytest.mark.parametrize(
+ ("remove_if_equals", "config_value", "expect_issue"),
+ [
+ pytest.param("removed_value", "removed_value", True, id="match"),
+ pytest.param("removed_value", "kept_value", False, id="no-match"),
+ pytest.param("", "", True, id="empty-string-match"),
+ pytest.param("", "kept_value", False, id="empty-string-no-match"),
+ ],
+ )
+ def test_lint_reports_conditional_removal_only_when_value_matches(
+ self, remove_if_equals, config_value, expect_issue, stdout_capture
+ ):
+ config_change = ConfigChange(
+ config=ConfigParameter("test_section", "test_option"),
+ was_removed=True,
+ remove_if_equals=remove_if_equals,
+ )
+ with (
+ mock.patch.object(config_command, "CONFIGS_CHANGES",
[config_change]),
+ conf_vars({("test_section", "test_option"): config_value}),
+ stdout_capture as temp_stdout,
+ ):
+
config_command.lint_config(cli_parser.get_parser().parse_args(["config",
"lint"]))
+
+ normalized_output = re.sub(r"\s+", " ", temp_stdout.getvalue().strip())
+ expected_message = (
+ "Removed deprecated `test_option` configuration parameter from
`test_section` section."
+ )
+
+ assert (expected_message in normalized_output) is expect_issue
+
+ @pytest.mark.parametrize(
+ ("section", "option", "value"),
+ [
+ ("core", "hostname", ":"),
+ ("email", "email_backend",
"airflow.contrib.utils.sendgrid.send_email"),
+ ("elasticsearch", "log_id_template",
"{dag_id}-{task_id}-{logical_date}-{try_number}"),
+ (
+ "logging",
+ "log_filename_template",
+ "{{ ti.dag_id }}/{{ ti.task_id }}/{{ ts }}/{{ try_number
}}.log",
+ ),
+ (
+ "logging",
+ "log_filename_template",
+ "dag_id={{ ti.dag_id }}/run_id={{ ti.run_id }}/task_id={{
ti.task_id }}/"
+ "{% if ti.map_index >= 0 %}map_index={{ ti.map_index }}/{%
endif %}"
+ "attempt={{ try_number }}.log",
+ ),
+ ],
+ )
+ def test_lint_detects_shipped_conditional_removals(self, section, option,
value, stdout_capture):
+ env_var = f"AIRFLOW__{section.upper()}__{option.upper()}"
+ with mock.patch.dict(os.environ, {env_var: value}), stdout_capture as
temp_stdout:
+ config_command.lint_config(
+ cli_parser.get_parser().parse_args(["config", "lint",
"--section", section])
+ )
+
+ normalized_output = re.sub(r"\s+", " ", temp_stdout.getvalue().strip())
+
+ assert f"`{option}` configuration parameter from `{section}` section."
in normalized_output
+
class TestCliConfigUpdate:
@conf_vars({("core", "executor"): "SequentialExecutor"})