<?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: Satish Kumar</title>
    <description>The latest articles on DEV Community by Satish Kumar (@satishkovuru).</description>
    <link>https://dev.to/satishkovuru</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%2F4143699%2Fc2d3b7c0-11ea-420f-b6b6-88c1d614335f.png</url>
      <title>DEV Community: Satish Kumar</title>
      <link>https://dev.to/satishkovuru</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/satishkovuru"/>
    <language>en</language>
    <item>
      <title>Detecting Anomalies in CI/CD Pipelines with ML</title>
      <dc:creator>Satish Kumar</dc:creator>
      <pubDate>Sat, 26 Sep 2026 02:13:09 +0000</pubDate>
      <link>https://dev.to/satishkovuru/detecting-anomalies-in-cicd-pipelines-with-ml-1ph3</link>
      <guid>https://dev.to/satishkovuru/detecting-anomalies-in-cicd-pipelines-with-ml-1ph3</guid>
      <description>&lt;p&gt;The pipeline fails. Again.&lt;/p&gt;

&lt;p&gt;Your CI run turns red. You open the logs, scroll through a wall of output,&lt;br&gt;
and fifteen minutes later find the answer: it's that flaky test again — the&lt;br&gt;
one everyone half-recognizes but nobody's fixed. You retry the job, it goes&lt;br&gt;
green, you move on. Multiply that by every engineer on the team, every week,&lt;br&gt;
and it adds up to real hours spent on triage that a five-second glance&lt;br&gt;
shouldn't require.&lt;/p&gt;

&lt;p&gt;The question I wanted to answer: could the pipeline tell you &lt;em&gt;this run looks&lt;br&gt;
unusual&lt;/em&gt; before a human has to dig in?&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this is harder than a red/green signal
&lt;/h2&gt;

&lt;p&gt;A single pass/fail bit isn't enough to build on. Pipelines fail for&lt;br&gt;
structurally different reasons — a flaky test, an infra hiccup, a dependency&lt;br&gt;
break, resource exhaustion — and "unusual" is relative to &lt;em&gt;that pipeline's&lt;/em&gt;&lt;br&gt;
own history, not a universal threshold. A 10-minute run might be completely&lt;br&gt;
normal for one workflow and a five-alarm anomaly for another that usually&lt;br&gt;
finishes in 30 seconds. Fixed thresholds and simple failure-rate alerts miss&lt;br&gt;
this: they treat every pipeline the same and only catch what you already&lt;br&gt;
thought to watch for.&lt;/p&gt;
&lt;h2&gt;
  
  
  The approach
&lt;/h2&gt;

&lt;p&gt;PipelineSentinel is a small, deployable layer that sits on top of existing&lt;br&gt;
CI tooling and scores each run against its own pipeline's history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data.&lt;/strong&gt; It pulls run metadata straight from the GitHub Actions REST API —&lt;br&gt;
run duration, pass/fail outcome, retry/attempt count, triggering event, and&lt;br&gt;
branch. No log-parsing required for a first pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model.&lt;/strong&gt; The baseline model is an &lt;code&gt;IsolationForest&lt;/code&gt; (scikit-learn) over&lt;br&gt;
three signals: run duration, whether the run failed, and how many attempts&lt;br&gt;
it took. IsolationForest is a good first choice here because it's&lt;br&gt;
unsupervised — it doesn't need a hand-labeled set of "here's what an anomaly&lt;br&gt;
looks like," which you don't have on day one — and it adapts to each&lt;br&gt;
pipeline's own distribution instead of a fixed global cutoff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IsolationForest&lt;/span&gt;

&lt;span class="n"&gt;FEATURE_COLUMNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IsolationForest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contamination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;contamination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_anomaly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit_predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FEATURE_COLUMNS&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each flagged run, a small rule layer explains &lt;em&gt;why&lt;/em&gt; it was flagged —&lt;br&gt;
unusual duration, a failure, retries required — so the output is&lt;br&gt;
interpretable, not just a binary flag with no story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dashboard.&lt;/strong&gt; A Streamlit app lists recent runs and flagged anomalies side&lt;br&gt;
by side, with the reason attached, so triage starts with "here's what's odd&lt;br&gt;
and why" instead of a blank log file.&lt;/p&gt;

