[v13.0/forgejo] fix: don't show ConEmu OSC escape sequences (#9919)

**Backport:** https://codeberg.org/forgejo/forgejo/pulls/9875

- Remove all [ConEMU OSC commands](https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC) from the output of Forgejo action logs when rendering.
- The regex is constructed as followed: Match the prefix `ESC ] 9 ;`. Then matches any number of digits, then match everything up to and including `ST` (this is either `ESC\` or `BELL`).
- Resolves forgejo/forgejo#9244

Co-authored-by: Gusted <postmaster@gusted.xyz>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9919
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
This commit is contained in:
forgejo-backport-action 2025-10-31 16:15:29 +01:00 committed by Gusted
parent a50968d0de
commit cb0845cd3e
4 changed files with 94 additions and 3 deletions

View file

@ -623,3 +623,73 @@ test('view with pre-execution error', async () => {
expect(block.exists()).toBe(true);
expect(block.text()).toBe('pre-execution error Oops, I dropped it.');
});
test('Offset index', async () => {
Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
vi.spyOn(global, 'fetch').mockImplementation((url, opts) => {
const stepsLog_value = [
{
step: 0,
cursor: 0,
lines: [
{index: 1, message: '\u001b]9;4;3\u0007\r\u001bM\u001b[?2026l\u001b[?2026h\u001b[J', timestamp: 0},
{index: 2, message: 'second line', timestamp: 0},
{index: 3, message: '\u001b]9;4;3\u0007\r\u001bM\u001b[?2026l\u001b[J\u001b]9;4;0\u0007\u001b[?2026h\u001b[J\u001b]9;4;1;0\u0007\u001b[?2026l\u001b[J\u001b]9;4;0\u0007', timestamp: 0},
{index: 4, message: 'fourth line', timestamp: 0},
],
},
];
const jobs_value = {
state: {
run: {
status: 'success',
commit: {
pusher: {},
},
},
currentJob: {
steps: [
{
summary: 'Test Job',
duration: '1s',
status: 'success',
},
],
allAttempts: [{number: 1, time_since_started_html: '', status: 'success'}],
},
},
logs: {
stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [],
},
};
return Promise.resolve({
ok: true,
json: vi.fn().mockResolvedValue(
url.endsWith('/artifacts') ? [] : jobs_value,
),
});
});
const wrapper = mount(RepoActionView, {
props: defaultTestProps,
});
await flushPromises();
await wrapper.get('.job-step-summary').trigger('click');
await flushPromises();
// Check if two lines where rendered
expect(wrapper.findAll('.job-log-line').length).toEqual(2);
// Check line one.
expect(wrapper.get('.job-log-line:nth-of-type(1)').attributes('id')).toEqual('jobstep-0-1');
expect(wrapper.get('.job-log-line:nth-of-type(1) .line-num').text()).toEqual('1');
expect(wrapper.get('.job-log-line:nth-of-type(1) .line-num').attributes('href')).toEqual('#jobstep-0-1');
expect(wrapper.get('.job-log-line:nth-of-type(1) .log-msg').text()).toEqual('second line');
// Check line two.
expect(wrapper.get('.job-log-line:nth-of-type(2)').attributes('id')).toEqual('jobstep-0-2');
expect(wrapper.get('.job-log-line:nth-of-type(2) .line-num').text()).toEqual('2');
expect(wrapper.get('.job-log-line:nth-of-type(2) .line-num').attributes('href')).toEqual('#jobstep-0-2');
expect(wrapper.get('.job-log-line:nth-of-type(2) .log-msg').text()).toEqual('fourth line');
});

View file

@ -33,6 +33,7 @@ const sfc = {
initialLoadComplete: false,
needLoadingWithLogCursors: null,
intervalID: null,
lineNumberOffset: [],
currentJobStepsStates: [],
artifacts: [],
menuVisible: undefined,
@ -203,15 +204,16 @@ const sfc = {
},
createLogLine(line, startTime, stepIndex, group) {
const lineNo = line.index - this.lineNumberOffset[stepIndex];
const div = document.createElement('div');
div.classList.add('job-log-line');
div.setAttribute('id', `jobstep-${stepIndex}-${line.index}`);
div.setAttribute('id', `jobstep-${stepIndex}-${lineNo}`);
div._jobLogTime = line.timestamp;
const lineNumber = document.createElement('a');
lineNumber.classList.add('line-num', 'muted');
lineNumber.textContent = line.index;
lineNumber.setAttribute('href', `#jobstep-${stepIndex}-${line.index}`);
lineNumber.textContent = lineNo;
lineNumber.setAttribute('href', `#jobstep-${stepIndex}-${lineNo}`);
div.append(lineNumber);
// for "Show timestamps"
@ -230,6 +232,13 @@ const sfc = {
let logMessage = document.createElement('span');
logMessage.innerHTML = renderAnsi(line.message);
// If the input to renderAnsi is not empty and the output is empty we can
// assume the input was only ANSI escape codes that have been removed. In
// that case we should not display this message
if (line.message !== '' && logMessage.innerHTML === '') {
this.lineNumberOffset[stepIndex]++;
return [];
}
if (group.isHeader) {
const details = document.createElement('details');
details.addEventListener('toggle', this.toggleGroupLogs);
@ -378,6 +387,7 @@ const sfc = {
// append logs to the UI
for (const logs of job.logs.stepsLog) {
// save the cursor, it will be passed to backend next time
this.lineNumberOffset[logs.step] = 0;
this.currentJobStepsStates[logs.step].cursor = logs.cursor;
this.appendLogs(logs.step, logs.lines, logs.started);
}

View file

@ -2,7 +2,9 @@ import {AnsiUp} from 'ansi_up';
const replacements = [
[/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
[/\x1bM/g, ''], // Move cursor one line up, threat them as no-op.
[/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
[/\x1b\]9;\d+.*?(\x07|\x1b\\)/g, ''], // ConEmu, treat them as no-op.
];
// render ANSI to HTML

View file

@ -17,4 +17,13 @@ test('renderAnsi', () => {
// treat "\033[0K" and "\033[0J" (Erase display/line) as "\r", then it will be covered to "\n" finally.
expect(renderAnsi('a\x1b[Kb\x1b[2Jc')).toEqual('a\nb\nc');
expect(renderAnsi('\x1b[48;5;88ma\x1b[38;208;48;5;159mb\x1b[m')).toEqual(`<span style="background-color:rgb(135,0,0)">a</span><span style="background-color:rgb(175,255,255)">b</span>`);
expect(renderAnsi('\x1b]9;4;0\x07')).toEqual('');
expect(renderAnsi('\x1b]9;4;1;25\x07compiling main.zig')).toEqual('compiling main.zig');
expect(renderAnsi('\x1b]9;4;1;25\x1b\\compiling main.zig')).toEqual('compiling main.zig');
expect(renderAnsi('\x1b]9;4;3\x07waiting...')).toEqual('waiting...');
expect(renderAnsi('\x1b]9;1;500\x07sleeping...')).toEqual('sleeping...');
expect(renderAnsi('\x1b]9;4;3\x07waiting...\x1b]9;4;3\x07')).toEqual('waiting...');
expect(renderAnsi('\x1b]9;12\x07')).toEqual('');
expect(renderAnsi('\x1b]9;4;1;25\x07\x1bMcompiling main.zig')).toEqual('compiling main.zig');
});