Input
Two plain-text UTF-8 log files from the same CI job: one you consider known good, one from the failed run. You choose the roles. A file extension is not treated as validation: the worker checks the actual bytes, rejects invalid UTF-8 and NUL bytes, and counts lines and line lengths.
GitHub Actions ::group:: / ::endgroup:: markers are tracked as sections. Unbalanced markers become diagnostics with their line number. ANSI colors, OSC hyperlinks/titles, DCS and other terminal control sequences are removed for display; bidirectional override characters are shown as visible [U+202E]-style tokens. CRLF line endings are accepted. Your files are never modified.
Normalization
The active profile is always visible in the app. It has two parts, both optional:
- ISO timestamp prefix (on by default): a leading
2026-09-15T10:00:00Zor[2026-09-15T10:00:00.123+02:00]is ignored when comparing. - Workspace prefix: an exact path such as
/home/runner/work/repo, one per side, replaced with<workspace>only at a path boundary.
Nothing else is normalized. There is no user-supplied regular expression. Changing the profile or either input discards the current result, the selection, the “include log text” choice and any AI permission.
Reading results
A result has one of three statuses:
pass— every line was compared and no differences remain after normalization. It does not mean the CI job passed.findings— there are changed blocks or group-marker diagnostics.incomplete— a budget ran out. The result states the last compared line on each side and the reason; lines after that are unknown and are labelled “not compared” in the app.
A changed block is a run of adjacent lines that differ, with no equal line in between. It is added, removed or changed, and carries the 1-based line range on each side. A block is an error candidate when the failed-run side contains an error-like line (for example error, fatal, exception, ##[error], a non-zero exit code) that is not in the known-good side of the same block. “0 failed” and “no errors” do not count. Candidates are leads, not a diagnosis, and a failure that never prints such a word has no candidates — its changes are still listed.
In the app, Original shows the file’s text with terminal controls removed; Normalized shows what was actually compared. The source context around a block can be widened to 40 lines; at most 100 lines per side are rendered at once, with paging for longer blocks.
Export contract
JSON and HTML exports come from the same report object: schema_version 2.1, ruleset ci-log-diff-v2, tool_version, status, coverage, summary, diagnostics, exportSelection (selected and available counts) and changes with id, kind, candidate, anchor and line ranges.
Default exports contain no log text, file names, group labels or workspace paths. With “Include log text” (--include-context in the CLI), each exported block also carries its excerpt: up to 4 lines per side, 500 characters per line, including the group label. This is not secret redaction; review before sharing. Exporting an empty selection keeps the original findings or incomplete status.
The HTML report is a single file with no scripts, images, links or remote resources, and its own restrictive Content Security Policy. See a real example →
Changes from schema 2.0: adjacent differing lines are reported as one block instead of many one-line blocks (ruleset v2); each change has an anchor giving the number of lines before the block on each side; reports carry tool_version; exportSelection.selected counts exported blocks.
Limits
| Limit | Value | When it is exceeded |
|---|---|---|
| File size | 8,388,608 bytes (8 MiB) per file | Refused before parsing, with the side named |
| Lines | 200,000 per file | Refused, with the side named |
| Line length | 65,536 characters | Refused, with the side named |
| Encoding | UTF-8 text, no NUL bytes | Refused; no lossy decoding |
| Alignment work | 4,000,000 line comparisons; 64-line lookahead, then re-sync on a line that is unique in both logs within 2,000 lines | incomplete with the covered range |
| Result size | 200 changed blocks or 512 KiB | incomplete with the covered range |
| Deadline | 5 seconds | Worker terminated; no result |
| Excerpt per block | 4 lines per side, 500 characters per line | Marked as shortened; line range stays exact |
| Review window (app only) | 100 lines per side, 2,000 characters per line | Paged; never exported |
Browsers do not offer a hard memory cap for a page, so resource use is bounded through these input, work and output budgets plus worker termination. Measured timings depend on the device and are not promised.
CLI
Requires Node.js 24 (24.12 or later). The CLI reads two files inside a root directory you name and can create one new report file there. It makes no network requests and never overwrites a file.
npm ci --ignore-scripts
node apps/cli/main.mjs --root . --before fixtures/good.txt --after fixtures/failed.txt
node apps/cli/main.mjs --root . --before good.log --after failed.log --format html --out report.html
node apps/cli/main.mjs --help
| Exit code | Meaning |
|---|---|
| 0 | Complete comparison, nothing that violates --fail-on |
| 1 | Complete comparison with policy findings (candidates by default, or changes) |
| 2 | Invalid arguments or input, refused path, existing output file, or incomplete comparison |
| 130 | Cancelled (SIGINT) |
Paths are relative to --root. Absolute paths, .., symbolic links, FIFOs and other non-regular files are refused. Errors are printed as a JSON code on stderr, never with file contents.
AI agents
There are two ways an assistant can use CI Log Diff today:
- Local CLI. A coding agent with shell access can run the command above and read the bounded JSON. The default output has no log text, so nothing from the log is fed back as instructions.
- Optional browser tool. When the browser exposes
document.modelContext, the app registers a read-onlyread_ci_comparison_summarytool. It returns the default export (classifications and line ranges, never text) and only after you tick a visible permission that resets on every new comparison, input change or reset. In browsers without that API nothing is registered and nothing is shown. It has been tested against a mock host only.
There is no remote MCP server, hosted connector or API. Log content is data: nothing in a log can trigger a command, a request or a tool call. Machine-readable copies of this page: overview.md, llms.txt.
Security model
The site is static files. The Content Security Policy allows same-origin scripts, styles and workers only, with connect-src 'none', no unsafe-eval and no inline script execution. The app writes nothing to localStorage, IndexedDB, cookies, the URL or the clipboard. Closing the tab, pressing Reset or changing an input terminates the worker that holds the parsed text. Downloads happen only when you press a download button. Privacy details →