dvc repro: rolls back .gitignore entries for successful stages when a later stage fails
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 15.9k
- Forks
- 1.3k
- PR merge metrics
- No merged PRs in 30d
Description
Bug Report
Description
When dvc repro runs a pipeline with multiple stages and a later stage fails, .gitignore entries added for outputs of earlier successfully completed stages are rolled back.
For comparison, when the same pipeline completes successfully, DVC correctly creates output/.gitignore containing:
/out1
/out2
If stage2 is changed to fail after creating its output, the files are still created, dvc.lock is updated, but the .gitignore entries are not created. After fixing the stage2 command and rerunning dvc repro, the outs of stage2 are correctly added to the .gitignore but stage1 .
The way I fix that now is by manually typing the succesfully run outputs into the .gitignore, which is cumbersome. Maybe there is a command to do it that I don't know about? But anyway, to me it would make more sense if the
Reproduce
Start in an empty Git repository:
$ git init
$ uvx dvc init
Create the following dvc.yaml:
stages:
stage1:
outs: [output/out1]
cmd:
- mkdir -p output
- echo ok > output/out1
stage2:
deps: [output/out1]
outs: [output/out2]
cmd:
- cp output/out1 output/out2
- exit 1
Run:
$ uvx dvc repro dvc.yaml
As expected, dvc repro fails because of exit 1.
Both output files have nevertheless been created:
$ ls output
out1 out2
However, the ignore rules for the outputs are not created in output/.gitignore. When running with -v it states explicitly that the .gitignore entry of out1 is removed. I guess this technically means this is not a bug but intended behavior, but it is counterintuitive and counterproductive for normal use.
As a control, removing the following command from stage2:
- exit 1
and running the same pipeline successfully (in a clean repo state) results in:
/out1
/out2
being added to output/.gitignore.
Expected
The .gitignore entry for output/out1 should be created. stage1 completed successfully, its output remains in the workspace, and the dvc.lock file is updated succesfully. A failure in the downstream stage2 should not roll back Git ignore state belonging to the already completed stage.
Environment information
Output of dvc doctor:
$ uvx dvc doctor
DVC version: 3.67.1 (pip)
-------------------------
Platform: Python 3.14.2 on Linux-7.1.5-76070105-generic-x86_64-with-glibc2.39
Subprojects:
dvc_data = 3.18.3
dvc_objects = 5.2.0
dvc_render = 1.0.2
dvc_task = 0.40.2
scmrepo = 3.6.2
Supports:
http (aiohttp = 3.14.3, aiohttp-retry = 2.9.1),
https (aiohttp = 3.14.3, aiohttp-retry = 2.9.1)
Config:
Global: /home/joostmeulenbeld/.config/dvc
System: /etc/xdg/dvc
Cache types: hardlink, symlink
Cache directory: ext4 on /dev/mapper/data_bb3Cv-root
Caches: local
Remotes: None
Workspace directory: ext4 on /dev/mapper/data_bb3Cv-root
Repo: dvc, git
Repo.site_cache_dir: /var/tmp/dvc/repo/3fb681d8dcf172821499f0051802062e
Additional Information (if any):
`dvc repro -v` run
2026-09-01 14:00:09,035 DEBUG: v3.67.1 (pip), CPython 3.14.2 on Linux-7.1.5-76070105-generic-x86_64-with-glibc2.39
2026-09-01 14:00:09,035 DEBUG: command: /home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/bin/dvc repro -v
2026-09-01 14:00:09,412 DEBUG: Output 'output/out1' of stage: 'stage1' changed because it is 'not in cache'.
2026-09-01 14:00:09,412 DEBUG: stage: 'stage1' changed.
2026-09-01 14:00:09,415 DEBUG: Removing output 'output/out1' of stage: 'stage1'.
2026-09-01 14:00:09,415 DEBUG: Removing '/home/joostmeulenbeld/git/mwe_dvc_noignore/output/out1'
Running stage 'stage1':
> mkdir -p output
> echo ok > output/out1
2026-09-01 14:00:09,428 DEBUG: Added '/home/joostmeulenbeld/git/mwe_dvc_noignore/output/out1' to gitignore file.
2026-09-01 14:00:09,437 DEBUG: Computed stage: 'stage1' md5: 'fc98065f95e8b773dcd45d4d3e4fb0f8'
2026-09-01 14:00:09,441 DEBUG: Preparing to transfer data from 'memory://dvc-staging-md5/cc0bb25b96409904853d0280d6caaf899e5e170c9701e667d661ed11eed04c8e' to '/home/joostmeulenbeld/git/mwe_dvc_noignore/.dvc/cache/files/md5'
2026-09-01 14:00:09,441 DEBUG: Preparing to collect status from '/home/joostmeulenbeld/git/mwe_dvc_noignore/.dvc/cache/files/md5'
2026-09-01 14:00:09,441 DEBUG: Collecting status from '/home/joostmeulenbeld/git/mwe_dvc_noignore/.dvc/cache/files/md5'
2026-09-01 14:00:09,442 DEBUG: Preparing to collect status from 'memory://dvc-staging-md5/cc0bb25b96409904853d0280d6caaf899e5e170c9701e667d661ed11eed04c8e'
2026-09-01 14:00:09,443 DEBUG: link type reflink is not available ([Errno 95] no more link types left to try out)
2026-09-01 14:00:09,443 DEBUG: Removing '/home/joostmeulenbeld/git/mwe_dvc_noignore/output/.svn_L2PF3RMz-pj5F5cSVw.tmp'
2026-09-01 14:00:09,444 DEBUG: Removing '/home/joostmeulenbeld/git/mwe_dvc_noignore/output/.svn_L2PF3RMz-pj5F5cSVw.tmp'
2026-09-01 14:00:09,444 DEBUG: Removing '/home/joostmeulenbeld/git/mwe_dvc_noignore/.dvc/cache/files/md5/.Tsjlf-3HNp_ytWCDNf1QMw.tmp'
2026-09-01 14:00:09,444 DEBUG: Removing '/home/joostmeulenbeld/git/mwe_dvc_noignore/output/out1'
2026-09-01 14:00:09,467 DEBUG: stage: 'stage1' was reproduced
2026-09-01 14:00:09,475 DEBUG: 'cmd' of stage: 'stage2' has changed.
2026-09-01 14:00:09,475 DEBUG: stage: 'stage2' changed.
2026-09-01 14:00:09,476 DEBUG: 'cmd' of stage: 'stage2' has changed.
2026-09-01 14:00:09,476 DEBUG: stage: 'stage2' changed.
2026-09-01 14:00:09,477 DEBUG: Removing output 'output/out2' of stage: 'stage2'.
2026-09-01 14:00:09,477 DEBUG: Removing '/home/joostmeulenbeld/git/mwe_dvc_noignore/output/out2'
2026-09-01 14:00:09,479 DEBUG: 'cmd' of stage: 'stage2' has changed.
Running stage 'stage2':
> cp output/out1 output/out2
> exit 1
2026-09-01 14:00:09,501 DEBUG: Removing '/home/joostmeulenbeld/git/mwe_dvc_noignore/output/out1' from gitignore file.
2026-09-01 14:00:09,502 ERROR: failed to reproduce 'stage2': failed to run: exit 1, exited with 1
Traceback (most recent call last):
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/repo/reproduce.py", line 187, in _reproduce
ret = repro_fn(stage, upstream=upstream, force=force_stage, **kwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/repo/reproduce.py", line 117, in _reproduce_stage
ret = stage.reproduce(**kwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/funcy/decorators.py", line 47, in wrapper
return deco(call, *dargs, **dkwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/decorators.py", line 44, in rwlocked
return call()
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/funcy/decorators.py", line 68, in __call__
return self._func(*self._args, **self._kwargs)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/__init__.py", line 448, in reproduce
self.run(**kwargs)
~~~~~~~~^^^^^^^^^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/funcy/decorators.py", line 47, in wrapper
return deco(call, *dargs, **dkwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/decorators.py", line 44, in rwlocked
return call()
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/funcy/decorators.py", line 68, in __call__
return self._func(*self._args, **self._kwargs)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/__init__.py", line 601, in run
self._run_stage(dry, force, **kwargs)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/funcy/decorators.py", line 47, in wrapper
return deco(call, *dargs, **dkwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/decorators.py", line 44, in rwlocked
return call()
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/funcy/decorators.py", line 68, in __call__
return self._func(*self._args, **self._kwargs)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/__init__.py", line 628, in _run_stage
return run_stage(self, dry, force, **kwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/run.py", line 182, in run_stage
run(stage, dry=dry, run_env=run_env)
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/decorators.py", line 53, in wrapper
ret = f(stage, *args, **kwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/run.py", line 153, in cmd_run
_run(executable, cmd, **kwargs)
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/stage/run.py", line 133, in _run
raise StageCmdFailedError(cmd, p.returncode)
dvc.stage.exceptions.StageCmdFailedError: failed to run: exit 1, exited with 1
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/cli/__init__.py", line 212, in main
ret = cmd.do_run()
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/cli/command.py", line 30, in do_run
return self.run()
~~~~~~~~^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/commands/repro.py", line 11, in run
stages = self.repo.reproduce(**self._common_kwargs, **self._repro_kwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/repo/__init__.py", line 59, in wrapper
return f(repo, *args, **kwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/repo/scm_context.py", line 150, in run
return method(repo, *args, **kw)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/repo/reproduce.py", line 248, in reproduce
return _reproduce(steps, graph=graph, on_error=on_error or "fail", **kwargs)
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/repo/reproduce.py", line 191, in _reproduce
_raise_error(exc, stage)
~~~~~~~~~~~~^^^^^^^^^^^^
File "/home/joostmeulenbeld/.cache/uv/archive-v0/KgpsAQHnaEG055oR/lib/python3.14/site-packages/dvc/repo/reproduce.py", line 155, in _raise_error
raise ReproductionError(f"failed to reproduce{segment} {names}") from exc
dvc.exceptions.ReproductionError: failed to reproduce 'stage2'
2026-09-01 14:00:09,506 DEBUG: Analytics is enabled.
2026-09-01 14:00:09,547 DEBUG: Trying to spawn ['daemon', 'analytics', '/tmp/tmpjk5e55cl', '-v']
2026-09-01 14:00:09,555 DEBUG: Spawned ['daemon', 'analytics', '/tmp/tmpjk5e55cl', '-v'] with pid 112440
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with dvc/repo/reproduce.py and dvc/stage/init.py, following the stage failure path into dvc/stage/run.py. Reproduce the two-stage dvc repro example, then verify that output/.gitignore retains stage1's /out1 entry after stage2 fails; add or update coverage where the repository's existing tests for this path are found.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, python
- Domain
- cli, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100