fix: keep logged package publish metadata on a single line (#3447)

The resolve step echoes the publish command with shlex.quote, which is shell
quoting rather than output escaping: it wraps a value holding a line break in
single quotes and leaves the break itself intact. A caller-supplied changelog,
categories or topics value carrying a newline therefore opened a second line
in the step log, and the runner parses each stdout line, so that second line
reached it as a workflow command.

Escape the parts that are not printable in the echo. The re-runnable .sh file
keeps plain shell quoting, because there the quoting is what makes the script
correct.
This commit is contained in:
Yiğit ERDOĞAN
2026-08-11 10:52:24 -07:00
committed by GitHub
parent 8b31a7e6e1
commit 8bf424cff1
2 changed files with 31 additions and 1 deletions
+12 -1
View File
@@ -363,6 +363,15 @@ jobs:
from urllib.parse import quote, urlparse
from urllib.request import Request, urlopen
def quote_for_log(part):
# shlex.quote is shell quoting, not output escaping: it wraps a value holding a
# line break in single quotes and leaves the break itself intact. One newline in
# a caller's metadata would then open a second log line, which the runner reads
# as a ::workflow-command. json.dumps escapes every control character, so one
# publish stays one line however the caller fills changelog, categories or topics.
quoted = shlex.quote(part)
return quoted if quoted.isprintable() else json.dumps(part)
def split_ref_path(value):
if not value:
return "", ""
@@ -572,7 +581,9 @@ jobs:
shell_line = " ".join(shlex.quote(part) for part in cmd)
path.write_text("#!/usr/bin/env bash\nset -euo pipefail\n" + shell_line + "\n", encoding="utf-8")
path.chmod(0o755)
print(shell_line)
# Log-only: the file above is what a maintainer re-runs, so it keeps plain shell
# quoting. This echo does not, because the runner parses each stdout line.
print(" ".join(quote_for_log(part) for part in cmd))
def write_output(fh, name, value):
delimiter = f"ghadelimiter_{uuid.uuid4().hex}"
@@ -232,4 +232,23 @@ describe("package publish workflow", () => {
expect(resolveStep?.run).toContain(`elif ${clearName}:\n cmd += ["--${name}", ""]`);
}
});
it("keeps a metadata value carrying CR or LF from opening a second log line", () => {
const workflow = readFileSync(resolve(".github/workflows/package-publish.yml"), "utf8");
// shlex.quote is shell quoting, not output escaping: it wraps a value in single quotes
// and leaves an embedded line break intact, so a changelog, categories or topics value
// carrying one would reach the runner as its own ::workflow-command line.
expect(workflow).toContain(
[
" quoted = shlex.quote(part)",
" return quoted if quoted.isprintable() else json.dumps(part)",
].join("\n"),
);
expect(workflow).toContain('print(" ".join(quote_for_log(part) for part in cmd))');
expect(workflow).not.toContain("print(shell_line)");
// The re-runnable .sh file is a real shell script, so it keeps plain shell quoting.
expect(workflow).toContain('shell_line = " ".join(shlex.quote(part) for part in cmd)');
});
});