diff --git a/src/gha_logs.rs b/src/gha_logs.rs index 482170e11..7b7608339 100644 --- a/src/gha_logs.rs +++ b/src/gha_logs.rs @@ -13,6 +13,7 @@ use std::collections::VecDeque; use std::sync::Arc; use uuid::Uuid; +pub const GHA_LOGS_JS: &str = include_str!("gha_logs/gha_logs.js"); pub const ANSI_UP_URL: &str = "/gha_logs/ansi_up@0.0.1-custom.js"; pub const SUCCESS_URL: &str = "/gha_logs/success@1.svg"; pub const FAILURE_URL: &str = "/gha_logs/failure@1.svg"; @@ -224,6 +225,11 @@ table {{ white-space: pre; table-layout: fixed; width: 100%; + border-spacing: 0; + border-collapse: collapse; +}} +tr.selected {{ + background: #1d1a16; /* similar to GitHub’s yellow-ish */ }} .timestamp {{ color: #848484; @@ -271,92 +277,11 @@ table {{ const logs = {logs}; const tree_roots = {tree_roots}; - const ansi_up = new AnsiUp(); - ansi_up.use_classes = true; - - // 1. Tranform the ANSI escape codes to HTML - var html = ansi_up.ansi_to_html(logs); - - // 2. Remove UTF-8 useless BOM and Windows Carriage Return - html = html.replace(/^\uFEFF/gm, ""); - html = html.replace(/\r\n/g, "\n"); - - // 3. Transform each log lines that doesn't start with a timestamp into a row where everything is in the second column - const untsRegex = /^(?!\d{{4}}-\d{{2}}-\d{{2}}T\d{{2}}:\d{{2}}:\d{{2}}\.\d+Z)(.*)(\n)?/gm; - html = html.replace(untsRegex, (match, log) => - `${{log}}` - ); - - // 3.b Transform each log lines that start with a timestamp in a row with two columns and make the timestamp be a - // self-referencial anchor. - const tsRegex = /^(\d{{4}}-\d{{2}}-\d{{2}}T\d{{2}}:\d{{2}}:\d{{2}}\.\d+Z) (.*)(\n)?/gm; - html = html.replace(tsRegex, (match, ts, log) => - `${{log}}` - ); - - // 4. Add a anchor around every "##[error]" string - let errorCounter = -1; - html = html.replace(/##\[error\]/g, () => - `##[error]` - ); - - // 4.b Add a span around every "##[warning]" string - html = html.replace(/##\[warning\]/g, () => - `##[warning]` - ); - - // pre-5. Polyfill the recently (2025) added `RegExp.escape` function. - // Inspired by the former MDN section on escaping: - // https://web.archive.org/web/20230806114646/https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping - const escapeRegExp = RegExp.escape || function(string) {{ - return string.replace(/[.*+?^${{}}()|[\]\\/]/g, "\\$&"); - }}; - - // 5. Add anchors around some paths - // Detailed examples of what the regex does is at https://regex101.com/r/vCnx9Y/2 - // - // But simply speaking the regex tries to find absolute (with `/checkout` prefix) and - // relative paths, the path must start with one of the repository level-2 directories. - // We also try to retrieve the lines and cols if given (`:line:col`). - // - // Some examples of paths we want to find: - // - src/tools/test-float-parse/src/traits.rs:173:11 - // - /checkout/compiler/rustc_macros - // - /checkout/src/doc/rustdoc/src/advanced-features.md - // - // Any other paths, in particular if prefixed by `./` or `obj/` should not taken. - const pathRegex = new RegExp( - "(?[^a-zA-Z0-9.\\/])" - + "(?(?:[\\\/]?(?:checkout[\\\/])?(?(?:" - + tree_roots.map(p => escapeRegExp(p)).join("|") - + ")(?:[\\\/][a-zA-Z0-9_$\\\-.\\\/]+)?))" - + "(?::(?[0-9]+):(?[0-9]+))?)(?[^a-zA-Z0-9.])", - "g" - ); - html = html.replace(pathRegex, (match, boundary_start, inner, path, line, col, boundary_end) => {{ - const pos = (line !== undefined) ? `#L${{line}}` : ""; - return `${{boundary_start}}${{inner}}${{boundary_end}}`; - }}); - - // 6. Add the html to the table - document.getElementById("logs").innerHTML = html; - - // 7. If no anchor is given, scroll to the last error - if (location.hash === "" && errorCounter >= 0) {{ - const hasSmallViewport = window.innerWidth <= 750; - document.getElementById(`error-${{errorCounter}}`).scrollIntoView({{ - behavior: 'instant', - block: 'end', - inline: hasSmallViewport ? 'start' : 'center' - }}); - }} + const owner = "{owner}"; + const repo = "{repo}"; + const sha = "{sha}"; - // 8. Add a copy handler that force plain/text copy - document.addEventListener("copy", function(e) {{ - var text = window.getSelection().toString(); - e.clipboardData.setData('text/plain', text); - e.preventDefault(); - }}); + {GHA_LOGS_JS} }} catch (e) {{ console.error(e); diff --git a/src/gha_logs/gha_logs.js b/src/gha_logs/gha_logs.js new file mode 100644 index 000000000..fb10f5e91 --- /dev/null +++ b/src/gha_logs/gha_logs.js @@ -0,0 +1,158 @@ +// Logic for triagebot GitHub Actions logs viewer + +const logsEl = document.getElementById("logs"); +const ansi_up = new AnsiUp(); +ansi_up.use_classes = true; + +let startingAnchorId = null; + +// 1. Tranform the ANSI escape codes to HTML +var html = ansi_up.ansi_to_html(logs); + +// 2. Remove UTF-8 useless BOM and Windows Carriage Return +html = html.replace(/^\uFEFF/gm, ""); +html = html.replace(/\r\n/g, "\n"); + +// 3. Transform each log lines that doesn't start with a timestamp into a row where everything is in the second column +const untsRegex = /^(?!\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)(.*)(\n)?/gm; +html = html.replace(untsRegex, (match, log) => + `${log}` +); + +// 3.b Transform each log lines that start with a timestamp in a row with two columns and make the timestamp be a +// self-referencial anchor. +const tsRegex = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z) (.*)(\n)?/gm; +html = html.replace(tsRegex, (match, ts, log) => + `${log}` +); + +// 4. Add a anchor around every "##[error]" string +let errorCounter = -1; +html = html.replace(/##\[error\]/g, () => + `##[error]` +); + +// 4.b Add a span around every "##[warning]" string +html = html.replace(/##\[warning\]/g, () => + `##[warning]` +); + +// pre-5. Polyfill the recently (2025) added `RegExp.escape` function. +// Inspired by the former MDN section on escaping: +// https://web.archive.org/web/20230806114646/https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping +const escapeRegExp = RegExp.escape || function(string) { + return string.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"); +}; + +// 5. Add anchors around some paths +// Detailed examples of what the regex does is at https://regex101.com/r/vCnx9Y/2 +// +// But simply speaking the regex tries to find absolute (with `/checkout` prefix) and +// relative paths, the path must start with one of the repository level-2 directories. +// We also try to retrieve the lines and cols if given (`:line:col`). +// +// Some examples of paths we want to find: +// - src/tools/test-float-parse/src/traits.rs:173:11 +// - /checkout/compiler/rustc_macros +// - /checkout/src/doc/rustdoc/src/advanced-features.md +// +// Any other paths, in particular if prefixed by `./` or `obj/` should not taken. +const pathRegex = new RegExp( + "(?[^a-zA-Z0-9.\\/])" + + "(?(?:[\\\/]?(?:checkout[\\\/])?(?(?:" + + tree_roots.map(p => escapeRegExp(p)).join("|") + + ")(?:[\\\/][a-zA-Z0-9_$\\\-.\\\/]+)?))" + + "(?::(?[0-9]+):(?[0-9]+))?)(?[^a-zA-Z0-9.])", + "g" +); +html = html.replace(pathRegex, (match, boundary_start, inner, path, line, col, boundary_end) => { + const pos = (line !== undefined) ? `#L${line}` : ""; + return `${boundary_start}${inner}${boundary_end}`; +}); + +// 6. Add the html to the table +logsEl.innerHTML = html; + +// 7. If no anchor is given, scroll to the last error +if (location.hash === "" && errorCounter >= 0) { + const hasSmallViewport = window.innerWidth <= 750; + document.getElementById(`error-${errorCounter}`).scrollIntoView({ + behavior: 'instant', + block: 'end', + inline: hasSmallViewport ? 'start' : 'center' + }); +} + +// 8. If a anchor is given, highlight and scroll to the selection +if (location.hash !== "") { + const match = window.location.hash + .match(/L?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)(?:-L(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z))?/); + + if (match) { + const [startId, endId] = [match[1], match[2] || match[1]].map(decodeURIComponent); + const startRow = logsEl.querySelector(`a[id="${startId}"]`)?.closest('tr'); + + if (startRow) { + startingAnchorId = startId; + highlightTimestampRange(startId, endId); + startRow.scrollIntoView({ block: 'center' }); + } + } +} + +// 9. Add a copy handler that force plain/text copy +logsEl.addEventListener("copy", function(e) { + var text = window.getSelection().toString(); + e.clipboardData.setData('text/plain', text); + e.preventDefault(); +}); + +// 10. Add click event to handle custom hightling +logsEl.addEventListener('click', (e) => { + const rowEl = e.target.closest('tr'); + if (!rowEl || !e.target.classList.contains("timestamp")) return; + + const ctrlOrMeta = e.ctrlKey || e.metaKey; + const shiftKey = e.shiftKey; + const rowId = getRowId(rowEl); + + // Prevent default link behavior + e.preventDefault(); + e.stopPropagation(); + + if (!ctrlOrMeta && !shiftKey) { + // Normal click: select single row, set anchor + startingAnchorId = rowId; + highlightTimestampRange(startingAnchorId, startingAnchorId); + } else if (shiftKey && startingAnchorId !== null) { + // Shift+click: extend selection from anchor + highlightTimestampRange(startingAnchorId, rowId); + } else if (ctrlOrMeta) { + // Ctrl/Cmd+click: new anchor (resets selection) + startingAnchorId = rowId; + highlightTimestampRange(startingAnchorId, startingAnchorId); + } + + // Update our URL hash after every selection change + const ids = Array.from(logsEl.querySelectorAll('tr.selected')).map(getRowId).sort(); + window.location.hash = ids.length ? + (ids.length === 1 ? `L${ids[0]}` : `L${ids[0]}-L${ids[ids.length-1]}`) : ''; +}); + +// Helper function to get the ID of the given row +function getRowId(rowEl) { + return rowEl.querySelector('a.timestamp').id; // "2025-12-12T21:28:09.6347029Z" +} + +// Helper function to highlight (toggle the selected class) on the given timestamp range +function highlightTimestampRange(startId, endId) { + const rows = Array.from(logsEl.querySelectorAll('tr')).filter(r => r.querySelector('.timestamp')); + + const startIndex = rows.findIndex(row => getRowId(row) === startId); + const endIndex = rows.findIndex(row => getRowId(row) === endId); + + const start = Math.min(startIndex, endIndex); + const end = Math.max(startIndex, endIndex); + + rows.forEach((row, index) => row.classList.toggle('selected', index >= start && index <= end)); +}