Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TASK_CACHE_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ outside this tracker.
- [x] Include dependency artifact keys in downstream task keys
- [x] Allow dependents to restore when dependencies publish stable artifact keys;
unkeyed dependency work still forces execution
- [ ] Capture and replay stdout and stderr while respecting mise output modes
- [x] Capture and replay stdout and stderr while respecting mise output modes
- [ ] Cache useful results for tasks with no filesystem outputs
- [ ] Support reusable and global input groups
- [ ] Support global environment inputs and pass-through environment variables
Expand Down
14 changes: 9 additions & 5 deletions docs/tasks/task-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,15 +495,19 @@ the values (or absence) of variables named in `cache.env`, resolved tool version
artifact keys, and the operating system and architecture. Variables inherited from the ambient
process are ignored unless listed in `cache.env`.

Artifacts are stored under `MISE_CACHE_DIR/task-artifacts/v1` as zstd-compressed tar archives. They
Artifacts are stored under `MISE_CACHE_DIR/task-artifacts/v2` as zstd-compressed tar archives. They
are included in the existing `mise cache clear` and `mise cache prune` behavior. Only successful task
runs are cached. Cache read/write failures are treated as misses and never turn a successful task run
into a failure.

This initial implementation is local-only and does not cache or replay task logs. Cacheable
dependencies contribute their artifact keys to dependent task keys, so a dependent can restore the
matching artifact after its dependencies execute, skip, or restore. If a dependency executes without
a stable artifact key, its dependents conservatively execute.
Stdout and stderr are stored as ordered, redacted streams and replayed using the output mode selected
for the cache hit. Prefix, interleave, keep-order, timed, replacing, quiet, silent, and per-stream
silence therefore apply to replayed output just as they do to live output. Raw and interactive tasks
retain inherited terminal I/O and conservatively bypass artifact caching.

Cacheable dependencies contribute their artifact keys to dependent task keys, so a dependent can
restore the matching artifact after its dependencies execute, skip, or restore. If a dependency
executes without a stable artifact key, its dependents conservatively execute.

### `shell`

Expand Down
139 changes: 139 additions & 0 deletions e2e/tasks/test_task_artifact_cache_output
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env bash

cat <<'EOF' >mise.toml
redactions = ["SECRET"]

[settings]
experimental = true

[env]
SECRET = "cache-secret"

[task_config.cache]
enabled = true

[tasks.build]
run = '''
mkdir -p dist
printf 'stdout-line\n'
printf 'stderr-line\n' >&2
printf 'secret=%s\n' "$SECRET"
printf 'artifact\n' > dist/result.txt
printf 'ran\n' >> runs.txt
'''
sources = ["input.txt"]
outputs = ["dist"]

[tasks.partial]
run = '''
mkdir -p partial-dist
printf 'partial-stdout\n'
printf 'partial-stderr\n' >&2
printf 'artifact\n' > partial-dist/result.txt
printf 'ran\n' >> partial-runs.txt
'''
sources = ["partial.txt"]
outputs = ["partial-dist"]
silent = "stdout"

[tasks.raw]
run = '''
mkdir -p raw-dist
printf 'raw-output\n'
printf 'artifact\n' > raw-dist/result.txt
printf 'ran\n' >> raw-runs.txt
'''
sources = ["raw.txt"]
outputs = ["raw-dist"]
raw = true

[tasks.hidden-first]
run = '''
mkdir -p hidden-dist
printf 'hidden-stdout\n'
printf 'hidden-stderr\n' >&2
printf 'artifact\n' > hidden-dist/result.txt
printf 'ran\n' >> hidden-runs.txt
'''
sources = ["hidden.txt"]
outputs = ["hidden-dist"]
EOF

printf 'input\n' >input.txt
printf 'partial\n' >partial.txt
printf 'raw\n' >raw.txt
printf 'hidden\n' >hidden.txt

# Capture streams during a normal execution.
output="$(mise run --output prefix build 2>&1)"
assert_contains "echo \"$output\"" "stdout-line"
assert_contains "echo \"$output\"" "stderr-line"
assert_contains "echo \"$output\"" "secret=[redacted]"
assert_not_contains "echo \"$output\"" "cache-secret"
assert "wc -l < runs.txt | tr -d ' '" "1"

# A fresh-output cache hit also replays the stored streams.
output="$(mise run --output prefix build 2>&1)"
assert_contains "echo \"$output\"" "sources up-to-date, skipping"
assert_contains "echo \"$output\"" "[build] stdout-line"
assert_contains "echo \"$output\"" "[build] stderr-line"
assert_contains "echo \"$output\"" "[build] secret=[redacted]"
assert_not_contains "echo \"$output\"" "cache-secret"
assert "wc -l < runs.txt | tr -d ' '" "1"

