<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 0x3f</title>
    <description>The latest articles on DEV Community by 0x3f (@tbnlzldyd).</description>
    <link>https://dev.to/tbnlzldyd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121022%2Fc0dac363-1442-4948-ae5a-37fa302c06b4.png</url>
      <title>DEV Community: 0x3f</title>
      <link>https://dev.to/tbnlzldyd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tbnlzldyd"/>
    <language>en</language>
    <item>
      <title>I got tired of pasting CI failure logs, so I wrote a CLI that turns one failed run into six sections of context</title>
      <dc:creator>0x3f</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:01:32 +0000</pubDate>
      <link>https://dev.to/tbnlzldyd/i-got-tired-of-pasting-ci-failure-logs-so-i-wrote-a-cli-that-turns-one-failed-run-into-six-3ebd</link>
      <guid>https://dev.to/tbnlzldyd/i-got-tired-of-pasting-ci-failure-logs-so-i-wrote-a-cli-that-turns-one-failed-run-into-six-3ebd</guid>
      <description>&lt;p&gt;Every developer knows the ritual. A GitHub Actions run goes red. You click through to the failing job. You scroll past 256 lines of runner setup and caching noise to finally find the one line that actually mattered. Then you open the commit, the PR, the history — by hand, one browser tab at a time. For every single failure.&lt;/p&gt;

&lt;p&gt;A failed run is not a stack trace in isolation. It's a &lt;em&gt;fact about your repo&lt;/em&gt;: which error, which commit caused it, what the PR reviews said, and whether we've seen this exact failure before. The log gives you the first; everything else you reassemble from memory.&lt;/p&gt;

&lt;p&gt;I got tired of doing that by hand, so I wrote &lt;a href="https://github.com/TBNLZLDYD/ci-context" rel="noopener noreferrer"&gt;&lt;code&gt;ci-context&lt;/code&gt;&lt;/a&gt;: one command that takes a failed run ID and prints a single-page report with six sections — what failed, what commit did it, what the PR said, and how often we've seen this before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;uv run ci-context gh run 30432597129 --repo TBNLZLDYD/ci-context
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't AI. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with &lt;code&gt;gh run view --log-failed&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gh run view --log-failed&lt;/code&gt; is genuinely useful, but it hands you a wall of text.&lt;br&gt;
The root cause is somewhere in there, guaranteed, but so is a hundred lines of runner bootstrap. Errors come out in the order the shell printed them, not in the order they matter. And it only ever shows you the log — the commit, PR, and history context are separate queries you make yourself.&lt;/p&gt;
&lt;h2&gt;
  
  
  The six sections
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ci-context&lt;/code&gt; assembles everything relevant around the failure into one report:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run Overview&lt;/strong&gt; — what run, what triggered it, which commit, duration, attempt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extracted Errors&lt;/strong&gt; — the &lt;em&gt;root-causing&lt;/em&gt; lines, pulled from the tail of the log and deduplicated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit Context&lt;/strong&gt; — the commit that triggered the run, its message, author, and changed files with add/delete counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR Context&lt;/strong&gt; — the PR behind it (when the event is a pull request), its title, reviews, and comments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History Pattern&lt;/strong&gt; — has this error occurred before? Tracked by fingerprint across the last N runs, with a failure-rate trend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick Actions&lt;/strong&gt; — copy-pasteable &lt;code&gt;gh&lt;/code&gt; commands to investigate or rerun.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real output for a real failed run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╭─ CI Failure Report ──────────── TBNLZLDYD/ci-context ─╮
│ Run #30432597129 · CI                                 │
│ Conclusion: failure                                   │
╰───────────────────────────────────────────────────────╯

Extracted Errors (2 found)
[high] Ruff lint error - E501 Line too long (116 &amp;gt; 100)
  File: src/ci_context/github/exceptions.py:54
  Step: test (3.12)
[medium] GHA exit code - Process completed with exit code 1

Commit Context
ef1b4e7 - docs: fix outdated documentation across CLAUDE.md, README, and source code
Author: TBNLZLDYD
  src/ci_context/github/exceptions.py  +1 -1

History Pattern (29 runs analyzed)
[exact] 7bf49b3c8fe94c39  Occurred 6 times  First: 2026-07-20 · Last: 2026-07-23
Failure rate: 17% overall · 0% recent · trend: decreasing

Quick Actions
  gh run rerun 30432597129 --failed
  gh repo view TBNLZLDYD/ci-context --commit ef1b4e7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See what happened there? The root cause is the first thing you read, not the last thing you scroll to. The recurring fingerprint is flagged &lt;code&gt;[exact]&lt;/code&gt; with a count.&lt;br&gt;
There are actions you can run without opening a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no AI
&lt;/h2&gt;

