Skip to content

fix(task): invalidate dependent task sources when dependency runs#8975

Merged
jdx merged 6 commits intomainfrom
fix/task-deps-invalidate-sources
Apr 9, 2026
Merged

fix(task): invalidate dependent task sources when dependency runs#8975
jdx merged 6 commits intomainfrom
fix/task-deps-invalidate-sources

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 9, 2026

Summary

  • When a task with sources depends on another task that also has sources, and the dependency actually runs (its sources changed), the dependent task now also runs even if its own sources haven't changed
  • Only dependencies with sources defined propagate invalidation — sourceless deps (which always run) do not trigger downstream re-runs, so sources on dependents remain useful
  • Tracks which tasks actually executed (vs skipped due to fresh sources) in the dependency graph, and uses this to bypass source freshness checks on downstream tasks

Fixes #8960

Test plan

  • New e2e test test_task_dep_invalidates_sources covers:
    • Core scenario: dep sources change → both dep and dependent re-run
    • Edge case: sourceless dep always runs but does NOT invalidate dependent
    • Edge case: sourceless dependent always runs regardless of dep status
  • Existing e2e tests pass (sources, depends, monorepo tests)
  • Manual test with monorepo tsc setup as described in the discussion

🤖 Generated with Claude Code


Note

Medium Risk
Changes task scheduling/skip logic for sources-based caching, which can affect when tasks execute or are skipped in complex dependency graphs. Risk is mitigated by new end-to-end coverage, but may surface edge cases with nested/parallel dependencies.

Overview
Fixes sources caching behavior so a task with sources will re-run when any of its direct dependencies with sources actually ran (rather than being skipped as up-to-date).

Implements this by tracking which tasks truly executed vs were skipped in Deps and passing a dep_ran flag into TaskExecutor::run_task_sched to bypass sources_are_fresh checks when needed; sourceless dependencies (which always run) do not trigger invalidation. Adds an e2e regression test and updates docs to describe the new dependency invalidation semantics.

Reviewed by Cursor Bugbot for commit 1491ab8. Bugbot is set up for automated code reviews on this repo. Configure here.

When a task with `sources` depends on another task, and the dependency
actually runs (its own sources changed), the dependent task now also
runs even if its own sources haven't changed. This fixes monorepo
workflows where downstream tasks should re-run when upstream tasks
are invalidated.

Closes #8960

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

This PR implements transitive source invalidation for mise task dependencies: when a task with sources actually runs (not skipped), downstream tasks that depend on it will also re-run even if their own sources are unchanged. The implementation adds a ran set and dep_edges map to Deps, marks tasks as ran post-execution, and passes a dep_ran boolean to bypass the freshness check for downstream tasks.

The logic is sound — mark_ran is always called before deps.remove, so downstream tasks (scheduled via emit_leaves inside remove) always see accurate ran state. Propagation through multi-hop chains works naturally: if B runs because A ran, B gets marked ran, which then invalidates C. The design choice to exclude sourceless tasks from propagation is well-reasoned and correctly implemented.

Confidence Score: 5/5

Safe to merge — logic is correct, well-tested, and the race-free ordering of mark_ran → remove → emit_leaves is preserved.

All findings are P2 or lower. The core invariant (mark_ran before remove/emit_leaves) is correctly maintained, dep_ran is read atomically with completed under the same mutex acquisition, and transitive propagation works naturally through the chain. The e2e test covers the three key scenarios.

No files require special attention.

Vulnerabilities

No security concerns identified. The changes are purely internal scheduling logic — no user input surfaces, no new I/O, no credential handling.

Important Files Changed

Filename Overview
src/task/deps.rs Adds ran set and dep_edges map with mark_ran / any_dep_ran methods; edge-building correctly restricted to pre-deps only.
src/cli/run.rs Reads dep_ran and completed atomically under the mutex before spawning the task body, then conditionally calls mark_ran after execution — ordering relative to deps.remove is correct.
src/task/task_executor.rs Signature change to return Result<bool> and accept dep_ran: bool; freshness bypass condition !dep_ran is correctly placed before the async sources_are_fresh call.
e2e/tasks/test_task_dep_invalidates_sources New e2e test covering the core scenario and two edge cases (sourceless dep, sourceless dependent); uses standard assertion helpers.
docs/tasks/task-configuration.md Adds a clear "Dependency invalidation" subsection with a realistic monorepo example explaining the new behavior.

Sequence Diagram

sequenceDiagram
    participant Scheduler
    participant DepTask as Dep Task (has sources)
    participant Deps
    participant DownstreamTask as Downstream Task (has sources)

    Scheduler->>Deps: emit_leaves() → schedules DepTask
    Scheduler->>DepTask: spawn_sched_job

    DepTask->>Deps: mark_executed(dep)
    DepTask->>DepTask: run_task_sched(dep_ran=false)
    Note over DepTask: sources changed → runs → returns Ok(true)
    DepTask->>Deps: mark_ran(dep)  [sources non-empty]
    DepTask->>Deps: remove(dep) → emit_leaves() → schedules DownstreamTask

    Scheduler->>DownstreamTask: spawn_sched_job
    DownstreamTask->>Deps: mark_executed(downstream)
    DownstreamTask->>Deps: any_dep_ran(downstream) → true
    DownstreamTask->>DownstreamTask: run_task_sched(dep_ran=true)
    Note over DownstreamTask: dep_ran=true → skips sources_are_fresh → runs
    DownstreamTask->>Deps: mark_ran(downstream) [sources non-empty]
    DownstreamTask->>Deps: remove(downstream)
