Turn a failing CI log into three useful sentences, using DigitalOcean serverless inference.
A failing job log is thousands of lines of things that worked and a few dozen that did not. This finds the few dozen, sends only those, and tells you what broke, why, and what to try first.
export DO_INFERENCE_KEY=...
cat build.log | npx ci-log-triage --stats**What failed:** Applying migration `20260727130000_team_scoped_unique_constraints`
failed because the PostgreSQL relation `Segment` does not exist.
**Why:** The migration references the table `Segment`, but PostgreSQL reports
`ERROR: relation "Segment" does not exist`. Either the table was never created,
was dropped, or the name differs (case sensitivity, or a mis-named migration).
**Try this first:** Open the migration file and check which statement uses
`Segment`, then verify the table exists with `psql -c "\d"`.
---
openai-gpt-oss-20b · 4949ms · 753 prompt + 527 completion tokens ·
log reduced 92.5% (272 lines to 26)
That output is from a real failing deploy, not a mock.
Sending the whole log works and is the obvious thing to do. It is also worse in both directions: you pay for every token, and the actual error gets buried in dependency-resolution noise, which makes the answer less accurate.
src/extract.mjs does the reduction:
- strips the per-line
job⇥step⇥timestampprefixes GitHub adds, and ANSI colour codes, both of which repeat on every line and cost tokens - keeps a window around every error signal, plus the tail of the log, where failures usually land
- marks the gaps (
... 41 lines omitted ...) so the model does not assume two unrelated lines are adjacent
On the example above that is 25,534 characters down to 1,926.
ci-log-triage build.log # from a file
cat build.log | ci-log-triage # from stdin
ci-log-triage build.log --stats # add latency, tokens, reduction
ci-log-triage build.log --json # machine-readable
ci-log-triage build.log --model llama3.3-70b-instruct--help lists everything.
Reasoning models need headroom. openai-gpt-oss-20b puts its thinking in reasoning_content and the answer in content. Set max_tokens too low and reasoning consumes the whole budget: you get HTTP 200, finish_reason: "length", and an empty string, which looks exactly like a broken API key. The client raises a specific error for that case rather than letting you debug it twice.
Commercial models are tier-gated. Requesting an Anthropic model on a base account returns 403 this model is not available for your subscription tier. The open-source models work without that.
Triage must never fail the build. A tool that explains failures should not create them, so any error exits 0 with a message on stderr.
triage:
needs: build
if: always() && needs.build.result == 'failure'
runs-on: ubuntu-latest
permissions:
actions: read # to read the failing job's log
pull-requests: write # only if you want a PR comment
steps:
- uses: The-DevOps-Daily/ci-log-triage@main
with:
do-api-key: ${{ secrets.DO_INFERENCE_KEY }}
pr-number: ${{ github.event.pull_request.number }}It writes the report to the job summary, and upserts a single PR comment
rather than stacking one per run. .github/workflows/demo.yml is a job that
fails on purpose so you can watch it work.
| Variable | Purpose |
|---|---|
DO_INFERENCE_KEY |
required, a DigitalOcean model access key |
DO_INFERENCE_BASE_URL |
optional, defaults to https://inference.do-ai.run/v1 |
MIT