fix: make Actions log parser support multiple line message encoding (#38659)

fix #38652


UI part (`.log-msg`) uses "white-space: break-spaces;" so the new line
can be correctly rendered.

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
wxiaoguang
2026-07-27 22:24:46 +08:00
committed by GitHub
parent 7efcb8d6ca
commit dd407ee3e8
3 changed files with 23 additions and 6 deletions
+1 -1
View File
@@ -722,7 +722,7 @@ async function hashChangeListener() {
.job-step-logs .job-log-line .log-msg {
flex: 1;
white-space: break-spaces;
white-space: break-spaces; /* decoded commands like "::error::foo%0Abar" contain "\n" */
margin-left: 12px;
overflow-wrap: anywhere;
}
@@ -14,6 +14,13 @@ test('LogLineMessage', () => {
'##[debug] foo': '<span class="log-msg log-cmd-debug"><span class="log-msg-label">Debug:</span><span> foo</span></span>',
'::error::foo': '<span class="log-msg log-cmd-error"><span class="log-msg-label">Error:</span><span> foo</span></span>',
'::warning file=test.js,line=1::foo': '<span class="log-msg log-cmd-warning"><span class="log-msg-label">Warning:</span><span> foo</span></span>',
'::error::foo%0Abar': '<span class="log-msg log-cmd-error"><span class="log-msg-label">Error:</span><span> foo\nbar</span></span>',
'::error::foo%0D%0Abar': '<span class="log-msg log-cmd-error"><span class="log-msg-label">Error:</span><span> foo\nbar</span></span>',
'::error::100%25 done%250A': '<span class="log-msg log-cmd-error"><span class="log-msg-label">Error:</span><span> 100% done%0A</span></span>',
'::error::keep%5Dsemi%3B': '<span class="log-msg log-cmd-error"><span class="log-msg-label">Error:</span><span> keep%5Dsemi%3B</span></span>',
'::group::foo%0Abar': '<span class="log-msg log-cmd-group">foo\nbar</span>',
'##[error]foo%0Abar%3B%5D': '<span class="log-msg log-cmd-error"><span class="log-msg-label">Error:</span><span> foo\nbar;]</span></span>',
'##[command]foo%0Abar': '<span class="log-msg log-cmd-command">foo%0Abar</span>',
'::notice::foo': '<span class="log-msg log-cmd-notice"><span class="log-msg-label">Notice:</span><span> foo</span></span>',
'::debug::foo': '<span class="log-msg log-cmd-debug"><span class="log-msg-label">Debug:</span><span> foo</span></span>',
'##[command] foo': '<span class="log-msg log-cmd-command"> foo</span>',
+15 -5
View File
@@ -9,7 +9,8 @@ import {POST} from '../modules/fetch.ts';
// * Workflow command outputs log commands like "::group::the-title", "::add-matcher::...."
// * Workflow runner parses and processes the commands to "##[group]", apply "matchers", hide secrets, etc.
// * The reported logs are the processed logs.
// HOWEVER: Gitea runner does not completely process those commands. Many works are done by the frontend at the moment.
// HOWEVER: Gitea cannot, a decoded message may contain newlines and FormatLog drops them,
// so the commands arrive here still escaped and the frontend decodes them.
const LogLinePrefixCommandMap: Record<string, LogLineCommandName> = {
'::group::': 'group',
'##[group]': 'group',
@@ -67,14 +68,23 @@ const LogLineLabelMap: Partial<Record<LogLineCommandName, string>> = {
'debug': 'Debug',
};
function decodeLineMessage(line: LogLine, cmd: LogLineCommand | null): string {
// TODO: for some commands (::group::), the "prefix removal" works well, for some commands with "arguments" (::remove-matcher ...::),
// it needs to do further processing in the future (fortunately, at the moment we don't need to handle these commands)
if (!cmd) return line.message;
let msg = line.message.substring(cmd.prefix.length);
if (cmd.name === 'command') return msg; // "command" is only an output tag, do not parse or escape it
// "##[cmd]" also escapes ";" and "]" which delimit its header, "::cmd::" does not
if (!cmd.prefix.startsWith('::')) msg = msg.replace(/%3B/g, ';').replace(/%5D/g, ']');
// renderAnsiInto breaks a line per "\r", so "%0D%0A" is one break. "%25" last keeps "%250A" literal
return msg.replace(/(?:%0D)?%0A/g, '\n').replace(/%0D/g, '\r').replace(/%25/g, '%');
}
export function createLogLineMessage(line: LogLine, cmd: LogLineCommand | null) {
const logMsgAttrs = {class: 'log-msg'};
if (cmd?.name) logMsgAttrs.class += ` log-cmd-${cmd.name}`; // make it easier to add styles to some commands like "error"
// TODO: for some commands (::group::), the "prefix removal" works well, for some commands with "arguments" (::remove-matcher ...::),
// it needs to do further processing in the future (fortunately, at the moment we don't need to handle these commands)
const msgContent = cmd ? line.message.substring(cmd.prefix.length) : line.message;
const msgContent = decodeLineMessage(line, cmd);
const logMsg = createElementFromAttrs('span', logMsgAttrs);
const label = cmd ? LogLineLabelMap[cmd.name] : null;
if (label) {