<?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: Adnan Mokhtar</title>
    <description>The latest articles on DEV Community by Adnan Mokhtar (@adnanmokhtar).</description>
    <link>https://dev.to/adnanmokhtar</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%2F4232483%2F5c0902b1-f70a-406f-97ef-77e377a20a71.png</url>
      <title>DEV Community: Adnan Mokhtar</title>
      <link>https://dev.to/adnanmokhtar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adnanmokhtar"/>
    <language>en</language>
    <item>
      <title>My CI gate reported "passing" without ever checking anything</title>
      <dc:creator>Adnan Mokhtar</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:56:56 +0000</pubDate>
      <link>https://dev.to/adnanmokhtar/my-ci-gate-reported-passing-without-ever-checking-anything-24fm</link>
      <guid>https://dev.to/adnanmokhtar/my-ci-gate-reported-passing-without-ever-checking-anything-24fm</guid>
      <description>&lt;p&gt;A red badge sat on my repo's README. Every check passed on my machine.&lt;/p&gt;

&lt;p&gt;The cause took a few minutes to find. The interesting part was what the red badge had been hiding. Fixing it took four rounds, and each round exposed something the previous failure had covered up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 1: one missing tool stopped more than 70 checks
&lt;/h2&gt;

&lt;p&gt;The failing step was a fixture test. The script under test needs &lt;code&gt;ripgrep&lt;/code&gt;, and it exits with a "blocked" code when &lt;code&gt;rg&lt;/code&gt; isn't installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; rg &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BLOCKED: rg (ripgrep) is not installed — no axis can be computed"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rg&lt;/code&gt; is on my machine and isn't on GitHub's &lt;code&gt;ubuntu-latest&lt;/code&gt; runner, so the test could only fail in CI. The fix is one workflow step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install ripgrep&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo apt-get update -qq &amp;amp;&amp;amp; sudo apt-get install -y -qq ripgrep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part I had missed: GitHub Actions stops a job at the first failed step. That failure was early in the job, so most checks had been skipped on every run for two weeks. The run page showed one failure, and it read like one small problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 2: the check that skipped and called it success
&lt;/h2&gt;

&lt;p&gt;Installing &lt;code&gt;rg&lt;/code&gt; turned a different gate red. This one runs every search command in my docs to prove it parses. It had this line at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; rg &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"rg-parses: SKIPPED — ripgrep not installed"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 0. The gate had been in CI for a month and had never checked anything there. The only sign was one word in a log nobody reads.&lt;/p&gt;

&lt;p&gt;Its first real run found three commands that fail on ripgrep 14, the version Ubuntu's &lt;code&gt;apt&lt;/code&gt; installs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A nested glob, &lt;code&gt;{application*.{yml,yaml},...}&lt;/code&gt;, which ripgrep 15 accepts and 14 rejects.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--type vue&lt;/code&gt; in two files, which ripgrep 14 doesn't know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These were bad in a quiet way. The commands exit with an error, and an agent reading the result sees no matches. Anyone on Ubuntu would have got "nothing found" from a check that never searched.&lt;/p&gt;

&lt;p&gt;The fix was small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rg &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="s1"&gt;'**/{application*.yml,application*.yaml}'&lt;/span&gt; ...
rg &lt;span class="s2"&gt;"pattern"&lt;/span&gt; &lt;span class="nt"&gt;--type-add&lt;/span&gt; &lt;span class="s1"&gt;'vue:*.vue'&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; vue &lt;span class="nt"&gt;--type&lt;/span&gt; ts ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Round 3: my own fix broke a count
&lt;/h2&gt;

&lt;p&gt;Now the job got further and failed on a gate that checks the number of steps in the workflow against the number in my contributing guide. I had added a step, so the guide was wrong by one.&lt;/p&gt;

&lt;p&gt;That gate was doing its job. It was also the third failure in a row that had been invisible until the one before it was fixed. So I stopped pushing and guessing, and ran the workflow's steps locally in one go.&lt;/p&gt;

&lt;p&gt;Green, finally. Then, a few days later, a commit that only touched docs went red.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 4: a gate that depended on filesystem order
&lt;/h2&gt;

&lt;p&gt;The failing command was one I hadn't changed, in a file nobody had touched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rg &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'pattern'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$B&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate runs each command in an empty temp directory so that nothing real gets searched. It replaced arrays with &lt;code&gt;.&lt;/code&gt; and left plain variables unset. Look at what that does here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$B&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# with both unset, this is just /&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the "empty directory" check was searching the entire filesystem. It stopped when it hit a match, and if it didn't hit one in 20 seconds, the gate failed. Whether it passed came down to the order the walk happened to visit files.&lt;/p&gt;

&lt;p&gt;The fix was to bind every variable to &lt;code&gt;.&lt;/code&gt;:&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="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\$\{?([A-Za-z_][A-Za-z0-9_]*)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;))&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;PATH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PATH&lt;/code&gt; is excluded because bash still has to find &lt;code&gt;rg&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in how I work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A skip should be visible.&lt;/strong&gt; If a check can't run, that's a different result from passing. In CI, a missing tool should fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After fixing a red build, run everything.&lt;/strong&gt; The first failure hides the rest. A local script that runs every step and collects all the failures is slower than one push, but it shows them all at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unset variables are values.&lt;/strong&gt; &lt;code&gt;"$A/$B"&lt;/code&gt; is &lt;code&gt;/&lt;/code&gt; when both are empty. That's easy to miss in any script that runs commands from somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the check itself.&lt;/strong&gt; I now keep a known-good and a known-bad fixture for these gates. The new good fixture contains a search that can't match anything, so if the variable bug returns, the probe walks the whole disk and times out.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This happened in &lt;a href="https://github.com/adnanmokhtar/refract" rel="noopener noreferrer"&gt;Refract&lt;/a&gt;, an open-source repo of mine. This article was created with the help of AI, and I checked it against the actual commits.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ci</category>
      <category>testing</category>
      <category>github</category>
      <category>bash</category>
    </item>
  </channel>
</rss>
