Explore guh.nvim's CI-log approach (raw job-logs endpoint + terminal render) against ours #784

Open
opened 2026-06-08 19:52:51 +00:00 by barrettruth · 0 comments
Owner

Context

justinmk recently shipped CI-log viewing in guh.nvim (his fork of ghlite.nvim, GitHub-only). It's a much smaller surface than ours but takes a few deliberate design decisions that are worth evaluating against forge.nvim's implementation.

The "recent solution" to dig into:

How guh.nvim does it

Entry point is dl from the PR view (<Plug>(guh-logs) -> pr.show_ci_logs). The whole feature is ~100 lines:

  1. Job discovery in one call. get_pr_ci_jobs_logs hits gh api --paginate --slurp repos/{owner}/{repo}/commits/{head_sha}/check-runs?filter=latest&per_page=100, keeps app.slug == 'github-actions', and extracts the Actions job id from the check-run's details_url (/job/(%d+)). filter=latest collapses re-runs server-side, so for the head commit this is usually a single page covering the whole matrix. Dedupes by job name keeping the latest startedAt.
  2. vim.ui.select picker formatted as [<conclusion|status>] <name>, skipping skipped jobs.
  3. Raw single-job log endpoint. get_pr_ci_logs fetches gh api repos/{owner}/{repo}/actions/jobs/{job_id}/logs — deliberately NOT `gh run view --log`, with the comment: "avoid gh's {workflow} / {job} {step} prefix". Then strips a leading UTF-8 BOM the REST API prepends.
  4. Renders through a real terminal. It creates a scratch buffer, nvim_open_term, and nvim_chan_send(chan, logs) — letting Neovim's terminal emulator handle ANSI/termcodes natively. No custom ANSI parser, no folding, no step structure.
  5. Gotchas captured in fixes: vim.json.decode turning JSON null into vim.NIL (truthy!) broke the status sort comparator -> fixed with luanil (ca8b3ac); the BOM strip (bd2c42c).

How forge.nvim does it today

We are far more capable (multi-forge, structured render) but also more machinery:

  • GitHub log fetch goes through `gh run view -R --job --log | tail -n N` — lua/forge/backends/github.lua check_log_cmd (~L428-437). This is exactly the command that emits the {workflow} / {job} {step}\t<ts> <content> prefix.
  • Because of that prefix, lua/forge/log/parser/github.lua has to parse tab-separated JOB\tSTEP\tTIMESTAMP CONTENT, with per-backend parsers (log/parser/{github,gitlab,forgejo}.lua), an SGR/ANSI -> highlight map in log/parser/common.lua, plus folding/durations/structure in log/render.lua.
  • PR checks -> job resolution lives in lua/forge/pr/checks.lua (inspect_check), pulling run_id/job_id out of the check link with regexes, with live-tail + auto-refresh on top.

