Improve severity labels in Actions logs and tweak colors (#36993)

Add support for error, warning, notice, and debug log commands with bold
label prefixes and colored backgrounds matching GitHub's style. Parse
both `##[cmd]` and `::cmd args::` formats.

Also improved the severity colors globally and added a devtest page for
these.

---------

Co-authored-by: Claude (claude-opus-4-6) <noreply@anthropic.com>
This commit is contained in:
silverwind
2026-03-26 11:18:50 +01:00
committed by GitHub
parent 9583e1a65c
commit d5a89805d9
8 changed files with 168 additions and 58 deletions

View File

@@ -0,0 +1,80 @@
{{template "devtest/devtest-header"}}
<div class="page-content devtest ui container">
<h1>Severity Colors</h1>
<h2>Messages</h2>
<div class="ui error message">
<div class="header">Error Message</div>
<p>This is an error message using --color-error-* variables.</p>
</div>
<div class="ui warning message">
<div class="header">Warning Message</div>
<p>This is a warning message using --color-warning-* variables.</p>
</div>
<div class="ui success message">
<div class="header">Success Message</div>
<p>This is a success message using --color-success-* variables.</p>
</div>
<div class="ui info message">
<div class="header">Info Message</div>
<p>This is an info message using --color-info-* variables.</p>
</div>
<h2>Form Fields</h2>
<div class="ui form">
<div class="field error">
<label>Error Field</label>
<input type="text" value="Invalid input" readonly>
</div>
</div>
<h2>Labels</h2>
<div class="flex-text-block tw-gap-2">
<div class="ui red label">Red</div>
<div class="ui orange label">Orange</div>
<div class="ui yellow label">Yellow</div>
<div class="ui green label">Green</div>
<div class="ui blue label">Blue</div>
<div class="ui violet label">Violet</div>
<div class="ui purple label">Purple</div>
</div>
<h2>Color Swatches</h2>
<h3>Error</h3>
<div class="flex-text-block tw-gap-4">
<div class="tw-inline-flex tw-flex-col tw-items-center tw-gap-1 tw-min-w-[120px]">
<div class="tw-w-[100px] tw-h-[60px] tw-rounded tw-flex tw-items-center tw-justify-center tw-text-xs tw-bg-error-bg tw-text-error-text tw-border tw-border-error-border">Text</div>
<code>error-bg</code>
</div>
<div class="tw-inline-flex tw-flex-col tw-items-center tw-gap-1 tw-min-w-[120px]">
<div class="tw-w-[100px] tw-h-[60px] tw-rounded tw-flex tw-items-center tw-justify-center tw-text-xs tw-bg-error-bg-hover tw-text-error-text tw-border tw-border-error-border">Hover</div>
<code>error-bg-hover</code>
</div>
<div class="tw-inline-flex tw-flex-col tw-items-center tw-gap-1 tw-min-w-[120px]">
<div class="tw-w-[100px] tw-h-[60px] tw-rounded tw-flex tw-items-center tw-justify-center tw-text-xs tw-bg-error-bg-active tw-text-error-text tw-border tw-border-error-border">Active</div>
<code>error-bg-active</code>
</div>
</div>
<h3>Warning</h3>
<div class="flex-text-block tw-gap-4">
<div class="tw-inline-flex tw-flex-col tw-items-center tw-gap-1 tw-min-w-[120px]">
<div class="tw-w-[100px] tw-h-[60px] tw-rounded tw-flex tw-items-center tw-justify-center tw-text-xs tw-bg-warning-bg tw-text-warning-text tw-border tw-border-warning-border">Text</div>
<code>warning-bg</code>
</div>
</div>
<h3>Success</h3>
<div class="flex-text-block tw-gap-4">
<div class="tw-inline-flex tw-flex-col tw-items-center tw-gap-1 tw-min-w-[120px]">
<div class="tw-w-[100px] tw-h-[60px] tw-rounded tw-flex tw-items-center tw-justify-center tw-text-xs tw-bg-success-bg tw-text-success-text tw-border tw-border-success-border">Text</div>
<code>success-bg</code>
</div>
</div>
<h3>Info</h3>
<div class="flex-text-block tw-gap-4">
<div class="tw-inline-flex tw-flex-col tw-items-center tw-gap-1 tw-min-w-[120px]">
<div class="tw-w-[100px] tw-h-[60px] tw-rounded tw-flex tw-items-center tw-justify-center tw-text-xs tw-bg-info-bg tw-text-info-text tw-border tw-border-info-border">Text</div>
<code>info-bg</code>
</div>
</div>
</div>
{{template "devtest/devtest-footer"}}

View File

@@ -393,12 +393,6 @@ img.ui.avatar,
aspect-ratio: 1;
}
.ui.error.message .header,
.ui.warning.message .header {
color: inherit;
filter: saturate(2);
}
.full.height {
flex-grow: 1;
padding-bottom: var(--page-space-bottom);

View File

@@ -41,9 +41,9 @@
margin-bottom: 1em;
}
.ui.info.message .header,
.ui.blue.message .header {
color: var(--color-blue);
.ui.message .header {
color: inherit;
filter: saturate(2);
}
.ui.info.message,
@@ -55,12 +55,6 @@
border-color: var(--color-info-border);
}
.ui.success.message .header,
.ui.positive.message .header,
.ui.green.message .header {
color: var(--color-green);
}
.ui.success.message,
.ui.attached.success.message,
.ui.positive.message,
@@ -70,12 +64,6 @@
border-color: var(--color-success-border);
}
.ui.error.message .header,
.ui.negative.message .header,
.ui.red.message .header {
color: var(--color-red);
}
.ui.error.message,
.ui.attached.error.message,
.ui.red.message,
@@ -87,11 +75,6 @@
border-color: var(--color-error-border);
}
.ui.warning.message .header,
.ui.yellow.message .header {
color: var(--color-yellow);
}
.ui.warning.message,
.ui.attached.warning.message,
.ui.yellow.message,

View File

@@ -162,20 +162,20 @@ gitea-theme-meta-info {
--color-diff-removed-row-border: #634343;
--color-diff-removed-word-bg: #6f3333;
--color-diff-inactive: #22282d;
--color-error-border: #a04141;
--color-error-bg: #522;
--color-error-bg-active: #744;
--color-error-bg-hover: #633;
--color-error-text: #f9cbcb;
--color-error-border: #da3633;
--color-error-bg: #3c2425;
--color-error-bg-active: #5a3637;
--color-error-bg-hover: #4c2d2e;
--color-error-text: #f5817c;
--color-success-border: #458a57;
--color-success-bg: #284034;
--color-success-text: #6cc664;
--color-warning-border: #bb9d00;
--color-warning-bg: #3a3a30;
--color-warning-text: #fbbd08;
--color-success-text: #69be61;
--color-warning-border: #9e6a03;
--color-warning-bg: #2f2a1b;
--color-warning-text: #d29922;
--color-info-border: #306090;
--color-info-bg: #26354c;
--color-info-text: #38a8e8;
--color-info-text: #48b7f8;
--color-red-badge: #db2828;
--color-red-badge-bg: #db28281a;
--color-red-badge-hover-bg: #db28284d;

View File

@@ -162,20 +162,20 @@ gitea-theme-meta-info {
--color-diff-removed-row-border: #f1c0c0;
--color-diff-removed-word-bg: #fdb8c0;
--color-diff-inactive: #f0f2f4;
--color-error-border: #e0b4b4;
--color-error-bg: #fff6f6;
--color-error-bg-active: #fbb;
--color-error-bg-hover: #fdd;
--color-error-text: #9f3a38;
--color-success-border: #a3c293;
--color-success-bg: #fcfff5;
--color-success-text: #2c662d;
--color-warning-border: #c9ba9b;
--color-warning-bg: #fffaf3;
--color-warning-text: #573a08;
--color-info-border: #a9d5de;
--color-info-bg: #f8ffff;
--color-info-text: #276f86;
--color-error-border: #d63333;
--color-error-bg: #ffebeb;
--color-error-bg-active: #fdd;
--color-error-bg-hover: #fee;
--color-error-text: #8a3231;
--color-success-border: #49842b;
--color-success-bg: #eef6e4;
--color-success-text: #2f6e30;
--color-warning-border: #bf8700;
--color-warning-bg: #fff8e1;
--color-warning-text: #744500;
--color-info-border: #2d8fa8;
--color-info-bg: #e8f4fd;
--color-info-text: #216078;
--color-red-badge: #db2828;
--color-red-badge-bg: #db28281a;
--color-red-badge-hover-bg: #db28284d;

View File

@@ -229,7 +229,8 @@ function createLogLine(stepIndex: number, startTime: number, line: LogLine, cmd:
toggleElem(logTimeStamp, timeVisible.value['log-time-stamp']);
toggleElem(logTimeSeconds, timeVisible.value['log-time-seconds']);
return createElementFromAttrs('div', {id: `jobstep-${stepIndex}-${line.index}`, class: 'job-log-line'},
const lineClass = cmd?.name ? `job-log-line log-line-${cmd.name}` : 'job-log-line';
return createElementFromAttrs('div', {id: `jobstep-${stepIndex}-${line.index}`, class: lineClass},
lineNum, logTimeStamp, logMsg, logTimeSeconds,
);
}
@@ -650,8 +651,28 @@ async function hashChangeListener() {
color: var(--color-ansi-blue);
}
.job-step-logs .job-log-line .log-cmd-error {
color: var(--color-ansi-red);
.job-step-logs .log-msg-label {
font-weight: var(--font-weight-semibold);
}
.job-step-logs .log-line-error {
background: var(--color-error-bg);
}
.job-step-logs .log-line-warning {
background: var(--color-warning-bg);
}
.job-step-logs .log-cmd-error > .log-msg-label {
color: var(--color-error-text);
}
.job-step-logs .log-cmd-warning > .log-msg-label {
color: var(--color-warning-text);
}
.job-step-logs .log-cmd-debug {
color: var(--color-violet);
}
/* selectors here are intentionally exact to only match fullscreen */

View File

@@ -8,8 +8,14 @@ test('LogLineMessage', () => {
'##[endgroup]': '<span class="log-msg log-cmd-endgroup"></span>',
'::endgroup::': '<span class="log-msg log-cmd-endgroup"></span>',
// parser shouldn't do any trim, keep origin output as-is
'##[error] foo': '<span class="log-msg log-cmd-error"> foo</span>',
'##[error] foo': '<span class="log-msg log-cmd-error"><span class="log-msg-label">Error:</span><span> foo</span></span>',
'##[warning] foo': '<span class="log-msg log-cmd-warning"><span class="log-msg-label">Warning:</span><span> foo</span></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>',
'::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>',
'::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>',
// hidden is special, it is actually skipped before creating

View File

@@ -17,6 +17,9 @@ const LogLinePrefixCommandMap: Record<string, LogLineCommandName> = {
'##[endgroup]': 'endgroup',
'##[error]': 'error',
'##[warning]': 'warning',
'##[notice]': 'notice',
'##[debug]': 'debug',
'[command]': 'command',
// https://github.com/actions/toolkit/blob/master/docs/commands.md
@@ -26,13 +29,16 @@ const LogLinePrefixCommandMap: Record<string, LogLineCommandName> = {
'::remove-matcher': 'hidden', // it has arguments
};
// Pattern for ::cmd:: and ::cmd args:: format (args are stripped for display)
const LogLineCmdPattern = /^::(error|warning|notice|debug)(?:\s[^:]*)?::/;
export type LogLine = {
index: number;
timestamp: number;
message: string;
};
export type LogLineCommandName = 'group' | 'endgroup' | 'command' | 'error' | 'hidden';
export type LogLineCommandName = 'group' | 'endgroup' | 'command' | 'error' | 'warning' | 'notice' | 'debug' | 'hidden';
export type LogLineCommand = {
name: LogLineCommandName,
prefix: string,
@@ -45,19 +51,39 @@ export function parseLogLineCommand(line: LogLine): LogLineCommand | null {
return {name: LogLinePrefixCommandMap[prefix], prefix};
}
}
// Handle ::cmd:: and ::cmd args:: format (runner may pass these through raw)
const match = LogLineCmdPattern.exec(line.message);
if (match) {
return {name: match[1] as LogLineCommandName, prefix: match[0]};
}
return null;
}
const LogLineLabelMap: Partial<Record<LogLineCommandName, string>> = {
'error': 'Error',
'warning': 'Warning',
'notice': 'Notice',
'debug': 'Debug',
};
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"
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 logMsg = createElementFromAttrs('span', logMsgAttrs);
logMsg.innerHTML = renderAnsi(msgContent);
const label = cmd ? LogLineLabelMap[cmd.name] : null;
if (label) {
logMsg.append(createElementFromAttrs('span', {class: 'log-msg-label'}, `${label}:`));
const msgSpan = document.createElement('span');
msgSpan.innerHTML = ` ${renderAnsi(msgContent.trimStart())}`;
logMsg.append(msgSpan);
} else {
logMsg.innerHTML = renderAnsi(msgContent);
}
return logMsg;
}