Files
orca/src/shared/gitlab-job-log-excerpt.ts
T
NeilandOrca 0927b9c156 fix(gitlab): load pipeline job traces in the Checks side panel (#7732) (#12266)
* test(repro): demonstrate #7732 GitLab pipeline job details never load in Checks panel

Co-authored-by: Orca <help@stably.ai>

* fix(gitlab): load pipeline job traces in the Checks side panel (#7732)

Expanding a GitLab pipeline job in the Checks panel always showed
"No inline details are available for this check.": the mapper dropped the
numeric job id, `PRCheckDetail` had nowhere to carry it, and every consumer
called the GitHub check-runs API, which returns null for a GitLab job.

- carry `gitlabJobId` on `PRCheckDetail` and add the `gitlab-job:` branch to
  all three identity ladders (panel rows, editor tabs, fix-prompt keys) so
  same-stage jobs with no web_url stop colliding
- add a runtime-routed trace client so SSH/remote workspaces work, not just
  local IPC, and thread the MR's `projectRef` for fork pipelines
- bound the trace in main via the existing `sliceCheckLogTail` (now shared,
  not GitHub-only) so a multi-megabyte CI log never crosses the 1 MB
  transport frame cap; strip ANSI/section markers up to the CR only, which
  keeps each section's visible header and command echo
- render the excerpt inline instead of "Log tail available in full details."
- feed GitLab traces to "Fix with AI", which previously sent bare check names
- skip the fetch for jobs that cannot have a trace (created/manual/skipped)
  so GitLab's 404 does not replace the benign empty state, and re-arm a
  failed load when the job's state changes since the panel has no retry

Co-authored-by: Orca <help@stably.ai>

* fix(gitlab): treat a missing job log as an empty log, not an error (#7732)

Round-1 review follow-up.

- a job canceled before it started (or whose log was erased/expired) is
  `completed`/`cancelled`, so the panel fetched its trace, GitLab answered 404,
  and `classifyGlabError`'s issue-edit copy ("Issue not found — it may have been
  deleted.") landed verbatim on the auto-expanded check row; main now maps that
  404 to an empty trace so the row keeps its benign empty state
- keep a missing project a real error (GitLab masks unauthorized projects as
  404) and add `classifyJobLogError` so 403/unknown failures stop borrowing
  issue-edit wording on a job-log read
- broaden the empty-log copy in all five catalogs: it now covers erased and
  expired logs, not only jobs that never ran
- e2e: derive the repro screenshot dir from `process.cwd()` (or an env
  override) instead of a hardcoded POSIX path to a throwaway worktree
- bound the raw trace before the ANSI/section passes so a multi-megabyte log
  is not scanned in full on the main-process event loop
- drop the redundant `if (repo)` in `handleFixChecksWithAI` and the now-dead
  "Log tail available in full details." catalog entry

Co-authored-by: Orca <help@stably.ai>

* fix(gitlab): address review — project ref on reload, retry re-arm, IPC timeout

- Carry the MR's GitLab project ref on the check-details tab so reloading a
  fork/cross-project job tab fetches the trace from the pipeline's own project.
- Re-arm the sidebar retry when a details load resolves to null, not only when
  it throws; a detail-less row otherwise never retried after the job moved on.
- Bound the local `gl.jobTrace` IPC call with the same 30s timeout the runtime
  RPC path uses — glab runs without a subprocess timeout in main.
- Document that the trace 404 -> empty-log mapping is deliberately broad.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-03 22:48:37 -07:00

53 lines
2.5 KiB
TypeScript

import { stripAnsiEscapeSequences } from './ansi-escape-sequences'
import { sliceCheckLogTail } from './check-job-log-tail-slice'
import type { GitLabJobTraceResult } from './gitlab-types'
// GitLab wraps collapsible sections in `section_start:<unix>:<name>\r<CSI>0K<visible header>`;
// strip only through the CR so the human-readable header on the same line survives.
const SECTION_MARKER_PREFIX_PATTERN = /^section_(?:start|end):\d+:[^\r\n]*?\r/gm
// Sections without a visible header leave a marker-only line; drop it whole.
const SECTION_MARKER_LINE_PATTERN = /^section_(?:start|end):\d+:[^\r\n]*\n?/gm
// Why: stripping makes several full passes over the input, so a multi-megabyte CI log
// would stall the main-process event loop before the tail is trimmed to 16 KB anyway.
// 512 K chars is far more than the excerpt can keep, even with markup removed.
const MAX_RAW_TRACE_CHARS = 512 * 1024
function rawTraceTail(trace: string): string {
if (trace.length <= MAX_RAW_TRACE_CHARS) {
return trace
}
const tail = trace.slice(trace.length - MAX_RAW_TRACE_CHARS)
// Drop the partial first line so a marker or escape cut in half cannot survive stripping.
const firstLineBreak = tail.indexOf('\n')
return firstLineBreak === -1 ? tail : tail.slice(firstLineBreak + 1)
}
/**
* Turn a raw `glab api .../jobs/:id/trace` body into a bounded, readable excerpt.
*
* Bounding is not cosmetic: the runtime RPC transports drop frames over 1 MB
* (`MAX_WS_MESSAGE_BYTES` / `MAX_RELAY_MESSAGE_BYTES`), so a multi-megabyte CI
* log must be cut before it crosses the wire, not after.
*/
export function gitLabJobTraceToLogExcerpt(trace: string): string {
if (!trace) {
return ''
}
// Order matters: ANSI first so the erase-to-EOL sequence between the marker's CR
// and its header text is gone, CR normalisation last so the marker patterns can
// still see the CR that separates a marker from its visible header.
const readable = stripAnsiEscapeSequences(rawTraceTail(trace))
.replace(SECTION_MARKER_PREFIX_PATTERN, '')
.replace(SECTION_MARKER_LINE_PATTERN, '')
// Why: progress output redraws with bare CR, which would otherwise make the
// whole job log a single line and defeat the line-based tail.
.replace(/\r\n?/g, '\n')
return sliceCheckLogTail(readable).trim()
}
/** Applied in main so only the excerpt crosses IPC / the runtime RPC socket. */
export function toGitLabJobLogExcerptResult(result: GitLabJobTraceResult): GitLabJobTraceResult {
return result.ok ? { ok: true, trace: gitLabJobTraceToLogExcerpt(result.trace) } : result
}