What's worth evaluating / borrowing

  1. check-runs?filter=latest for the PR head commit. This is a notably cleaner path to "the CI for this PR's head commit" than our gh run list + summary normalization, and it gets the whole matrix with rerun-collapsing in one paginated call. Could simplify our GitHub PR-checks -> job-id resolution (we currently regex it out of details_url/link after the fact anyway).
  2. Raw actions/jobs/{id}/logs endpoint instead of gh run view --log. Switching our GitHub check_log_cmd/run_log_cmd to the raw endpoint would remove the {workflow}/{job}/{step} prefix at the source — meaning a big chunk of parser/github.lua exists only to undo noise gh adds. Worth measuring how much parser fragility we could delete. (Note the BOM gotcha if we do.)
  3. Terminal-emulator rendering as an option/fallback. nvim_open_term gives perfect ANSI/termcode fidelity for free. We deliberately went the other way (structured forgelog buffer w/ folds, step durations, failure nav) and recently removed CI-log error-jump heuristics (#779/#780) — so this is a genuine tradeoff, not a clear win. But a terminal-render fallback might be more robust for formats our parsers don't model well.
  4. vim.ui.select job picker. Lightweight, integrates with whatever picker the user already has, vs. our checks-list buffer + cursor/name-filter. Possibly a nice alternate entry point.
  5. The luanil / vim.NIL sort crash is a good cautionary tale for our own vim.json.decode call sites.

Where we're already ahead (keep in mind)

Multi-forge support (GitHub/GitLab/Forgejo), structured + folded rendering with step durations, live tail + auto-refresh for in-progress runs, and per-backend parsers. The goal here is to cherry-pick techniques, not to flatten our feature set to match a GitHub-only plugin.

Next steps

  • Prototype GitHub raw actions/jobs/{id}/logs fetch and measure how much of parser/github.lua it lets us drop
  • Decide whether check-runs?filter=latest should back our GitHub PR-checks/job resolution
  • Evaluate a nvim_open_term render path (option or fallback) vs. the structured forgelog buffer
  • Audit vim.json.decode call sites for the vim.NIL-is-truthy footgun (luanil)
## Context justinmk recently shipped CI-log viewing in [guh.nvim](https://github.com/justinmk/guh.nvim) (his fork of `ghlite.nvim`, GitHub-only). It's a much smaller surface than ours but takes a few deliberate design decisions that are worth evaluating against `forge.nvim`'s implementation. **The "recent solution" to dig into:** - Feature commit: https://github.com/justinmk/guh.nvim/commit/479a3d89494c24d2d8b0759b82f0ec208e057b2d (`feat: get CI logs for a PR`) - Merged via PR #12 — https://github.com/justinmk/guh.nvim/pull/12 — closing https://github.com/justinmk/guh.nvim/issues/11 - Recent follow-up fixes worth reading for the gotchas: `819d9ab` (avoid gh's `{workflow} / {job} {step}` line prefix, #13), `bd2c42c` (strip UTF-8 BOM), `ca8b3ac` (2026-06-07, `luanil` fix for a `vim.NIL` sort crash) - Current source: - jobs + single-job log: https://github.com/justinmk/guh.nvim/blob/99cde3b545ccba2a8d9e7d214f95bb3f626ec245/lua/guh/gh.lua#L429-L536 - picker + render: https://github.com/justinmk/guh.nvim/blob/99cde3b545ccba2a8d9e7d214f95bb3f626ec245/lua/guh/pr.lua#L536-L574 ## How guh.nvim does it Entry point is `dl` from the PR view (`<Plug>(guh-logs)` -> `pr.show_ci_logs`). The whole feature is ~100 lines: 1. **Job discovery in one call.** `get_pr_ci_jobs_logs` hits `gh api --paginate --slurp repos/{owner}/{repo}/commits/{head_sha}/check-runs?filter=latest&per_page=100`, keeps `app.slug == 'github-actions'`, and extracts the Actions **job id from the check-run's `details_url`** (`/job/(%d+)`). `filter=latest` collapses re-runs server-side, so for the head commit this is usually a single page covering the whole matrix. Dedupes by job name keeping the latest `startedAt`. 2. **`vim.ui.select` picker** formatted as `[<conclusion|status>] <name>`, skipping `skipped` jobs. 3. **Raw single-job log endpoint.** `get_pr_ci_logs` fetches `gh api repos/{owner}/{repo}/actions/jobs/{job_id}/logs` — deliberately NOT \`gh run view --log\`, with the comment: *"avoid gh's {workflow} / {job} {step} prefix"*. Then strips a leading UTF-8 BOM the REST API prepends. 4. **Renders through a real terminal.** It creates a scratch buffer, `nvim_open_term`, and `nvim_chan_send(chan, logs)` — letting Neovim's terminal emulator handle ANSI/termcodes natively. No custom ANSI parser, no folding, no step structure. 5. **Gotchas captured in fixes:** `vim.json.decode` turning JSON `null` into `vim.NIL` (truthy!) broke the status sort comparator -> fixed with `luanil` (`ca8b3ac`); the BOM strip (`bd2c42c`). ## How forge.nvim does it today We are far more capable (multi-forge, structured render) but also more machinery: - GitHub log fetch goes through \`gh run view <id> -R <repo> --job <id> --log | tail -n N\` — `lua/forge/backends/github.lua` `check_log_cmd` (~L428-437). This is exactly the command that emits the `{workflow} / {job} {step}\t<ts> <content>` prefix. - Because of that prefix, `lua/forge/log/parser/github.lua` has to parse tab-separated `JOB\tSTEP\tTIMESTAMP CONTENT`, with per-backend parsers (`log/parser/{github,gitlab,forgejo}.lua`), an SGR/ANSI -> highlight map in `log/parser/common.lua`, plus folding/durations/structure in `log/render.lua`. - PR checks -> job resolution lives in `lua/forge/pr/checks.lua` (`inspect_check`), pulling `run_id`/`job_id` out of the check `link` with regexes, with live-tail + auto-refresh on top. ## What's worth evaluating / borrowing 1. **`check-runs?filter=latest` for the PR head commit.** This is a notably cleaner path to "the CI for *this PR's head commit*" than our `gh run list` + summary normalization, and it gets the whole matrix with rerun-collapsing in one paginated call. Could simplify our GitHub PR-checks -> job-id resolution (we currently regex it out of `details_url`/`link` after the fact anyway). 2. **Raw `actions/jobs/{id}/logs` endpoint instead of `gh run view --log`.** Switching our GitHub `check_log_cmd`/`run_log_cmd` to the raw endpoint would remove the `{workflow}/{job}/{step}` prefix at the source — meaning a big chunk of `parser/github.lua` exists only to undo noise `gh` adds. Worth measuring how much parser fragility we could delete. (Note the BOM gotcha if we do.) 3. **Terminal-emulator rendering as an option/fallback.** `nvim_open_term` gives perfect ANSI/termcode fidelity for free. We deliberately went the other way (structured `forgelog` buffer w/ folds, step durations, failure nav) and recently *removed* CI-log error-jump heuristics (#779/#780) — so this is a genuine tradeoff, not a clear win. But a terminal-render fallback might be more robust for formats our parsers don't model well. 4. **`vim.ui.select` job picker.** Lightweight, integrates with whatever picker the user already has, vs. our checks-list buffer + cursor/name-filter. Possibly a nice alternate entry point. 5. **The `luanil` / `vim.NIL` sort crash** is a good cautionary tale for our own `vim.json.decode` call sites. ## Where we're already ahead (keep in mind) Multi-forge support (GitHub/GitLab/Forgejo), structured + folded rendering with step durations, live tail + auto-refresh for in-progress runs, and per-backend parsers. The goal here is to cherry-pick techniques, not to flatten our feature set to match a GitHub-only plugin. ## Next steps - [ ] Prototype GitHub raw `actions/jobs/{id}/logs` fetch and measure how much of `parser/github.lua` it lets us drop - [ ] Decide whether `check-runs?filter=latest` should back our GitHub PR-checks/job resolution - [ ] Evaluate a `nvim_open_term` render path (option or fallback) vs. the structured `forgelog` buffer - [ ] Audit `vim.json.decode` call sites for the `vim.NIL`-is-truthy footgun (`luanil`)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
barrettruth/forge.nvim#784
No description provided.