treeverse / treeverse/dvc

dvc repro: rolls back .gitignore entries for successful stages when a later stage fails

Open
#11,091 0 comments 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.