Skip to content

Publish reports via GitHub Pages so they open in the browser directly #2

Description

@vitlav

Hi! Thanks for these reports, they are really useful.

Right now, to read a report you have to download the .html file and open it locally (as the README says). That is a bit awkward, especially when you just want to share a link to a finding with a maintainer.

The reports are fully self-contained: CSS and JS are inlined, with no external scripts or styles. So GitHub Pages can serve them as they are:

  • Minimal option: Settings → Pages → Deploy from a branch → master / (root). Every report then gets a URL such as
    https://qarmin.github.io/ClaudeReports/wine_20260914.html.
    The size is within Pages limits: about 185 MB without .git, against the 1 GB site limit. The largest file is 7.5 MB, against the 100 MB per-file limit.

  • Slightly nicer option: a small Actions workflow that publishes only the *.html reports (not Details/) and generates:

    • index.html: a searchable list of all projects with their report dates and links to older reports that are still kept;
    • latest/<Project>.html: a stable redirect to the newest report of each project. Links then keep working after a refresh renames the file to a new date, which would reduce the need for keep in projects.toml.

    The published site is ~114 MB. After adding the two files below, enable it once in Settings → Pages → Source: GitHub Actions.

.github/workflows/pages.yml
name: Publish reports to GitHub Pages

on:
  push:
    branches: [master]
    paths: ['*.html', 'scripts/build_site.py', '.github/workflows/pages.yml']
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.x'
      - run: python3 scripts/build_site.py . _site
      - uses: actions/configure-pages@v5
      - uses: actions/upload-pages-artifact@v3
        with:
          path: _site
      - id: deployment
        uses: actions/deploy-pages@v4
scripts/build_site.py
#!/usr/bin/env python3
"""Build the GitHub Pages site: copy reports, generate index and stable links."""
import html
import re
import shutil
import sys
from collections import defaultdict
from pathlib import Path

REPORT_RE = re.compile(r"^(?P<name>.+)_(?P<date>\d{8})\.html$")


def main(src: Path, out: Path) -> None:
    out.mkdir(parents=True, exist_ok=True)
    (out / "latest").mkdir(exist_ok=True)
    (out / ".nojekyll").touch()

    projects = defaultdict(list)
    for f in src.glob("*.html"):
        m = REPORT_RE.match(f.name)
        if not m:
            continue
        shutil.copy2(f, out / f.name)
        projects[m["name"]].append((m["date"], f.name, f.stat().st_size))

    rows = []
    for name in sorted(projects, key=str.lower):
        reports = sorted(projects[name], reverse=True)
        date, fname, size = reports[0]
        # Stable URL that always points to the newest report of the project
        (out / "latest" / f"{name}.html").write_text(
            f'<!doctype html><meta charset="utf-8">'
            f'<meta http-equiv="refresh" content="0; url=../{fname}">'
            f'<link rel="canonical" href="../{fname}">'
            f'<a href="../{fname}">{html.escape(fname)}</a>\n')
        older = " ".join(
            f'<a class="old" href="{f}">{d[:4]}-{d[4:6]}-{d[6:]}</a>'
            for d, f, _ in reports[1:])
        rows.append(
            f'<tr><td><a href="{fname}">{html.escape(name)}</a></td>'
            f'<td>{date[:4]}-{date[4:6]}-{date[6:]}</td>'
            f'<td class="num">{size / 1024:.0f}&nbsp;KB</td><td>{older}</td></tr>')

    (out / "index.html").write_text(INDEX.replace("{count}", str(len(rows)))
                                    .replace("{rows}", "\n".join(rows)))
    print(f"{len(rows)} projects written to {out}")


INDEX = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Claude Code audit reports</title>
<style>
:root{color-scheme:light dark;--fg:#1f2328;--bg:#fff;--muted:#656d76;--line:#d0d7de;--link:#0969da}
@media (prefers-color-scheme:dark){:root{--fg:#e6edf3;--bg:#0d1117;--muted:#8d96a0;--line:#30363d;--link:#4493f8}}
body{font:15px/1.5 system-ui,sans-serif;color:var(--fg);background:var(--bg);max-width:900px;margin:0 auto;padding:1.5rem 1rem}
a{color:var(--link);text-decoration:none}a:hover{text-decoration:underline}
input{width:100%;box-sizing:border-box;padding:.5rem .7rem;font:inherit;color:inherit;background:transparent;border:1px solid var(--line);border-radius:6px;margin:1rem 0}
table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:.35rem .5rem;border-bottom:1px solid var(--line)}
th{color:var(--muted);font-weight:600}.num{text-align:right;white-space:nowrap}.old{color:var(--muted);font-size:.9em;margin-right:.5em}
p{color:var(--muted)}
</style>
</head>
<body>
<h1>Claude Code audit reports</h1>
<p>{count} projects. Source: <a href="https://github.com/qarmin/ClaudeReports">qarmin/ClaudeReports</a>.
Stable link to the newest report of a project: <code>latest/&lt;Project&gt;.html</code>.</p>
<input id="q" type="search" placeholder="Filter projects..." autofocus>
<table>
<thead><tr><th>Project</th><th>Date</th><th class="num">Size</th><th>Older</th></tr></thead>
<tbody id="t">
{rows}
</tbody>
</table>
<script>
const q = document.getElementById('q'), rows = [...document.querySelectorAll('#t tr')];
q.addEventListener('input', () => {
  const s = q.value.toLowerCase();
  rows.forEach(r => r.hidden = !r.cells[0].textContent.toLowerCase().includes(s));
});
</script>
</body>
</html>
"""

if __name__ == "__main__":
    main(Path(sys.argv[1] if len(sys.argv) > 1 else "."),
         Path(sys.argv[2] if len(sys.argv) > 2 else "_site"))

I tested the generator locally on the current master: it produces 279 projects. I'm happy to open a PR with these files if you prefer.

Until then, a report can be viewed without downloading through raw.githack.com, e.g.
https://raw.githack.com/qarmin/ClaudeReports/master/ripgrep_20260909.html

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions