cross-rs/cross · error
output ` ` contains newlines, consider serializing with…
Error message
output `{tag}` contains newlines, consider serializing with json and deserializing in gha with fromJSON() What it means
gha_output writes a `tag=content` line to the GITHUB_OUTPUT environment file so later GitHub Actions steps can read the value. GitHub Actions environment files cannot contain raw multiline values (actions/toolkit#403), so this function proactively rejects any content containing a newline ('\n') instead of silently producing a corrupted workflow output. The caller is expected to serialize the value as JSON and read it back with fromJSON() in the workflow.
Solutions
- Serialize the multi-line value with serde_json::to_string (or json!([...])) before passing it to gha_output, then in the workflow read it with fromJSON(steps.<step>.outputs.<tag>).
- Join lines with a single-line delimiter (e.g. space or comma) if JSON round-tripping is overkill.
- Split the content into multiple scalar outputs (one gha_output call per line item) if the consumer only needs individual values.
Example fix
// before
let targets = targets.join("\n");
gha_output("targets", &targets)?;
// after
let targets = serde_json::to_string(&targets)?;
gha_output("targets", &targets)?;
// workflow: ${{ fromJSON(steps.build.outputs.targets)[0] }} Defensive patterns
Strategy: validation
Validate before calling
fn gha_output_safe(tag: &str, content: &str) -> cross::Result<()> {
if content.contains('\n') {
eyre::bail!("refusing to write multi-line output `{tag}`; serialize as JSON first");
}
Ok(())
}
// call gha_output_safe(tag, &content)?; before gha_output Type guard
fn is_single_line(s: &str) -> bool { !s.contains('\n') } Prevention
- Always JSON-serialize list/multi-line values before passing to gha_output.
- Add a unit test asserting outputs passed to gha_output contain no '\n'.
- Prefer building outputs from typed structs serialized with serde_json rather than hand-joined strings.
When it happens
Trigger: Calling gha_output(tag, content) with a content string that contains a '\n' character, e.g. multi-line tool output, a multi-line version list, or text built with format! over several lines.
Common situations: A maintainer adds a CI task (via the ci, build_docker_image, or run xtask commands) that emits multi-line output such as a list of matrix entries, test names, or docker tags; locally or in a runner the collected content ends up multiline and the xtask step fails.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- expected GHA envfile to exist
- Refusing to push without tag or branch. Specify a…
- no valid choice to pick for image name
- most common base image is
- pr should be a number, got
AI-assisted analysis of cross-rs/cross@8c1a8aa4b6 (2026-09-13).
Data as JSON: /api/errors/1ee7694ce171bb4a.
Report an issue: GitHub.
Appendix: source
Thrown at xtask/src/util.rs:322
eprintln!($fmt $(,$args)*);
};
}
// note: for GHA actions we need to output these tags no matter the verbosity level
pub fn gha_print(content: &str) {
gha_output!("{}", content);
}
// note: for GHA actions we need to output these tags no matter the verbosity level
pub fn gha_error(content: &str) {
gha_output!("::error {}", content);
}
#[track_caller]
pub fn gha_output(tag: &str, content: &str) -> cross::Result<()> {
if content.contains('\n') {
// https://github.com/actions/toolkit/issues/403
eyre::bail!(
"output `{tag}` contains newlines, consider serializing with json and deserializing in gha with fromJSON()"
);
}
write_to_gha_env_file("GITHUB_OUTPUT", &format!("{tag}={content}"))?;
Ok(())
}
pub fn read_dockerfiles(msg_info: &mut MessageInfo) -> cross::Result<Vec<(PathBuf, String)>> {
let root = project_dir(msg_info)?;
let docker = root.join("docker");
let mut dockerfiles = vec![];
for entry in fs::read_dir(docker)? {
let entry = entry?;
let file_type = entry.file_type()?;
let file_name = entry.file_name();
if file_type.is_file() && file_name.to_utf8()?.starts_with("Dockerfile") {
let contents = fs::read_to_string(entry.path())?;
dockerfiles.push((entry.path().to_path_buf(), contents));View on GitHub (pinned to 8c1a8aa4b6)