Skip to content

Commit 509caa8

Browse files
milesjclaude
andauthored
fix: Preserve non-UTF-8 output in captured task logs (#2614)
fix: Preserve non-UTF-8 output in captured task logs. Captured stdout/stderr was converted with a strict `String::from_utf8` falling back to an empty string, so a single invalid byte (e.g. cp1252 output from Python on Windows, as in #2394) erased the entire captured output: empty stdout.log/stderr.log state files, cache hits replaying nothing, and missing output in run reports. Convert lossily instead, matching the hydration and process error paths. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 07639de commit 509caa8

3 files changed

Lines changed: 52 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
task `env` option.
2525
- Fixed an issue where task `outputStyle` was being applied to the primary target. It will only
2626
apply to transitive targets.
27+
- Fixed an issue where captured task output containing non-UTF-8 bytes (for example, Windows
28+
codepage output from Python) was discarded entirely, resulting in empty `stdout.log`/`stderr.log`
29+
state files, cache hits replaying no output, and missing output in run reports. Invalid bytes are
30+
now replaced with `` instead.
2731
- Fixed an issue where a task's dependents would not run when requested with a downstream scope
2832
(`moon ci`, `--dependents`, `--downstream`), if the task was first added to the action graph as a
2933
dependency of another target. For example, `moon run app:build lib:build --dependents` would skip

crates/action/src/operation.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,10 @@ impl Operation {
148148
output.exit_status = Some(status);
149149
}
150150

151-
output.set_stderr(String::from_utf8(stderr).unwrap_or_default());
152-
output.set_stdout(String::from_utf8(stdout).unwrap_or_default());
151+
// Lossy conversion, as output from foreign processes may not
152+
// be UTF-8 (Windows codepages, etc), and we can't lose it all
153+
output.set_stderr(String::from_utf8_lossy(&stderr).into_owned());
154+
output.set_stdout(String::from_utf8_lossy(&stdout).into_owned());
153155
}
154156

155157
self.finish(if success {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use moon_action::Operation;
2+
3+
mod finish_from_output {
4+
use super::*;
5+
6+
#[test]
7+
fn preserves_utf8_output() {
8+
let mut operation = Operation::task_execution("pytest");
9+
10+
operation.finish_from_output(
11+
None,
12+
b"tests passed\n".to_vec(),
13+
b"warning issued\n".to_vec(),
14+
);
15+
16+
let output = operation.get_exec_output().unwrap();
17+
18+
assert_eq!(output.stdout.as_deref().unwrap(), "tests passed\n");
19+
assert_eq!(output.stderr.as_deref().unwrap(), "warning issued\n");
20+
}
21+
22+
#[test]
23+
fn preserves_non_utf8_output_lossily() {
24+
let mut operation = Operation::task_execution("pytest");
25+
26+
// 0xFC is `ü` in Windows codepage 1252, but invalid UTF-8
27+
operation.finish_from_output(
28+
None,
29+
b"person=\"Hans M\xfcller\",\nFAILED tests\n".to_vec(),
30+
b"locator(\"Absenden f\xfcr alle\")\n".to_vec(),
31+
);
32+
33+
let output = operation.get_exec_output().unwrap();
34+
35+
assert_eq!(
36+
output.stdout.as_deref().unwrap(),
37+
"person=\"Hans M\u{fffd}ller\",\nFAILED tests\n"
38+
);
39+
assert_eq!(
40+
output.stderr.as_deref().unwrap(),
41+
"locator(\"Absenden f\u{fffd}r alle\")\n"
42+
);
43+
}
44+
}

0 commit comments

Comments
 (0)