# Replay uses the current prefix style rather than storing formatted lines.
rm -rf dist
output="$(mise run --output prefix build 2>&1)"
assert_contains "echo \"$output\"" "[build] stdout-line"
assert_contains "echo \"$output\"" "[build] stderr-line"
assert_contains "echo \"$output\"" "restored outputs from cache"
assert "wc -l < runs.txt | tr -d ' '" "1"

# The same cached streams can be replayed without prefixes in interleave mode.
rm -rf dist
output="$(mise run --output interleave build 2>&1)"
assert_contains "echo \"$output\"" "stdout-line"
assert_contains "echo \"$output\"" "stderr-line"
assert_not_contains "echo \"$output\"" "[build] stdout-line"
assert_not_contains "echo \"$output\"" "[build] stderr-line"
assert "wc -l < runs.txt | tr -d ' '" "1"

# Quiet hides mise metadata but still replays task output using the chosen style.
rm -rf dist
output="$(mise run --quiet --output prefix build 2>&1)"
assert_contains "echo \"$output\"" "[build] stdout-line"
assert_contains "echo \"$output\"" "[build] stderr-line"
assert_not_contains "echo \"$output\"" "restored outputs from cache"
assert "wc -l < runs.txt | tr -d ' '" "1"

# Silent mode suppresses both cached streams.
rm -rf dist
output="$(mise run --output silent build 2>&1)"
assert "echo -n \"$output\"" ""
assert "wc -l < runs.txt | tr -d ' '" "1"

# Per-stream silence is also applied during replay.
output="$(mise run partial 2>&1)"
assert_not_contains "echo \"$output\"" "partial-stdout"
assert_contains "echo \"$output\"" "partial-stderr"
rm -rf partial-dist
output="$(mise run partial 2>&1)"
assert_not_contains "echo \"$output\"" "partial-stdout"
assert_contains "echo \"$output\"" "partial-stderr"
assert "wc -l < partial-runs.txt | tr -d ' '" "1"

# Streams captured during a silent execution remain available to a later
# cache hit with visible output.
output="$(mise run --output silent hidden-first 2>&1)"
assert "echo -n \"$output\"" ""
rm -rf hidden-dist
output="$(mise run --output prefix hidden-first 2>&1)"
assert_contains "echo \"$output\"" "[hidden-first] hidden-stdout"
assert_contains "echo \"$output\"" "[hidden-first] hidden-stderr"
assert "wc -l < hidden-runs.txt | tr -d ' '" "1"

# Raw tasks retain inherited stdio and conservatively bypass artifact caching.
mise run raw
rm -rf raw-dist
mise run raw
assert "wc -l < raw-runs.txt | tr -d ' '" "2"
43 changes: 43 additions & 0 deletions e2e/tasks/test_task_artifact_cache_resilience
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash

cat <<'EOF' >mise.toml
[settings]
experimental = true

[task_config.cache]
enabled = true

[tasks.build]
run = '''
mkdir -p dist
printf 'artifact\n' > dist/result.txt
printf 'ran\n' >> runs.txt
'''
sources = ["input.txt"]
outputs = ["dist"]

[tasks.stdin]
run = '''
mkdir -p stdin-dist
IFS= read -r line
printf '%s\n' "$line" > stdin-dist/result.txt
'''
sources = ["stdin.txt"]
outputs = ["stdin-dist"]
output = "interleave"
EOF

printf 'input\n' >input.txt
printf 'stdin\n' >stdin.txt

# A corrupt current manifest is a cache miss, so the task executes instead of
# reporting a successful no-work hit without replaying output.
mise run build
manifest="$(find "$MISE_CACHE_DIR/task-artifacts/v2" -type f -name '*.json' -print -quit)"
printf 'invalid manifest\n' >"$manifest"
mise run build
assert "wc -l < runs.txt | tr -d ' '" "2"

