diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue index 4e439a6db7..b297ff98d2 100644 --- a/web_src/js/components/ActionRunJobView.vue +++ b/web_src/js/components/ActionRunJobView.vue @@ -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; } diff --git a/web_src/js/components/ActionRunView.test.ts b/web_src/js/components/ActionRunView.test.ts index b625b15e71..e6eb658503 100644 --- a/web_src/js/components/ActionRunView.test.ts +++ b/web_src/js/components/ActionRunView.test.ts @@ -14,6 +14,13 @@ test('LogLineMessage', () => { '##[debug] foo': 'Debug: foo', '::error::foo': 'Error: foo', '::warning file=test.js,line=1::foo': 'Warning: foo', + '::error::foo%0Abar': 'Error: foo\nbar', + '::error::foo%0D%0Abar': 'Error: foo\nbar', + '::error::100%25 done%250A': 'Error: 100% done%0A', + '::error::keep%5Dsemi%3B': 'Error: keep%5Dsemi%3B', + '::group::foo%0Abar': 'foo\nbar', + '##[error]foo%0Abar%3B%5D': 'Error: foo\nbar;]', + '##[command]foo%0Abar': 'foo%0Abar', '::notice::foo': 'Notice: foo', '::debug::foo': 'Debug: foo', '##[command] foo': ' foo', diff --git a/web_src/js/components/ActionRunView.ts b/web_src/js/components/ActionRunView.ts index 52fcdf56cd..d94c6510a1 100644 --- a/web_src/js/components/ActionRunView.ts +++ b/web_src/js/components/ActionRunView.ts @@ -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 = { '::group::': 'group', '##[group]': 'group', @@ -67,14 +68,23 @@ const LogLineLabelMap: Partial> = { '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) {