&lt;p&gt;When every tool is bolting on an LLM, the interesting differentiation is sometimes to &lt;em&gt;not&lt;/em&gt;. &lt;code&gt;ci-context&lt;/code&gt; is deliberately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic&lt;/strong&gt; — the same run always produces the same report. No model, no temperature, no drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero cost&lt;/strong&gt; — it's a few PyGithub and httpx calls against the public GitHub API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-first&lt;/strong&gt; — your logs never leave your machine except to GitHub itself. No third-party service sees them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditable&lt;/strong&gt; — the extraction is regular expressions over normalised log text. You can read every pattern in &lt;a href="https://github.com/TBNLZLDYD/ci-context/blob/main/src/ci_context/analysis/patterns.py" rel="noopener noreferrer"&gt;&lt;code&gt;patterns.py&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regex over AI isn't a downgrade here; it's a feature. The language of CI failures is repetitive and well-bounded (tracebacks, &lt;code&gt;npm ERR!&lt;/code&gt;, &lt;code&gt;Error:&lt;/code&gt;, &lt;code&gt;FAILED&lt;/code&gt;), so a deterministic extractor is both fast and explainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the error extraction works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Normalise&lt;/strong&gt; — strip ANSI codes, GitHub Actions &lt;code&gt;##[section]&lt;/code&gt; and &lt;code&gt;::group::&lt;/code&gt;/&lt;code&gt;::endgroup::&lt;/code&gt; markers, and timestamp prefixes. Preserve line numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match&lt;/strong&gt; — scan the log &lt;em&gt;tail-first&lt;/em&gt; against per-language error patterns (Python, Node, Go, Java, Shell). Each pattern has a start, a message, a location (file:line), and an end condition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicate&lt;/strong&gt; — collapse repeated identical errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence&lt;/strong&gt; — tag each extraction high / medium / low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap&lt;/strong&gt; — at ten errors, so a noisy job never drowns the signal.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Fingerprinting: turning an error into an identity
&lt;/h2&gt;

&lt;p&gt;To ask "have we seen this before?", an error needs an identity stable enough to survive minor variation. &lt;code&gt;ci-context&lt;/code&gt; fingerprints each error by normalising away the volatile bits — numbers → &lt;code&gt;&amp;lt;NUM&amp;gt;&lt;/code&gt;, paths → &lt;code&gt;&amp;lt;ROOT&amp;gt;/&lt;/code&gt;, commit SHAs → &lt;code&gt;&amp;lt;SHA&amp;gt;&lt;/code&gt; — then lowercasing and taking a hash.&lt;/p&gt;

&lt;p&gt;It's the same idea as &lt;code&gt;eslint&lt;/code&gt;'s rules reusing a stable rule ID, or crash reporters grouping by stack-frame signature. Once you have a fingerprint, the matcher can compare it across the last N runs: exact → &lt;code&gt;[EXACT]&lt;/code&gt;, Levenshtein similar → &lt;code&gt;[SIMILAR]&lt;/code&gt;, otherwise &lt;code&gt;[NEW]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other plumbing worth mentioning
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite cache&lt;/strong&gt; (&lt;code&gt;~/.cache/ci-context/history.db&lt;/code&gt;) stores fingerprints and run metadata so a warm history scan doesn't re-fetch runs it's already fingerprinted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two HTTP engines under one client&lt;/strong&gt; — PyGithub for the typed API, httpx for job-log downloads PyGithub doesn't support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token resolution&lt;/strong&gt; is explicit: &lt;code&gt;--token&lt;/code&gt; → config file → &lt;code&gt;gh auth token&lt;/code&gt;. No magic from environment variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.11+&lt;/strong&gt;, strict &lt;code&gt;mypy&lt;/code&gt;, &lt;code&gt;ruff&lt;/code&gt; clean, 348 tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest trade-off
&lt;/h2&gt;

&lt;p&gt;It only speaks to &lt;em&gt;failed&lt;/em&gt; runs, and it reads GitHub Actions specifically — there's no GitLab / CircleCI support yet. The value is proportional to how much Actions you run and how often you debug it. For a solo maintainer or a small team running CI every push, that's a lot of saved tab-switching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ci-context
ci-context gh run &amp;lt;your-failed-run-id&amp;gt; &lt;span class="nt"&gt;--repo&lt;/span&gt; owner/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or build from source — the &lt;a href="https://github.com/TBNLZLDYD/ci-context" rel="noopener noreferrer"&gt;README&lt;/a&gt; covers both. There are real, verified examples in &lt;a href="https://github.com/TBNLZLDYD/ci-context/blob/main/EXAMPLES.md" rel="noopener noreferrer"&gt;&lt;code&gt;EXAMPLES.md&lt;/code&gt;&lt;/a&gt; so you can see exactly what you'll get before installing anything.&lt;/p&gt;

&lt;p&gt;If you've ever spent ten minutes reconstructing a failure's context by hand, that's the gap it fills. I'm genuinely curious whether the six-section shape matches your mental model — issues and feedback welcome. What do you wish a CI failure report showed you?&lt;/p&gt;

</description>
      <category>tools</category>
      <category>cli</category>
      <category>python</category>
      <category>githubactions</category>
    </item>
  </channel>
</rss>