Loading

Reviews (5): Last reviewed commit: "docs(task): document dependency source i..." | Re-trigger Greptile

jdx and others added 2 commits April 9, 2026 14:38
Covers: dep with no sources (always runs) invalidates dependent,
and dep with sources skipped doesn't affect sourceless dependent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tasks without sources always run but should not invalidate their
dependents' source freshness checks. Only tasks that have sources
defined and actually ran (because those sources changed) should
trigger downstream invalidation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the task execution logic to ensure that dependent tasks are correctly invalidated when their upstream dependencies execute, even if the dependent task's own sources have not changed. This is achieved by tracking which tasks actually ran and propagating this state through the dependency graph. I have reviewed the suggested changes and agree with the feedback regarding performance optimization by hoisting the task key calculation and the functional requirement to include post-dependencies in the dependency tracking map.

Comment thread src/task/deps.rs
Comment on lines +95 to +98
dep_edges
.entry(task_key(&a))
.or_default()
.insert(task_key(&b));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There are two points for improvement in this section:

  1. Performance Inefficiency: task_key(&a) is called repeatedly inside the for b in pre loop. Since a is constant for the duration of the loop, its key should be computed once before the loop starts to avoid redundant string cloning and sorting operations.

  2. Functional Omission for Post-Dependencies: The dep_edges map is currently only updated for pre dependencies. However, post dependencies (tasks that run after the current task) also logically depend on the parent task. To ensure that a post-dependency task's sources are correctly invalidated when its parent task runs, you should also update dep_edges inside the for b in post loop (mapping b to a). Without this, the new invalidation logic will not work for tasks defined in depends_post.

@jdx jdx merged commit 015ceb8 into main Apr 9, 2026
38 checks passed
@jdx jdx deleted the fix/task-deps-invalidate-sources branch April 9, 2026 15:18
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 9, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.7 x -- echo 25.2 ± 1.2 23.9 38.0 1.00
mise x -- echo 25.4 ± 0.5 24.2 26.8 1.01 ± 0.05

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.7 env 24.3 ± 0.7 23.1 30.9 1.00
mise env 24.7 ± 0.9 23.5 34.3 1.02 ± 0.05

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.7 hook-env 25.8 ± 0.9 24.4 40.4 1.01 ± 0.04
mise hook-env 25.5 ± 0.5 24.7 27.3 1.00

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.7 ls 22.2 ± 0.4 21.3 23.6 1.00
mise ls 23.0 ± 1.1 21.8 35.6 1.04 ± 0.05

xtasks/test/perf

Command mise-2026.4.7 mise Variance
install (cached) 158ms 157ms +0%
ls (cached) 82ms 82ms +0%
bin-paths (cached) 89ms 87ms +2%
task-ls (cached) 810ms 802ms +0%

mise-en-dev added a commit that referenced this pull request Apr 10, 2026
### 🚀 Features

- **(config)** add lockfile_platforms setting to restrict lockfile
platforms by @cameronbrill in
[#8966](#8966)
- **(sandbox)** support wildcard patterns in allow_env by @jdx in
[#8974](#8974)
- bump usage-lib v2 → v3 to render examples in task --help by @baby-joel
in [#8890](#8890)

### 🐛 Bug Fixes

- **(activate)** handle empty __MISE_FLAGS array with set -u on bash 3.2
by @jdx in [#8988](#8988)
- **(env)** add trace logging for module hook PATH diagnostics by @jdx
in [#8981](#8981)
- **(go)** Query module proxy directly for version resolution by @c22 in
[#8968](#8968)
- **(install)** render tera templates in tool postinstall hooks by @jdx
in [#8978](#8978)
- **(install)** add missing env vars to tool postinstall hooks by @jdx
in [#8977](#8977)
- **(task)** prevent hang when skipped task has dependents by @jdx in
[#8937](#8937)
- **(task)** invalidate dependent task sources when dependency runs by
@jdx in [#8975](#8975)
- **(task)** prevent deadlock when MISE_JOBS=1 with sub-task references
by @jdx in [#8976](#8976)
- **(task)** fetch remote task files before parsing usage specs by @jdx
in [#8979](#8979)
- **(task)** prevent panic when running parallel sub-tasks with
replacing output by @jdx in
[#8986](#8986)
- **(upgrade)** update lockfile and config when upgrading to specific
version by @jdx in [#8983](#8983)

### 📚 Documentation

- **(node)** remove "recommended for teams" from pin example by @jdx in
[b334363](b334363)

### 📦️ Dependency Updates

- update ghcr.io/jdx/mise:alpine docker digest to 17a29f2 by
@renovate[bot] in [#8995](#8995)
- update docker/dockerfile:1 docker digest to 2780b5c by @renovate[bot]
in [#8994](#8994)

### New Contributors

- @baby-joel made their first contribution in
[#8890](#8890)
- @cameronbrill made their first contribution in
[#8966](#8966)
- @c22 made their first contribution in
[#8968](#8968)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant