<?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: Heinrich Neb</title>
    <description>The latest articles on DEV Community by Heinrich Neb (@heinrichneb).</description>
    <link>https://dev.to/heinrichneb</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%2F4074633%2Faa487288-817c-4e70-973a-53b0fca6f51b.jpg</url>
      <title>DEV Community: Heinrich Neb</title>
      <link>https://dev.to/heinrichneb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/heinrichneb"/>
    <language>en</language>
    <item>
      <title>Your CI is not flaky. Your cache expires every seven days.</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:48:49 +0000</pubDate>
      <link>https://dev.to/heinrichneb/your-ci-is-not-flaky-your-cache-expires-every-seven-days-703</link>
      <guid>https://dev.to/heinrichneb/your-ci-is-not-flaky-your-cache-expires-every-seven-days-703</guid>
      <description>&lt;p&gt;&lt;em&gt;CI timeouts&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A pull request that changed one blog page turned the build red. The lint job it broke had found zero problems. Someone re-ran it, it passed, and everyone moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero issues, and red anyway
&lt;/h2&gt;

&lt;p&gt;The log said two things that should never appear together. First: 0 issues. Then: Timeout exceeded.&lt;/p&gt;

&lt;p&gt;The linter had finished its work. It had read every file, found nothing wrong, and then died on the clock. It ran for 311 seconds against a budget of 300.&lt;/p&gt;

&lt;p&gt;Eleven seconds over. On a pull request that touched one page of prose and no Go code at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why re-running it made things worse
&lt;/h2&gt;

&lt;p&gt;The obvious move is to press re-run. It passed. That felt like proof that the first failure was noise.&lt;/p&gt;

&lt;p&gt;It was proof of the opposite. Two lines above the timeout, the log said: Cache not found for input keys.&lt;/p&gt;

&lt;p&gt;The first run had a cold cache and had to do the full analysis from scratch. It also populated the cache. The second run read that cache and finished in a fraction of the time. Re-running did not clear a random glitch. It removed the only condition under which the job fails.&lt;/p&gt;

&lt;p&gt;So the green run taught us nothing, and it hid the fact that the budget was too small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three questions that separate a flake from a costume
&lt;/h2&gt;

&lt;p&gt;Ask them before you press re-run. They take a minute and they work for any job, in any language.&lt;/p&gt;

&lt;p&gt;One: did the work finish? A job that produced its result and then failed is not flaky. Something outside the work killed it. Look for a completed output line above the error.&lt;/p&gt;

&lt;p&gt;Two: what was different about this run? Cache hit or miss, cold container, a dependency fetched from the network, a first run after a weekend. Real flakes have no pattern. This one had a name printed in its own log.&lt;/p&gt;

&lt;p&gt;Three: how close was it? A job that finishes in 40 percent of its budget is healthy. One that finishes at 90 percent is a scheduled outage. Print the duration and compare it to the limit, every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find out how often the condition returns
&lt;/h2&gt;

&lt;p&gt;This is the step that turns a shrug into a decision. Our cache configuration said the invalidation interval is seven days.&lt;/p&gt;

&lt;p&gt;That means the first run after every weekly expiry is cold, and every one of those runs was over budget. Not a rare glitch. A weekly failure, hidden by the six warm days around it.&lt;/p&gt;

&lt;p&gt;The rest of the week papers over it. That is exactly why it looked random: the failures are regular, the observations are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of calling it a flake
&lt;/h2&gt;

&lt;p&gt;A red check that says nothing about the code is not free. It teaches people that red sometimes means nothing.&lt;/p&gt;

&lt;p&gt;After the third time, nobody reads the log. They re-run it. And the day a red check means something real, it gets the same treatment.&lt;/p&gt;

&lt;p&gt;That is the actual damage, and it is done to your team, not to your pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it took to fix
&lt;/h2&gt;

&lt;p&gt;One line. The timeout went from 5 minutes to 15. The job still runs the same checks and still finds the same problems.&lt;/p&gt;

&lt;p&gt;The comment above that line is longer than the change. It names the numbers: 311 seconds, 300 budget, cold cache, seven day expiry. The next person will not need to rediscover it.&lt;/p&gt;

&lt;p&gt;That is the whole trade. One minute of reading the log instead of pressing re-run, and a class of failure disappears.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. ChatGPT and Claude remember your conversations. cachly remembers your codebase: the bug you fixed, why you chose Postgres, the deploy step that always breaks — including what your teammates learned. And every assistant you use reads the same memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop re-running jobs and hoping — and give your assistant a memory that remembers which failures were real.&lt;/strong&gt; Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=the-flake-that-was-not-a-flake" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your publish pipeline is green. Nobody can install your plugin.</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:59:35 +0000</pubDate>
      <link>https://dev.to/heinrichneb/your-publish-pipeline-is-green-nobody-can-install-your-plugin-9ok</link>
      <guid>https://dev.to/heinrichneb/your-publish-pipeline-is-green-nobody-can-install-your-plugin-9ok</guid>
      <description>&lt;p&gt;&lt;em&gt;Silent failure&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For three weeks nobody could install your plugin. The publish job was green every single day. You find out when a user asks why the version is so old.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three weeks green, zero installs
