Repository navigation
feat: show why CI failed — failed job log excerpts (--logs, MCP get_failed_logs) - #21
Conversation
`prw checks <PR_URL>` shows each check run and commit status of the PR's head commit with its result, how long it took and a link: failures first, then cancelled and running ones, then passed, neutral and skipped. Superseded runs of the same check are left out, as they don't count towards the result. The header has the short SHA, overall state, counts per bucket and merge state. `--json` prints the same as an object with a `checks` array. Exit codes match `prw wait` (0 passed, 1 failed, 3 error, 4 no CI), plus 8 while checks are still running, like `gh pr checks`. Verified live on devblac/docker-lint#5, zephyrproject-rtos/zephyr#105183 (35 checks; the cancelled run superseded by a newer one is hidden) and #13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_failed_logs`) For each failed GitHub Actions job, prw now downloads the job log and extracts the output of the step that failed: its last lines up to the first `##[error]`, starting after the step's "Run ..." header, with timestamps, ANSI colors and runner markup (groups, composite action boundaries, debug) stripped, and errors/warnings rendered as "Error:" / "Warning:". That's what you'd scroll to in the browser. - `prw checks --logs` and `prw wait --logs` (`--log-lines`, default 40) print the excerpts after the result; `logs` / `failed_logs` in `--json`. - MCP tool `get_failed_logs` returns them to agents (text rendered for reading, data in structuredContent); `get_ci_status` and `wait_for_ci` point to it on failure. - Bounded: only the tail (4 MiB) of a log is kept, at most 5 logs per call, superseded and cancelled runs are skipped. Logs aren't cached. - Checks from other CI systems and failed commit statuses are listed with their link, since the API has no log for them; expired logs (404/410) and a missing "Actions: read" permission get a clear message. - The log download redirects to blob storage; Go drops the Authorization header on that cross-host redirect (tested). Parser tested on a real staticcheck failure log (devblac/go-semver-audit). Verified live on cloudposse/atmos#3476 (action download failure), #3422 (git fetch failure, matrix job), #2199 (failed commit status) and over MCP stdio. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
It exited 0, so `prw checks "$PR" && ...` went ahead on an abandoned PR, and the exit codes didn't match `prw wait` as documented. Addresses Codex review on #20. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit only updated the README; this is the code change: a PR closed without merging now exits 3 (like `prw wait`) instead of 0. Addresses Codex review on #20. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49854218e4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for i, l := range lines { | ||
| if strings.HasPrefix(l, "##[error]") { | ||
| errAt = i | ||
| break |
There was a problem hiding this comment.
Select errors from the step that actually failed
If an earlier successful step emits an error annotation, this scan selects that annotation and excludes the later step that actually caused the job to fail. GitHub's Actions toolkit documentation explicitly notes that core.error() can annotate an error while the action still succeeds. Consequently, both --logs and get_failed_logs can return an unrelated successful step as the failure explanation. Identify the failed step before choosing its error marker, rather than assuming the first annotation determines the job's outcome.
Useful? React with 👍 / 👎.
| logCtx, cancelLogs := context.WithTimeout(context.Background(), time.Minute) | ||
| result.FailedLogs, err = fetchFailedLogs(logCtx, client, owner, repo, result.SHA, waitLogLines) |
There was a problem hiding this comment.
Preserve signal cancellation while downloading failure logs
When wait --logs is downloading logs, Ctrl+C or SIGTERM only cancels the wait context handled by the signal goroutine above; this new background context remains active. Since signal.Notify is still installed, the signal no longer terminates the process either, leaving the command downloading until completion or the one-minute deadline and eventually returning the CI failure code instead of the documented interruption code. Keep signal cancellation connected to log retrieval even if its timeout must be independent of the polling timeout.
Useful? React with 👍 / 👎.
A job that failed and then passed when its workflow run was re-run is flaky:
same commit, same code, different result. `prw flaky <owner/repo | URL>`
scans the repository's last --runs workflow runs (default 300, max 1000),
fetches the jobs of every attempt of the re-run ones, and reports each job
that failed (failure / timed_out) in one attempt and succeeded in a later one,
grouped by workflow and job: how many runs it flaked in, when last, and the
latest failed attempt's link. Cancelled jobs (fail-fast victims) don't count;
jobs that kept failing aren't flaky. `--json` for scripts.
The MCP tool `get_flaky_checks` gives agents the same, so they can tell a
known-flaky failure from one their change caused.
GitHub takes seconds per runs/jobs request on large repositories, so pages
and attempts are fetched in parallel (4 at a time) and runs are listed
without their pull_requests payload: grafana/grafana went from 24s to 9s.
Verified live: grafana/grafana (4 flaky jobs in 300 runs), hashicorp/terraform
("Unit Tests"), devblac/prw (none) and over MCP stdio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: find flaky checks (prw flaky, MCP get_flaky_checks)
captureStdout restored os.Stdout right after the pipe was drained, while the goroutine running the command had read os.Stdout earlier. Closing a pipe isn't a synchronization point for the race detector, so `go test -race` on CI flagged TestChecksCmd_PendingExitCode (main went red after #21). os.Stdout is now restored after receiving the command's error from the channel, which orders the two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Roadmap #15. For each failed GitHub Actions job, prw downloads the log and shows the output of the step that failed — what you'd scroll to in the browser.
What
github.ExtractFailure): from the step'sRun …header to the first##[error](consecutive errors kept together), last--log-lineslines (default 40); timestamps, ANSI colors and runner markup (##[group],##[start-action …],##[debug]) stripped;##[error]/##[warning]/##[notice]→Error:/Warning:/Notice:. No error marker → end of the log.prw checks --logs,prw wait --logs(only on failure);logs/failed_logsin--json.get_failed_logs(max_lines, max 200): text has the rendered excerpts (not duplicated as JSON), structuredContent has the data.get_ci_status/wait_for_cidescriptions point to it on failure.Authorizationon the cross-host redirect — covered by a test.Test plan
go test ./... -race,go vet ./...testdata/, from devblac/go-semver-audit), long output, no marker, consecutive errors, composite-action markup, tail reading of large streamswait --logstext and JSON,checks --logs, no download without the flag, failed commit statuses listedget_failed_logsover stdio with the real binaryStacked on #20.
🤖 Generated with Claude Code