From afd4ea391a92862ec360419dc1704538267d5887 Mon Sep 17 00:00:00 2001 From: Vincent Hsiao <124506982+fat-catTW@users.noreply.github.com> Date: Fri, 24 Jul 2026 08:54:15 +0000 Subject: [PATCH] Fix BashOperator script detection after templating BashOperator template fields are rendered after construction, so script detection based on the raw constructor argument can choose the wrong execution path for templated commands. --- .../providers/standard/decorators/bash.py | 1 - .../providers/standard/operators/bash.py | 5 ++--- .../tests/unit/standard/operators/test_bash.py | 17 +++++++++++++++++ .../prek/validate_operators_init_exemptions.txt | 1 - 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/providers/standard/src/airflow/providers/standard/decorators/bash.py b/providers/standard/src/airflow/providers/standard/decorators/bash.py index 169a8dea58adc..8cf704b9f8480 100644 --- a/providers/standard/src/airflow/providers/standard/decorators/bash.py +++ b/providers/standard/src/airflow/providers/standard/decorators/bash.py @@ -88,7 +88,6 @@ def execute(self, context: Context) -> Any: if not isinstance(self.bash_command, str) or self.bash_command.strip() == "": raise TypeError("The returned value from the TaskFlow callable must be a non-empty string.") - self._is_inline_cmd = self._is_inline_command(bash_command=self.bash_command) self.render_template_fields(context) return super().execute(context) diff --git a/providers/standard/src/airflow/providers/standard/operators/bash.py b/providers/standard/src/airflow/providers/standard/operators/bash.py index 8f5205c11bd8e..c094928736060 100644 --- a/providers/standard/src/airflow/providers/standard/operators/bash.py +++ b/providers/standard/src/airflow/providers/standard/operators/bash.py @@ -178,9 +178,7 @@ def __init__( self.cwd = cwd self.append_env = append_env self.output_processor = output_processor - self._is_inline_cmd = None - if isinstance(bash_command, str): - self._is_inline_cmd = self._is_inline_command(bash_command=bash_command) + self._is_inline_cmd: bool | None = None @cached_property def subprocess_hook(self): @@ -215,6 +213,7 @@ def execute(self, context: Context): raise AirflowException(f"The cwd {self.cwd} must be a directory") env = self.get_env(context) + self._is_inline_cmd = self._is_inline_command(bash_command=cast("str", self.bash_command)) if self._is_inline_cmd: result = self._run_inline_command(bash_path=bash_path, env=env) else: diff --git a/providers/standard/tests/unit/standard/operators/test_bash.py b/providers/standard/tests/unit/standard/operators/test_bash.py index 1a8aad3798151..b38a53d9d078b 100644 --- a/providers/standard/tests/unit/standard/operators/test_bash.py +++ b/providers/standard/tests/unit/standard/operators/test_bash.py @@ -33,6 +33,7 @@ AirflowTaskTimeout, timezone, ) +from airflow.providers.standard.hooks.subprocess import SubprocessResult from airflow.providers.standard.operators.bash import BashOperator from airflow.utils.state import State from airflow.utils.types import DagRunType @@ -286,6 +287,22 @@ def test_templated_fields(self, dag_maker, create_task_instance_of_operator): assert task.bash_command == 'echo "test_templated_fields_dag"' assert task.cwd == Path(__file__).absolute().parent.as_posix() + @mock.patch.object(BashOperator, "_run_inline_command") + @mock.patch.object( + BashOperator, "_run_rendered_script_file", return_value=SubprocessResult(exit_code=0, output="ok") + ) + def test_execute_detects_script_after_bash_command_is_rendered( + self, mock_run_rendered_script_file, mock_run_inline_command, context + ): + op = BashOperator(task_id="abc", bash_command="{{ bash_script }}") + op.bash_command = "sample.sh" + + result = op.execute(context) + + assert result == "ok" + mock_run_rendered_script_file.assert_called_once() + mock_run_inline_command.assert_not_called() + @pytest.mark.db_test def test_templated_bash_script(self, dag_maker, create_task_instance_of_operator, tmp_path, session): """ diff --git a/scripts/ci/prek/validate_operators_init_exemptions.txt b/scripts/ci/prek/validate_operators_init_exemptions.txt index 76ba180595a56..8ee379c86366b 100644 --- a/scripts/ci/prek/validate_operators_init_exemptions.txt +++ b/scripts/ci/prek/validate_operators_init_exemptions.txt @@ -30,4 +30,3 @@ providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_gcs.py::GCS providers/google/src/airflow/providers/google/marketing_platform/operators/campaign_manager.py::GoogleCampaignManagerDeleteReportOperator providers/microsoft/azure/src/airflow/providers/microsoft/azure/transfers/gcs_to_wasb.py::GCSToAzureBlobStorageOperator providers/microsoft/psrp/src/airflow/providers/microsoft/psrp/operators/psrp.py::PsrpOperator -providers/standard/src/airflow/providers/standard/operators/bash.py::BashOperator