Force LF line breaks in DAP YAML emitters

`StringWriter` defaults to `Environment.NewLine`, which is CRLF on
Windows. YamlDotNet's `Emitter` calls `WriteLine` internally, so the
emitted YAML mixes CRLF (from the emitter) with the explicit `\n` we
append in the renderer skeleton. That breaks the document-end
stripping in `FormatScalar` and `TemplateTokenYamlAdapter.Serialize`
and corrupts the rendered view on Windows.

Set `sw.NewLine = "\n"` in both emitter entry points so the output
is always LF, regardless of host. Add regression tests asserting no
`\r` appears in the rendered view or in adapter output. The tests
are noop guards on Linux (where `Environment.NewLine` is already
\n) but catch any future regression on the Windows CI matrix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Francesco Renzi
2026-05-13 03:18:06 -07:00
committed by GitHub
parent e51d0dfba7
commit fedd9a3089
4 changed files with 53 additions and 0 deletions

View File

@@ -367,6 +367,11 @@ namespace GitHub.Runner.Worker.Dap
}
using var sw = new StringWriter(CultureInfo.InvariantCulture);
// Force LF line breaks; YamlDotNet's Emitter calls WriteLine,
// which would otherwise produce CRLF on Windows and break
// both our document-end stripping below and downstream
// consumers that assume a single line-break convention.
sw.NewLine = "\n";
var emitter = new Emitter(sw);
emitter.Emit(new StreamStart());
emitter.Emit(new DocumentStart(null, null, true));

View File

@@ -95,6 +95,11 @@ namespace GitHub.Runner.Worker.Dap
}
using var sw = new StringWriter(CultureInfo.InvariantCulture);
// Force LF line breaks; YamlDotNet's Emitter calls WriteLine,
// which would otherwise produce CRLF on Windows and corrupt
// both the document-end stripping below and the per-line
// indentation pass that follows.
sw.NewLine = "\n";
var emitter = new Emitter(sw);
var adapter = new TemplateTokenYamlAdapter(emitter);
adapter.WriteStart();

View File

@@ -613,5 +613,31 @@ namespace GitHub.Runner.Common.Tests.Worker
// entry gets an inline skipped annotation.
Assert.Equal(unmarked.EntryStartLines[1], marked.EntryStartLines[1]);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Render_AlwaysUsesLfLineBreaks()
{
// Regression: YamlDotNet's Emitter calls WriteLine, which on
// Windows produces CRLF (the host's Environment.NewLine).
// FormatScalar / TemplateTokenYamlAdapter.Serialize must force
// LF so the rendered view round-trips regardless of platform.
var entry = new JobExecutionViewEntry(JobExecutionPhase.Main, "with: colon", id: "step-1", uses: "actions/checkout@v4");
var result = JobExecutionViewRenderer.Render("job-1", new[] { entry });
Assert.DoesNotContain("\r", result.Yaml);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void FormatScalar_AlwaysUsesLfLineBreaks()
{
// Direct check on FormatScalar to guard against future refactors
// that bypass the full Render path but still emit through
// YamlDotNet.
Assert.DoesNotContain("\r", JobExecutionViewRenderer.FormatScalar("with: colon"));
Assert.DoesNotContain("\r", JobExecutionViewRenderer.FormatScalar("hello"));
}
}
}

View File

@@ -170,5 +170,22 @@ namespace GitHub.Runner.Common.Tests.Worker
string yaml = TemplateTokenYamlAdapter.Serialize(token, 0);
Assert.False(yaml.EndsWith("\n"));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Serialize_AlwaysUsesLfLineBreaks()
{
// Regression: YamlDotNet's Emitter calls WriteLine, which on
// Windows produces CRLF (the host's Environment.NewLine).
// Serialize must force LF so the rendered view round-trips
// regardless of platform.
var map = new MappingToken(null, null, null);
map.Add(Str("k1"), Str("v1"));
map.Add(Str("k2"), Num(2));
map.Add(Str("k3"), Bool(true));
string yaml = TemplateTokenYamlAdapter.Serialize(map, indentSpaces: 2);
Assert.DoesNotContain("\r", yaml);
}
}
}