labctl pr logs requests the wrong run attempt and misreports the cause as a log-adapter shape mismatch #266

Open
opened 2026-08-03 01:31:07 +02:00 by dominik.polakovics · 0 comments

Found while landing PR #264, where labctl pr logs could not retrieve the logs of a failing required check and pointed the wrong way for the fix.

Symptom

$ labctl pr logs 264
labctl pr logs: tracker: forge log route did not answer the shape lab's log adapter expects:
GET /Cloonar/coding-lab/actions/runs/358/jobs/0/attempt/2/logs answered 500 "text/plain"
for check "ci / native (pull_request)" — lab's Forgejo log adapter does not match this forge
version; file an issue on coding-lab, then debug from local repro

Same result with --check "ci / native (pull_request)".

What is actually happening

The forge serves the log fine — on attempt/1:

attempt 1 → http=200 size=52026
attempt 2 → http=500 size=62
attempt 3 → http=500 size=62

Attempt 1's body is the complete 52 KB job log, and its timestamps (22:59:04 → 23:08:07, ≈9m03s) match the Failing after 9m7s that labctl pr checks reported for that same run — so attempt 1 is the run that failed. labctl asked for an attempt that does not exist.

Two defects, and the second is the more expensive one:

  1. Wrong attempt number. labctl derived attempt/2 for a run whose only attempt is 1.
  2. The error misdiagnoses itself. A 500 is reported as "did not answer the shape lab's log adapter expects" / "lab's Forgejo log adapter does not match this forge version". Nothing about the response shape was wrong — the route was wrong. The message sends the reader off to check adapter/forge-version compatibility and to "debug from local repro", when the actual fix is a URL the tool already had the information to build.

Prior art: commit 84f8fc5 ("chore(ci): retrigger checks after runner outage") records the same attempt/2-serves-no-logs behaviour from a different angle, so the forge returning nothing useful for a non-existent or reaped attempt is a recurring shape worth handling deliberately rather than per-incident.

Impact

CI logs are how a lander turns a red required check into an actionable finding. When this fails, the fallback is either reproducing the whole gate locally — impossible in a session with no Go/Node toolchain — or bypassing labctl entirely with curl against the forge, which is what unblocked #264. That defeats the point of the verb.

Acceptance criteria

  • labctl pr logs <N> retrieves the log for the attempt the failing check actually ran (verify against a PR with a single-attempt run and one with a genuine re-run — #264's run 358 is a live single-attempt case)
  • When the resolved attempt has no retrievable log, fall back to the attempts that do rather than failing outright
  • A transport failure (500, 404, timeout) is reported as what it is — status code and URL — and is no longer described as a response-shape or adapter-version mismatch; keep that wording for the case where the body genuinely does not parse
  • The remediation hint matches the diagnosis (a 500 on one attempt is not "file an issue about the adapter, then debug from local repro")
  • Tests cover attempt resolution and the no-log-for-this-attempt fallback

Reproduction

labctl pr logs 264                                    # the misleading failure (PR #264, run 358)
curl -sS -o /dev/null -w '%{http_code}\n' \
  https://git.cloonar.com/Cloonar/coding-lab/actions/runs/358/jobs/0/attempt/1/logs   # 200
curl -sS -o /dev/null -w '%{http_code}\n' \
  https://git.cloonar.com/Cloonar/coding-lab/actions/runs/358/jobs/0/attempt/2/logs   # 500
Found while landing PR #264, where `labctl pr logs` could not retrieve the logs of a failing required check and pointed the wrong way for the fix. ## Symptom ``` $ labctl pr logs 264 labctl pr logs: tracker: forge log route did not answer the shape lab's log adapter expects: GET /Cloonar/coding-lab/actions/runs/358/jobs/0/attempt/2/logs answered 500 "text/plain" for check "ci / native (pull_request)" — lab's Forgejo log adapter does not match this forge version; file an issue on coding-lab, then debug from local repro ``` Same result with `--check "ci / native (pull_request)"`. ## What is actually happening The forge serves the log fine — on **attempt/1**: ``` attempt 1 → http=200 size=52026 attempt 2 → http=500 size=62 attempt 3 → http=500 size=62 ``` Attempt 1's body is the complete 52 KB job log, and its timestamps (22:59:04 → 23:08:07, ≈9m03s) match the `Failing after 9m7s` that `labctl pr checks` reported for that same run — so attempt 1 **is** the run that failed. labctl asked for an attempt that does not exist. Two defects, and the second is the more expensive one: 1. **Wrong attempt number.** labctl derived `attempt/2` for a run whose only attempt is 1. 2. **The error misdiagnoses itself.** A `500` is reported as *"did not answer the shape lab's log adapter expects"* / *"lab's Forgejo log adapter does not match this forge version"*. Nothing about the response shape was wrong — the route was wrong. The message sends the reader off to check adapter/forge-version compatibility and to "debug from local repro", when the actual fix is a URL the tool already had the information to build. Prior art: commit `84f8fc5` ("chore(ci): retrigger checks after runner outage") records the same attempt/2-serves-no-logs behaviour from a different angle, so the forge returning nothing useful for a non-existent or reaped attempt is a recurring shape worth handling deliberately rather than per-incident. ## Impact CI logs are how a lander turns a red required check into an actionable finding. When this fails, the fallback is either reproducing the whole gate locally — impossible in a session with no Go/Node toolchain — or bypassing labctl entirely with `curl` against the forge, which is what unblocked #264. That defeats the point of the verb. ## Acceptance criteria - [ ] `labctl pr logs <N>` retrieves the log for the attempt the failing check actually ran (verify against a PR with a single-attempt run and one with a genuine re-run — #264's run 358 is a live single-attempt case) - [ ] When the resolved attempt has no retrievable log, fall back to the attempts that do rather than failing outright - [ ] A transport failure (500, 404, timeout) is reported as what it is — status code and URL — and is no longer described as a response-shape or adapter-version mismatch; keep that wording for the case where the body genuinely does not parse - [ ] The remediation hint matches the diagnosis (a 500 on one attempt is not "file an issue about the adapter, then debug from local repro") - [ ] Tests cover attempt resolution and the no-log-for-this-attempt fallback ## Reproduction ``` labctl pr logs 264 # the misleading failure (PR #264, run 358) curl -sS -o /dev/null -w '%{http_code}\n' \ https://git.cloonar.com/Cloonar/coding-lab/actions/runs/358/jobs/0/attempt/1/logs # 200 curl -sS -o /dev/null -w '%{http_code}\n' \ https://git.cloonar.com/Cloonar/coding-lab/actions/runs/358/jobs/0/attempt/2/logs # 500 ```
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
Cloonar/coding-lab#266
No description provided.