&lt;/h2&gt;

&lt;p&gt;This happened to us. The publish job for our JetBrains plugin reported success on every run since the middle of July. The plugin was not in the marketplace at all.&lt;/p&gt;

&lt;p&gt;The registry API answered with a 404. A search for the product name returned nothing. Meanwhile the build was green, the release notes were written, and the changelog was up to date.&lt;/p&gt;

&lt;p&gt;Nobody noticed. Not the pipeline, not the dashboard, not us. The gap between the last good release and the discovery was three weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your pipeline is not lying to you
&lt;/h2&gt;

&lt;p&gt;This is the part worth understanding, because it is why the same thing is probably waiting in your repository too.&lt;/p&gt;

&lt;p&gt;A release pipeline has one job: get the artifact somewhere a stranger can install it. Almost every pipeline checks something else. It checks that the upload command exited zero.&lt;/p&gt;

&lt;p&gt;Those two questions agree nearly always. Our publish step went further and did the sensible thing. It caught the failure, compared the error text against a list of known-harmless cases, and exited zero for those.&lt;/p&gt;

&lt;p&gt;One of those cases was pending moderation. A new version sits in review before it becomes visible. Failing the build for that would be noise, so it was allowed through.&lt;/p&gt;

&lt;p&gt;Here is the trap. A harmless transient state and a permanent block produce the same message. Once the plugin was stuck, every later run matched the same friendly pattern and reported success. The pipeline answered its question correctly. It was the wrong question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that catches it, in about two minutes
&lt;/h2&gt;

&lt;p&gt;Ask the store, not the pipeline. That is the whole idea, and you can add it today without changing anything else.&lt;/p&gt;

&lt;p&gt;One: after publishing, fetch the public listing the way a stranger would. No credentials, no internal API, no authenticated client. Seeing what an outsider sees is the entire point.&lt;/p&gt;

&lt;p&gt;Two: read the version from that response and compare it to the version in your manifest. Not to the version you just built, and not to a variable in the job. To the file that is checked in.&lt;/p&gt;

&lt;p&gt;Three: print one line per place you publish to, and say which one you could not reach. Reached, not reached, or stale. Never a bare zero.&lt;/p&gt;

&lt;p&gt;That last part matters more than it looks. A source that errors out and gets counted as zero results looks exactly like a quiet day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it fail on purpose, before you trust it
&lt;/h2&gt;

&lt;p&gt;A watcher nobody has seen go red is not a watcher. It is a hope with a log file.&lt;/p&gt;

&lt;p&gt;Give it a fixture where the listing is stale and assert that it exits non-zero. Run it once and look at the red. It takes a few minutes and it is the only thing that separates a real check from a comfortable one.&lt;/p&gt;

&lt;p&gt;We skipped this step once on a different watcher. It sat there for weeks reporting nothing, and reporting nothing was indistinguishable from everything being fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Once you see this shape, you see it everywhere
&lt;/h2&gt;

&lt;p&gt;A cache file written into a workspace that the next run wipes clean. A daily report whose delta is computed against a seeded example row. A source that returns an HTTP error and gets counted as zero.&lt;/p&gt;

&lt;p&gt;Every one of them is green. Every one of them delivers nothing.&lt;/p&gt;

&lt;p&gt;The missing ingredient is always the same: a third state. Systems model success and failure and forget not measured. Without it, an outage is indistinguishable from a quiet day, and people act on the number anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for you
&lt;/h2&gt;

&lt;p&gt;Before: you find out from a user, three weeks late. After: you find out the next morning, from a check you wrote in two minutes.&lt;/p&gt;

&lt;p&gt;It also catches the one case no exit code can reach. Sometimes the upload really did work and the store still shows the old version. Your pipeline has no way to see that. A stranger looking at the page does.&lt;/p&gt;

&lt;p&gt;Ours is about forty lines. It opens three public pages, one for each store we publish to. It reads the version shown on each page and compares it to the version number in the file we ship. No login, no API key, no access to anything private.&lt;/p&gt;

&lt;p&gt;The day we switched it on, it went red. It had been red for three weeks and nobody knew.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. ChatGPT and Claude remember your conversations. cachly remembers your codebase: the bug you fixed, why you chose Postgres, the deploy step that always breaks — including what your teammates learned. And every assistant you use reads the same memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop finding out from your users — give your assistant a memory that survives the session.&lt;/strong&gt; Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=green-pipeline-nobody-can-install" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>devops</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
