From 9f525db23429ce71998e6e68a53d2d7c4bdc2b3d Mon Sep 17 00:00:00 2001 From: arose26 Date: Tue, 18 Aug 2026 06:22:50 -0400 Subject: [PATCH] Don't reproduce frozen stages with --force Forcing reproduction rewrote a frozen stage's dependency hashes in dvc.lock even though its command is never re-run, so the lock stopped recording what the stage was actually built against. --- dvc/stage/__init__.py | 7 +++++++ tests/func/repro/test_repro.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/dvc/stage/__init__.py b/dvc/stage/__init__.py index 6c3d290d3f..85e3c314d0 100644 --- a/dvc/stage/__init__.py +++ b/dvc/stage/__init__.py @@ -422,6 +422,13 @@ def reproduce(self, interactive=False, **kwargs) -> Optional["Stage"]: allow_missing = kwargs.get("allow_missing", False) pull = kwargs.get("pull", False) upstream = kwargs.pop("upstream", None) + if self.frozen and not self.is_import: + # `dvc freeze` documents a frozen stage as always treated as + # unchanged, so `--force` must not reproduce it. The command is + # skipped in `run()` either way, but reproducing re-saves the stage + # and rewrites its dependency hashes in dvc.lock. Falling through to + # the checks below still lets it restore missing outputs. + force = False if force: pass # Skip stages with missing data if otherwise unchanged diff --git a/tests/func/repro/test_repro.py b/tests/func/repro/test_repro.py index 6806f62fa4..4b86d2ae86 100644 --- a/tests/func/repro/test_repro.py +++ b/tests/func/repro/test_repro.py @@ -17,7 +17,7 @@ from dvc.stage.exceptions import StageFileDoesNotExistError, StageNotFound from dvc.testing import matchers as M from dvc.utils.fs import remove -from dvc.utils.serialize import modify_yaml +from dvc.utils.serialize import load_yaml, modify_yaml from dvc_data.hashfile.hash import file_md5 @@ -56,6 +56,21 @@ def test_repro_frozen(tmp_dir, dvc, run_copy): assert stages == [data_stage, stage0] +def test_repro_frozen_force(tmp_dir, dvc, run_copy): + """Check that `--force` doesn't rewrite a frozen stage's dependency hashes""" + tmp_dir.dvc_gen("data", "foo") + run_copy("data", "stage0", name="copy-data-stage0") + run_copy("stage0", "stage1", name="copy-data-stage1") + + dvc.freeze("copy-data-stage1") + frozen_before = load_yaml("dvc.lock")["stages"]["copy-data-stage1"] + + tmp_dir.gen("stage0", "bar") + + assert dvc.reproduce("copy-data-stage1", force=True) == [] + assert load_yaml("dvc.lock")["stages"]["copy-data-stage1"] == frozen_before + + def test_downstream(tmp_dir, dvc): # The dependency graph should look like this: #