<?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: ProveDone</title>
    <description>The latest articles on DEV Community by ProveDone (@provedone).</description>
    <link>https://dev.to/provedone</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%2F4106919%2Fde2e9c95-76f3-48d5-a699-074b37e53d12.png</url>
      <title>DEV Community: ProveDone</title>
      <link>https://dev.to/provedone</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/provedone"/>
    <language>en</language>
    <item>
      <title>Your CI is green and your pipeline produced nothing</title>
      <dc:creator>ProveDone</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:43:33 +0000</pubDate>
      <link>https://dev.to/provedone/your-ci-is-green-and-your-pipeline-produced-nothing-213o</link>
      <guid>https://dev.to/provedone/your-ci-is-green-and-your-pipeline-produced-nothing-213o</guid>
      <description>&lt;p&gt;Two failures broke an unattended pipeline I ran for a few months, and neither of them looked like an error. I want to describe them precisely, because I spent a long time debugging the wrong thing both times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure one: exit 0, nothing on disk
&lt;/h2&gt;

&lt;p&gt;A publisher posted a blog entry successfully. It then verified the post by rebuilding the URL from parts — including a hardcoded month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/2026/08/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.html`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// August, forever&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On September 1st, every publish reported 404. Publishing worked perfectly. The &lt;em&gt;check&lt;/em&gt; was broken. Because the check ran inside the same script and swallowed its own error, the job still exited 0.&lt;/p&gt;

&lt;p&gt;For a week the dashboard was green and the blog was empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure two: the work finished, then cleanup died
&lt;/h2&gt;

&lt;p&gt;An image job produced 43 of 45 files, then threw &lt;code&gt;EPERM&lt;/code&gt; deleting its own temp directory — a child process still held a handle on Windows. Exit code 1.&lt;/p&gt;

&lt;p&gt;The 43 finished images were discarded and a fallback path never ran, because everything downstream keyed off the exit code. Re-running cost forty minutes of compute to undo an &lt;code&gt;rm -rf&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing both have in common
&lt;/h2&gt;

&lt;p&gt;An exit code has one bit of information, and the interesting question has two:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;artifacts missing&lt;/th&gt;
&lt;th&gt;artifacts present&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;exit 0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;silent failure&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;exit != 0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;failure&lt;/td&gt;
&lt;td&gt;false failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;An exit code cannot tell the top-left cell from the top-right one.&lt;/strong&gt; Both return 0. It cannot separate the bottom two either. It only knows the row, and the column is the half that decides whether you actually shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I check now
&lt;/h2&gt;

&lt;p&gt;After the command finishes, before deciding anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;exists&lt;/strong&gt; — the obvious one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;size&lt;/strong&gt; — ffmpeg exits 0 having written a 0-byte mp4 more often than you'd think&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;count, recursively&lt;/strong&gt; — &lt;code&gt;dist/index.js&lt;/code&gt; plus &lt;code&gt;dist/assets/*.css&lt;/code&gt; is four artifacts, not one. Counting only the top level made a normal bundler build look empty&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;freshness&lt;/strong&gt; — not older than N seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;novelty&lt;/strong&gt; — and this is the one that actually matters&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Novelty is the check everyone skips
&lt;/h2&gt;

&lt;p&gt;A file with the right name and the right size, written yesterday, passes every naive check ever written. A pipeline that has been dead for a week keeps reporting green.&lt;/p&gt;

&lt;p&gt;So snapshot mtimes &lt;em&gt;before&lt;/em&gt; the command runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// newest mtime under dir&lt;/span&gt;
&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// the files are there, but this run did not make them&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One trap: a &lt;strong&gt;directory's own mtime does not change when its files are overwritten in place&lt;/strong&gt;. It changes on create, delete and rename only. If you stat the directory itself, every incremental rebuild that reuses filenames looks like it produced nothing. You have to walk the tree and take the newest mtime among the entries.&lt;/p&gt;

&lt;p&gt;I shipped that bug and it flagged healthy builds for a day before I understood why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thresholds go stale, so learn them instead
&lt;/h2&gt;

&lt;p&gt;You set &lt;code&gt;minEntries: 30&lt;/code&gt;. The job grows to 200 over six months. The day it emits 40, nothing fires.&lt;/p&gt;

&lt;p&gt;Recording what each check produced on its healthy runs fixes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[X] out/img  &amp;lt;- 5 vs usual 30 (dropped below 50% of baseline)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody configured 30. Two rules keep it honest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Only passing runs teach the baseline.&lt;/strong&gt; Otherwise a broken run quietly lowers the bar and the next broken run looks normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never record a measurement you did not finish.&lt;/strong&gt; If the walk hit its budget and bailed, publishing the partial count as if it were the real one is exactly the lie the whole exercise exists to prevent.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The one that cost me the most
&lt;/h2&gt;

&lt;p&gt;I built a version that, when a tree was too large to walk, "withheld judgement" — which I implemented as &lt;code&gt;ok = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A build that produced literally nothing inside a large directory came back &lt;strong&gt;PASS&lt;/strong&gt;, green dashboard, exit 0.&lt;/p&gt;

&lt;p&gt;Withholding a verdict is not the same as passing one. It now refuses the check outright and exits 2, and the reason travels all the way to the dashboard and the Slack message. If I can't measure it, I don't get to say it's fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should a false failure fail your build?
&lt;/h2&gt;

&lt;p&gt;I made &lt;code&gt;FALSE_FAILURE&lt;/code&gt; exit 0 on purpose. If every artifact is on disk and the process only died in cleanup, re-running a forty-minute render buys nothing.&lt;/p&gt;

&lt;p&gt;I'm genuinely unsure this is right, and it's the decision I'd most like to hear arguments against. What do you do — trust the artifacts, or trust the exit code?&lt;/p&gt;




&lt;p&gt;I packaged this up as a small tool. The five failure patterns and a runnable two-second demo are free, no signup: &lt;a href="https://provedone.gumroad.com/l/pipeline-lies" rel="noopener noreferrer"&gt;Five ways your pipeline lies to you&lt;/a&gt;. The tool itself is &lt;a href="https://provedone.gumroad.com/l/provedone" rel="noopener noreferrer"&gt;$19&lt;/a&gt; — but run the demo first, it reproduces every verdict on your machine and installs nothing.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>testing</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
