This is an automated email from the ASF dual-hosted git repository.
potiuk 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 e5f859778c5 [v3-3-test] Stop variables export/import from silently
corrupting values (#70944) (#71791)
e5f859778c5 is described below
commit e5f859778c56b38d71829e835db8d0126c04d2eb
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Aug 30 02:01:49 2026 +0200
[v3-3-test] Stop variables export/import from silently corrupting values
(#70944) (#71791)
---
.../src/airflow/cli/commands/variable_command.py | 5 +++-
.../unit/cli/commands/test_variable_command.py | 34 ++++++++++++++++++++++
2 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/airflow-core/src/airflow/cli/commands/variable_command.py
b/airflow-core/src/airflow/cli/commands/variable_command.py
index 194b02b529a..8cdf5490d3a 100644
--- a/airflow-core/src/airflow/cli/commands/variable_command.py
+++ b/airflow-core/src/airflow/cli/commands/variable_command.py
@@ -186,11 +186,14 @@ def variables_export(args):
data = json.JSONDecoder()
for var in qry:
+ # Mirror variables_import's reconstruction so export/import
round-trips.
try:
val = data.decode(var.val)
except Exception:
val = var.val
- if var.description:
+ if isinstance(val, str):
+ val = var.val
+ if var.description or (isinstance(val, dict) and "value" in val):
var_dict[var.key] = {
"value": val,
"description": var.description,
diff --git a/airflow-core/tests/unit/cli/commands/test_variable_command.py
b/airflow-core/tests/unit/cli/commands/test_variable_command.py
index a02c95aa317..ac8eb11a3ae 100644
--- a/airflow-core/tests/unit/cli/commands/test_variable_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_variable_command.py
@@ -360,6 +360,40 @@ class TestCliVariables:
"""Test variables_export command"""
variable_command.variables_export(self.parser.parse_args(["variables",
"export", os.devnull]))
+ @pytest.mark.parametrize(
+ ("stored_value", "expected_export"),
+ [
+ pytest.param(
+ '{"value": "a", "description": "b"}',
+ {"value": {"value": "a", "description": "b"}, "description":
None},
+ id="envelope_lookalike",
+ ),
+ pytest.param(
+ '{"value": 1, "other": 2}',
+ {"value": {"value": 1, "other": 2}, "description": None},
+ id="envelope_lookalike_with_extra_keys",
+ ),
+ pytest.param('"hello"', '"hello"', id="json_string"),
+ ],
+ )
+ def test_variables_export_survives_reimport(self, tmp_path, stored_value,
expected_export):
+ """Values that collide with the export format must round-trip through
export/import."""
+ path = tmp_path / "variables.json"
+ variable_command.variables_set(self.parser.parse_args(["variables",
"set", "k", stored_value]))
+ variable_command.variables_export(self.parser.parse_args(["variables",
"export", os.fspath(path)]))
+
+ assert json.loads(path.read_text()) == {"k": expected_export}
+
+ variable_command.variables_delete(self.parser.parse_args(["variables",
"delete", "k"]))
+ with create_session() as session:
+ variable_command.variables_import(
+ self.parser.parse_args(["variables", "import",
os.fspath(path)]), session=session
+ )
+
+ assert Variable.get("k", deserialize_json=True) ==
json.loads(stored_value)
+ with create_session() as session:
+ assert
session.scalar(select(Variable.description).where(Variable.key == "k")) is None
+
def test_variables_isolation(self, tmp_path):
"""Test isolation of variables"""
path1 = tmp_path / "testfile1.json"