# Capturing output for a cacheable interleave task must not disconnect stdin.
printf 'from-stdin\n' | mise run stdin
assert "cat stdin-dist/result.txt" "from-stdin"
12 changes: 7 additions & 5 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,12 +758,14 @@ impl Run {
{
let mut outputs = timed_outputs.lock().unwrap();
for (prefix, out) in outputs.clone() {
let (time, line) = out;
let (time, lines) = out;
if time.elapsed().unwrap().as_secs() >= 1 {
if console::colors_enabled() {
prefix_println!(prefix, "{line}\x1b[0m");
} else {
prefix_println!(prefix, "{line}");
for line in lines {
if console::colors_enabled() {
prefix_println!(prefix, "{line}\x1b[0m");
} else {
prefix_println!(prefix, "{line}");
}
}
outputs.shift_remove(&prefix);
}
Expand Down
34 changes: 34 additions & 0 deletions src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ where
duct::cmd(program, args)
}

type OutputObserver<'a> = Box<dyn Fn(&str) + Send + 'a>;

pub struct CmdLineRunner<'a> {
cmd: Command,
pr: Option<&'a dyn SingleReport>,
Expand All @@ -113,6 +115,8 @@ pub struct CmdLineRunner<'a> {
pass_signals: bool,
on_stdout: Option<Box<dyn Fn(String) + Send + 'a>>,
on_stderr: Option<Box<dyn Fn(String) + Send + 'a>>,
observe_stdout: Option<OutputObserver<'a>>,
observe_stderr: Option<OutputObserver<'a>>,
timeout: Option<Duration>,
sandbox: Option<crate::sandbox::SandboxConfig>,
}
Expand Down Expand Up @@ -328,6 +332,8 @@ impl<'a> CmdLineRunner<'a> {
pass_signals: false,
on_stdout: None,
on_stderr: None,
observe_stdout: None,
observe_stderr: None,
timeout: None,
sandbox: None,
}
Expand Down Expand Up @@ -414,6 +420,16 @@ impl<'a> CmdLineRunner<'a> {
self
}

pub(crate) fn with_stdout_observer<F: Fn(&str) + Send + 'a>(mut self, observer: F) -> Self {
self.observe_stdout = Some(Box::new(observer));
self
}

pub(crate) fn with_stderr_observer<F: Fn(&str) + Send + 'a>(mut self, observer: F) -> Self {
self.observe_stderr = Some(Box::new(observer));
self
}

pub fn current_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
self.cmd.current_dir(dir);
self
Expand Down Expand Up @@ -1211,6 +1227,9 @@ impl<'a> CmdLineRunner<'a> {

fn on_stdout(&self, line: String) {
let _lock = OUTPUT_LOCK.lock().unwrap();
if let Some(observer) = &self.observe_stdout {
observer(&line);
}
if let Some(on_stdout) = &self.on_stdout {
on_stdout(line);
return;
Expand All @@ -1234,6 +1253,9 @@ impl<'a> CmdLineRunner<'a> {

fn on_stderr(&self, line: String) {
let _lock = OUTPUT_LOCK.lock().unwrap();
if let Some(observer) = &self.observe_stderr {
observer(&line);
}
if let Some(on_stderr) = &self.on_stderr {
on_stderr(line);
return;
Expand Down Expand Up @@ -1472,17 +1494,29 @@ mod tests {
async fn test_cmd_line_runner_execute_async() {
let stdout = Arc::new(Mutex::new(Vec::new()));
let stderr = Arc::new(Mutex::new(Vec::new()));
let observed_stdout = Arc::new(Mutex::new(Vec::new()));
let observed_stderr = Arc::new(Mutex::new(Vec::new()));
let stdout_clone = stdout.clone();
let stderr_clone = stderr.clone();
let observed_stdout_clone = observed_stdout.clone();
let observed_stderr_clone = observed_stderr.clone();
super::CmdLineRunner::new("sh")
.args(["-c", "printf out; printf err >&2"])
.with_on_stdout(move |line| stdout_clone.lock().unwrap().push(line))
.with_on_stderr(move |line| stderr_clone.lock().unwrap().push(line))
.with_stdout_observer(move |line| {
observed_stdout_clone.lock().unwrap().push(line.to_string());
})
.with_stderr_observer(move |line| {
observed_stderr_clone.lock().unwrap().push(line.to_string());
})
.execute_async()
.await
.unwrap();
assert_eq!(stdout.lock().unwrap().as_slice(), ["out"]);
assert_eq!(stderr.lock().unwrap().as_slice(), ["err"]);
assert_eq!(observed_stdout.lock().unwrap().as_slice(), ["out"]);
assert_eq!(observed_stderr.lock().unwrap().as_slice(), ["err"]);
}

#[tokio::test]
Expand Down
4 changes: 2 additions & 2 deletions src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ pub mod task_sources;
pub mod task_template;
pub mod task_tool_installer;

pub use task_cache::TaskArtifactCache;
pub use task_cache::TaskCacheConfig;
pub(crate) use task_cache::TaskCacheOutput;
pub use task_cache::{TaskArtifactCache, TaskCacheConfig};
pub use task_confirm::TaskConfirm;
pub(crate) use task_load_context::monorepo_scope;
pub use task_load_context::{TaskLoadContext, expand_colon_task_syntax};
Expand Down
Loading