&lt;p&gt;The data flow looks like: GitHub Actions API → ingest.py → anomaly_detector.py → scored_runs.csv → Streamlit dashboard&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've actually validated so far — and what I haven't
&lt;/h2&gt;

&lt;p&gt;This is the honest part, and it matters more than the demo.&lt;/p&gt;

&lt;p&gt;I ran the full pipeline end to end against a real (if small) dataset: 12&lt;br&gt;
workflow runs pulled from this repo's own GitHub Actions history. The model&lt;br&gt;
flagged 3 of the 12 — a run that needed three attempts and took nearly 5&lt;br&gt;
minutes (versus a normal ~30 seconds), and two genuine failed runs. The nine&lt;br&gt;
ordinary single-attempt successful runs were left alone. That's a&lt;br&gt;
correct-looking result, and it's real proof the ingestion → model →&lt;br&gt;
dashboard path works mechanically.&lt;/p&gt;

&lt;p&gt;But I'm not going to oversell it. &lt;strong&gt;12 runs is not a validation set.&lt;/strong&gt;&lt;br&gt;
IsolationForest's &lt;code&gt;contamination&lt;/code&gt; parameter tells it what &lt;em&gt;fraction&lt;/em&gt; of runs&lt;br&gt;
to flag — at n=12, it's effectively forcing the most-extreme ~20% to be&lt;br&gt;
flagged, whether or not they're truly anomalous. It happened to land on&lt;br&gt;
exactly the right runs here, which is a good sign but not proof the model&lt;br&gt;
discriminates well on its own.&lt;/p&gt;

&lt;p&gt;So I built a second check: a labeled synthetic dataset — 400 simulated runs&lt;br&gt;
with ground-truth anomaly labels across four patterns (normal runs, a&lt;br&gt;
recurring flaky test that self-heals on retry, infra duration spikes, and&lt;br&gt;
genuine first-attempt regressions) — specifically so I could measure real&lt;br&gt;
precision and recall instead of eyeballing 12 rows. Sweeping &lt;code&gt;contamination&lt;/code&gt;&lt;br&gt;
against those labels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;contamination&lt;/th&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;th&gt;recall&lt;/th&gt;
&lt;th&gt;false-positive rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.05&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.556&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.09 (true rate)&lt;/td&gt;
&lt;td&gt;0.972&lt;/td&gt;
&lt;td&gt;0.972&lt;/td&gt;
&lt;td&gt;0.003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;td&gt;0.600&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.066&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.450&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.121&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is exactly what theory predicts: set &lt;code&gt;contamination&lt;/code&gt; too low and&lt;br&gt;
the model gets conservative and misses real anomalies; set it too high and&lt;br&gt;
precision collapses under false positives. The useful number is near the&lt;br&gt;
true anomaly rate — 97% precision and 97% recall at &lt;code&gt;contamination=0.09&lt;/code&gt; on&lt;br&gt;
this synthetic set.&lt;/p&gt;

&lt;p&gt;To be clear about what that number is and isn't: it validates that the&lt;br&gt;
&lt;em&gt;model&lt;/em&gt; behaves sensibly and that &lt;code&gt;contamination&lt;/code&gt; tuning matters — a real&lt;br&gt;
methodology result. It is not a claim about real-world accuracy, because&lt;br&gt;
synthetic data by construction has patterns cleaner than a messy real&lt;br&gt;
pipeline. The real false-positive rate, and the real time-saved number,&lt;br&gt;
still only come from running this against an actual team's pipeline over&lt;br&gt;
weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The plan is to run this against a real, higher-volume pipeline, tune&lt;br&gt;
&lt;code&gt;contamination&lt;/code&gt; against an actual history instead of a guess, and capture&lt;br&gt;
real before/after numbers — time saved on triage, anomalies caught early,&lt;br&gt;
and a false-positive rate that means something. That's the follow-up&lt;br&gt;
article, with real metrics instead of a 12-run smoke test.&lt;/p&gt;

&lt;p&gt;The repo is open source: &lt;a href="https://github.com/satishkovuru/satishkovuru.github.io/tree/master/projects/pipeline-sentinel" rel="noopener noreferrer"&gt;pipeline-sentinel&lt;/a&gt;.&lt;br&gt;
Feedback, and especially anyone with a similar approach on their own&lt;br&gt;
pipelines, is welcome.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>machinelearning</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
