<?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: Flude team</title>
    <description>The latest articles on DEV Community by Flude team (@flude_team).</description>
    <link>https://dev.to/flude_team</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%2F4043834%2F57eca9f0-8b66-4e9d-b493-6c13ecd57dfd.png</url>
      <title>DEV Community: Flude team</title>
      <link>https://dev.to/flude_team</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flude_team"/>
    <language>en</language>
    <item>
      <title>The Configuration Trap: How Enterprise Solutions Become SaaS Vulnerabilities</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:15:44 +0000</pubDate>
      <link>https://dev.to/flude_team/the-configuration-trap-how-enterprise-solutions-become-saas-vulnerabilities-2ndc</link>
      <guid>https://dev.to/flude_team/the-configuration-trap-how-enterprise-solutions-become-saas-vulnerabilities-2ndc</guid>
      <description>&lt;p&gt;After deciding to split the product into Enterprise and SaaS, we started designing the cloud architecture. During the planning phase, the task seemed straightforward: spin up an API, configure container-workers in the cloud, and simply pass the code to them for processing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdj1htsyoysc7q3fgdqu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdj1htsyoysc7q3fgdqu.jpg" alt="Zero Trust Cloud Config Trap" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, during a detailed security audit, we discovered that architectural patterns which worked flawlessly in a trusted corporate environment turn a public SaaS into an open hacking sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a Vulnerability: RCE via Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8o9a7l8h8b4574otwx2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8o9a7l8h8b4574otwx2.jpg" alt="RCE Vulnerability in Config" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the Enterprise version, the configuration logic was maximally flexible: the engine simply read the &lt;code&gt;flude_*_config.json&lt;/code&gt; configuration file directly from the client's repository.&lt;/p&gt;

&lt;p&gt;The problem lay in exactly &lt;em&gt;how&lt;/em&gt; these parameters were translated into parser settings under the hood. Our pipeline relied on Doxygen, which we extended with our own scripts to bypass C++ bugs. To make these filters work, the engine uses a standard Doxygen directive—&lt;code&gt;INPUT_FILTER&lt;/code&gt;, which allows executing arbitrary shell commands for processed files.&lt;/p&gt;

&lt;p&gt;In an isolated client perimeter, reading settings from the repository was absolutely safe. But when transferring this logic to a public cloud (GCP Cloud Run), a critical vector for Remote Code Execution (RCE) emerged. If a server blindly trusts a &lt;code&gt;flude_*_config.json&lt;/code&gt; file from a third-party repository, an attacker can pass through overrides for parser parameters. By injecting a command like &lt;code&gt;rm -rf /&lt;/code&gt; or a reverse-shell via &lt;code&gt;INPUT_FILTER&lt;/code&gt; substitution, an attacker would force our system to legitimately execute this code on our own servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero-Trust Architecture and Breaking the Trust
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfrd2pv61hgedpalzdxh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfrd2pv61hgedpalzdxh.jpg" alt="Zero-Trust Architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Realizing this risk forced us to completely rethink our approach to configuration. In a public cloud, you cannot trust a single line coming from the outside. We implemented a strict Zero-Trust principle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Abandoning Client Configurations:&lt;/strong&gt; The SaaS version will &lt;em&gt;never&lt;/em&gt; read &lt;code&gt;flude_*_config.json&lt;/code&gt; from a user's repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Templates:&lt;/strong&gt; Our cloud worker operates exclusively on hardcoded, versioned server templates for Doxyfiles and configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Parameter Passing:&lt;/strong&gt; A user can only pass a strictly limited set of safe data-parameters (e.g., directories to include or exclude) via the API, which are rigidly validated on the Control Plane side. Control over which tools and filters run in the container has been permanently handed over to the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Fortifying the Perimeter: SSRF and Workload Identity
&lt;/h2&gt;

&lt;p&gt;Having plugged the RCE hole, we turned our attention to network security. Cloning user repositories by a server is a classic Server-Side Request Forgery (SSRF) risk. To prevent attackers from forcing our workers to scan the internal GCP network under the guise of a git repository, we implemented a strict Allowlist: cloning is allowed only via HTTPS and strictly from the domains &lt;code&gt;github.com&lt;/code&gt;, &lt;code&gt;gitlab.com&lt;/code&gt;, and &lt;code&gt;bitbucket.org&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Additionally, to minimize damage in the hypothetical event of a container breach, we completely abandoned static keys (Service Account Keys), transitioning all workers to dynamic authentication via GCP Workload Identity Federation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deferred Threats: Attack via Source Code
&lt;/h2&gt;

&lt;p&gt;Having secured the configuration and network, we asked the next question: what if the attack vector becomes the very source code we are parsing?&lt;/p&gt;

&lt;p&gt;We analyzed several highly specific scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Doxygen Tag Abuse:&lt;/strong&gt; An attacker could write standard tags like &lt;code&gt;\include&lt;/code&gt;, &lt;code&gt;\verbinclude&lt;/code&gt;, or &lt;code&gt;\htmlinclude&lt;/code&gt; into a code comment with a path like &lt;code&gt;../../../etc/passwd&lt;/code&gt;. These will force the parser to read a file outside the project directory and insert its contents into the final documentation, which is then returned to the client. Our strict directory validation is powerless here, as it only checks the declared input paths, not what Doxygen decides to read during the parsing process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malicious Symlinks:&lt;/strong&gt; If an attacker commits a symlink pointing to system files of the container, and the parser follows it while traversing directories—this is another way to extract data outside the repository sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Denial of Service (DoS):&lt;/strong&gt; In our system, the call to the Doxygen binary is limited to 60 seconds, but the parent &lt;code&gt;flude_engine compile&lt;/code&gt; process had no timeout. A specially crafted source file (with infinite template recursion or explosive macro expansion) could permanently hang the cloud worker process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After considering these vectors, we made a conscious decision to &lt;strong&gt;defer&lt;/strong&gt; fixing them.&lt;/p&gt;

&lt;p&gt;First, architecturally it was unclear at what level to implement these defenses—especially in light of the upcoming introduction of native parsers. We honestly recorded this debt in the project roadmap under the "Input Validation &amp;amp; Sanitization" section.&lt;/p&gt;

&lt;p&gt;Second, the actual risk was significantly reduced thanks to the Data Plane architecture. All workers in Cloud Run are isolated via gVisor, and the containers themselves are absolutely ephemeral. Even in the event of a successful exploit of a symlink or an &lt;code&gt;\include&lt;/code&gt; tag, an attacker would read files from a disposable, sterile environment without access to neighboring tasks or the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Accumulating Dissatisfaction with Doxygen
&lt;/h2&gt;

&lt;p&gt;that the incidents with potential RCE via &lt;code&gt;INPUT_FILTER&lt;/code&gt; and the vulnerability of standard tags like &lt;code&gt;\include&lt;/code&gt; became more than just cloud security issues. They were another wake-up call reminding us of Doxygen's fundamental architectural flaws. Its inflexibility, reliance on shell calls, and poor performance made it a burden for a scalable SaaS solution (and for the architecture as a whole).&lt;/p&gt;

&lt;p&gt;Running ahead, we can say: we did not put up with this technical debt. In a couple of weeks, as soon as we finish the series of articles on SaaS infrastructure, we will explain in detail why we finally fired Doxygen.&lt;/p&gt;

&lt;p&gt;But before that, we had to solve another pressing cloud problem. We had designed a secure Data Plane, protected from hackers, but now we needed to protect it from bankruptcy. How we built a Free-Tier, protected the API from abuse loads, and designed billing—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/zero-trust-cloud-config-trap" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/zero-trust-cloud-config-trap&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>architecture</category>
      <category>saas</category>
      <category>flude</category>
    </item>
    <item>
      <title>A Tale of Two Fludes: The Architectural Split of Enterprise and SaaS Models</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 06 Oct 2026 06:06:10 +0000</pubDate>
      <link>https://dev.to/flude_team/a-tale-of-two-fludes-the-architectural-split-of-enterprise-and-saas-models-3ahm</link>
      <guid>https://dev.to/flude_team/a-tale-of-two-fludes-the-architectural-split-of-enterprise-and-saas-models-3ahm</guid>
      <description>&lt;p&gt;Extracting specific code into an independent private submodule, as described in the &lt;a href="https://dev.to/en/blog/clean-room-private-plugin"&gt;previous article&lt;/a&gt;, allowed us to stabilize the engine's core. The legal boundaries were drawn, and the base architecture—a smart parser and two-level caching—proved its effectiveness.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6edz5qig3xci87jzeal.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6edz5qig3xci87jzeal.jpg" alt="Tale Of Two Fludes" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next logical step was transitioning from an internal tool to a commercial product. However, at this stage, we faced a fundamental contradiction between the corporate operational model and the demands of the mass market.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion of Universal Distribution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxcnl6hgro5eu6wxvtvkl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxcnl6hgro5eu6wxvtvkl.jpg" alt="SaaS vs Enterprise Architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initially, Flude's architecture was shaped by the needs of a large corporation. Security requirements in such environments are categorical: the tool must function within a closed perimeter, guaranteeing that the source code never leaves the company's servers.&lt;/p&gt;

&lt;p&gt;To solve this, we integrated directly. A package with scripts was handed over to the client and run locally in their CI pipeline, reading configuration files directly from the repository. Within a corporate environment, this delivery model (whether source or bytecode) is ideal: the closed perimeter and strict legal obligations reliably guarantee the safety of intellectual property.&lt;/p&gt;

&lt;p&gt;However, when planning to scale to the public market, it became obvious that directly copying the Enterprise distribution approach was technically and conceptually inapplicable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Losing Control over Distribution
&lt;/h2&gt;

&lt;p&gt;The closed-contract model works perfectly in the Enterprise sector, but to ensure stable product growth, we needed to enter the mass B2B/B2C market: small teams and independent developers.&lt;/p&gt;

&lt;p&gt;The obvious problem was that handing over source Python scripts or a ready-made Docker image to users outside of corporate agreements inevitably leads to losing control over product distribution. Without strict legal barriers, open distribution technically allows bypassing licensing mechanisms and uncontrollably replicating the system's core.&lt;/p&gt;

&lt;p&gt;Consequently, the approach that is standard for a trusted corporate environment turned out to be structurally incompatible with a public business model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Divergence
&lt;/h2&gt;

&lt;p&gt;To protect the product's core, we had to rethink the delivery paradigm itself. Flude's architecture was physically divided into two independent models:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise Flude:&lt;/strong&gt; An autonomous, local solution. Delivered exclusively under targeted contracts. Functions offline, processes code locally, and integrates with the configuration directly in the client's repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS Flude (Public Market):&lt;/strong&gt; A cloud-oriented model. The parser's core is never handed over to end users. Instead, a "thin client" (lightweight CI scripts) is provided, whose sole task is packing source files and securely transmitting them via API. All computational load and parsing logic remain encapsulated on our servers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Moving to the Cloud and the Unexpected Benefit of Binaries
&lt;/h2&gt;

&lt;p&gt;By adopting the SaaS model, we secured our algorithms, but significantly complicated the technical implementation. We had to design a distributed system from scratch capable of receiving and processing third-party code.&lt;/p&gt;

&lt;p&gt;Interestingly, it was at this stage that the concept of a "single binary", which we had rejected for public distribution, turned out to be our own salvation. To avoid managing complex Python environments and dependencies on every cloud worker, we compiled our core. A standalone binary became the perfect solution for quick startups inside our own SaaS infrastructure (and later, a convenient delivery format for Enterprise clients).&lt;/p&gt;

&lt;p&gt;How we designed the cloud with the Zero-Trust concept in mind, and why configuration files that worked flawlessly locally turned into a critical attack vector (RCE) in SaaS, we'll tell you in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/tale-of-two-fludes" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/tale-of-two-fludes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>business</category>
      <category>architecture</category>
      <category>saas</category>
      <category>flude</category>
    </item>
    <item>
      <title>Clean Room: A Refactor That Answered a Legal Question, Not an Architectural One</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 02 Oct 2026 04:34:36 +0000</pubDate>
      <link>https://dev.to/flude_team/clean-room-a-refactor-that-answered-a-legal-question-not-an-architectural-one-7e0</link>
      <guid>https://dev.to/flude_team/clean-room-a-refactor-that-answered-a-legal-question-not-an-architectural-one-7e0</guid>
      <description>&lt;p&gt;Code born inside the core engine rarely has to stay there forever. This specific move started with a question about legal boundaries. We needed a hard line between our own original logic and knowledge acquired from an external format. One of Flude's renderer families—the one built to precisely mirror an outside documentation tool—sat dangerously close to that line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Risks a Refactor-in-Place Misses
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcetxwpuu53znruae0und.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcetxwpuu53znruae0und.jpg" alt="Tangled data streams" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Carving out a module inside the same repository does practically nothing here. A shared commit history leaves the biggest vulnerabilities wide open.&lt;/p&gt;

&lt;p&gt;Authorship is the first major headache. Code mirroring an external format gets thoroughly mixed up with the rest of the engine. Proving independent development after the fact becomes incredibly messy. You build a credible evidence trail through total separation—a dedicated repository and a fresh commit history.&lt;/p&gt;

&lt;p&gt;The logic itself carries its own risks. The way code bends to match an external format is highly specific knowledge. Tightly weaving it into the engine blurs the line between a general tool and client-specific insights.&lt;/p&gt;

&lt;p&gt;A physical split cleanly shuts down both risks. A simple folder rename just papers over the cracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Played Out in Practice
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0gpv1ulm6mguy4jqfss.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0gpv1ulm6mguy4jqfss.jpg" alt="Clean Room plugin" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We pulled the renderer family into a standalone git repository. It plugs into the main project as a private submodule. The engine interacts with it through the standard renderer interface. The class simply lives in a new zip code. The rest of the system barely noticed the change.&lt;/p&gt;

&lt;p&gt;Standing up the new repository brought plenty of friction. The test harness relies heavily on the engine's core models. We had to check out the main engine inside the new CI pipeline. Shared relative paths completely broke down. Nailing the dependency paths took a few frustrating attempts. We temporarily dropped the markdownlint gate before restoring it to maintain project standards. Turning the CI pipeline green took some serious trial and error.&lt;/p&gt;

&lt;p&gt;The quality checks moved right alongside the code. The entire infrastructure for comparing results against reference documents shifted over. Leaving it behind would have made it impossible to spot rendering drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4z3m4i7m21tcvsd7qyl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4z3m4i7m21tcvsd7qyl.jpg" alt="Core and Module separated" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main engine feels much leaner now. The core and the remaining renderer family can evolve at their own natural pace. The extracted code operates completely independently. It boasts its own CI and a bulletproof line of provenance. We no longer have to worry about proving its origins retroactively.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/clean-room-private-plugin" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/clean-room-private-plugin&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>plugins</category>
      <category>flude</category>
    </item>
    <item>
      <title>Ghost in the Wire: Our Two-Day Hunt for a Network Bug That Didn't Exist</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 29 Sep 2026 03:38:53 +0000</pubDate>
      <link>https://dev.to/flude_team/ghost-in-the-wire-our-two-day-hunt-for-a-network-bug-that-didnt-exist-4pd1</link>
      <guid>https://dev.to/flude_team/ghost-in-the-wire-our-two-day-hunt-for-a-network-bug-that-didnt-exist-4pd1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fketpqlgvaqolk8d94j6w.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fketpqlgvaqolk8d94j6w.jpg" alt="Ghost in the wire" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our self-hosted runner serves CI for five repositories on one physical VM. It started acting completely crazy. The &lt;code&gt;engine&lt;/code&gt; job sat queued in GitHub Actions for over a day. It just hung there doing absolutely nothing. The supervisor meant to pick it up was restarting itself every two and a half to three minutes around the clock. We called this endless loop "the carousel" and wasted two days chasing ghosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom, and the First False Lead
&lt;/h2&gt;

&lt;p&gt;We already had a documented network bug in this exact infrastructure. A classic VirtualBox NAT "black hole" used to kill TCP connections inside the guest VM silently. The guest OS would consider the connection alive forever. We tracked down and closed that issue a week and a half earlier.&lt;/p&gt;

&lt;p&gt;When the supervisor started hanging on the host side, our reflexes kicked in. We assumed it was the exact same disease in a fresh disguise. That feeling was convincing enough to steer the whole investigation sideways for a full day. The watchdog detected the hang, killed the process, and brought it back up. Every new instance hung reliably after 150-190 seconds. That unnatural precision should have made us suspicious way earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escalation: Packets, Memory Dumps, Power Settings
&lt;/h2&gt;

&lt;p&gt;A thorough and completely useless sweep through network theories followed. We disabled a third-party VirtualBox NDIS filter attached to the Wi-Fi adapter. We reset the MIMO power-saving settings. Nothing made a dent in the problem.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;pktmon&lt;/code&gt; during an actual hang showed a completely healthy network. New TCP connections to GitHub established in 60 milliseconds. The "hung" process supposedly couldn't send a single byte. We wrongly concluded the hang was happening before the packet got sent.&lt;/p&gt;

&lt;p&gt;A memory dump of the process via WinDbg showed twenty-two threads. None of them were sitting inside an HTTP call. We spotted suspicious modules like antivirus drivers and &lt;code&gt;crypt32&lt;/code&gt; certificate validators. Every single one of these plausible candidates turned out to be a dead end.&lt;/p&gt;

&lt;p&gt;We did notice one genuinely interesting detail. The host machine dropped into Modern Standby right when the hangs occurred. We disabled sleep and screen timeouts entirely. The restarts kept happening on the exact same schedule. The correlation was totally real—but completely unrelated.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Second Pair of Eyes Solves Half the Problem
&lt;/h2&gt;

&lt;p&gt;Another session looked at the logs and caught what we missed. The watchdog was treating plain silence as a fatal hang. The GitHub API poll writes nothing when the queue is empty. It simply sleeps for twenty seconds and tries again. A few quiet cycles looked exactly like a dead process to the watchdog.&lt;/p&gt;

&lt;p&gt;We verified this by pulling the I/O counters directly. The bytes were genuinely moving. The process was perfectly healthy with nothing to log. We updated the watchdog to check those exact counters before killing anything. The restarts stopped completely. The &lt;code&gt;engine&lt;/code&gt; job still refused to dispatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cause
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizjt299o2sr99lto3m7s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizjt299o2sr99lto3m7s.jpg" alt="API filter bug" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We ran a direct test using the exact token the supervisor uses. Querying &lt;code&gt;runs?status=queued&lt;/code&gt; for &lt;code&gt;engine&lt;/code&gt; returned zero results. Running the same query with a personal token showed the job instantly. Access rights were perfectly fine.&lt;/p&gt;

&lt;p&gt;The answer was buried in the API response itself. The run had a &lt;code&gt;"pending"&lt;/code&gt; status. The job inside that run was correctly marked as &lt;code&gt;"queued"&lt;/code&gt;. Our code filtered runs at the top level. GitHub had split the run's status from the job's status. One bad line of filtering made the code completely blind to a waiting job.&lt;/p&gt;

&lt;p&gt;We dropped the top-level filter completely. The nested check handled everything perfectly on its own. The very first poll picked up the stuck jobs and ran the full suite beautifully.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Small Postscript: Fixing Our Own Fix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flzs2uw7k2798849u4gdi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flzs2uw7k2798849u4gdi.jpg" alt="Ghost In The Wire Part 3" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Removing the filter spawned a new headache. The code started pulling dozens of recent runs and making separate job requests for each one. The call volume spiked massively. The carousel of restarts came back because I/O genuinely dropped to zero. We added client-side filtering to skip runs marked as &lt;code&gt;completed&lt;/code&gt;. Everything stabilized immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Taking Away From This
&lt;/h2&gt;

&lt;p&gt;Solid diagnostic tools will gladly prove the wrong theory. WinDbg and packet sniffers correctly showed a healthy network. We just kept interpreting those results to fit our initial bias. A simple API call with the right token proved infinitely more useful than all the heavy debugging gear.&lt;/p&gt;

&lt;p&gt;Next time, we get into legal boundaries and clean room design. We ended up moving an entire renderer family into a private plugin. It was the only way to firmly separate our core engine from logic shaped by an external client format.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/ghost-in-the-wire" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/ghost-in-the-wire&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postmortem</category>
      <category>cicd</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Growing Pains with Five Repositories: Gitlinks and Dual CI</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 25 Sep 2026 04:02:27 +0000</pubDate>
      <link>https://dev.to/flude_team/growing-pains-with-five-repositories-gitlinks-and-dual-ci-383k</link>
      <guid>https://dev.to/flude_team/growing-pains-with-five-repositories-gitlinks-and-dual-ci-383k</guid>
      <description>&lt;p&gt;Usually, infrastructure stories start with the words: "Our project got too big, and we decided to split it into parts." But with us, everything was different.&lt;/p&gt;

&lt;p&gt;From the very first minute of the project, we consciously chose an architecture consisting of a scatter of independent repositories. The &lt;code&gt;Pipeline&lt;/code&gt;, &lt;code&gt;engine&lt;/code&gt;, &lt;code&gt;design-docs&lt;/code&gt;, and &lt;code&gt;user-docs&lt;/code&gt; projects were created almost simultaneously, and the &lt;code&gt;.gitmodules&lt;/code&gt; configuration file sat in the very first commit of the parent repository.&lt;/p&gt;

&lt;p&gt;For the sake of code cleanliness, CI isolation, independent history, and separation of access rights, we built the system on Git Submodules from day one. The idea looked flawless on paper. But here is what we had to pay for it in practice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foklxi35e3cd2kg1hvdaj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foklxi35e3cd2kg1hvdaj.jpg" alt="Git Submodule Trap" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gitlink Trap: The Ghost of a Local Commit
&lt;/h2&gt;

&lt;p&gt;The first price we paid was the non-obvious mechanics of state synchronization. When you use submodules, the workflow demands strict discipline. If you edit code inside &lt;code&gt;engine/&lt;/code&gt; (while physically located in the folder of the parent repository &lt;code&gt;Pipeline&lt;/code&gt;), you need to make two pushes. First, a &lt;code&gt;git push&lt;/code&gt; from the folder of the submodule itself, and then, after moving up to the root of &lt;code&gt;Pipeline&lt;/code&gt;—you must commit the updated pointer (gitlink) to the new &lt;code&gt;engine&lt;/code&gt; commit and push &lt;code&gt;Pipeline&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;It is incredibly easy to forget the first push. On your local disk, the commit physically exists. Everything builds, the tests are green, and &lt;code&gt;Pipeline&lt;/code&gt; happily goes to the cloud. But this only surfaces on a fresh checkout in CI. The &lt;code&gt;git submodule update --init&lt;/code&gt; command fails with the text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fatal: remote error: upload-pack: not our ref &amp;lt;sha&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
    ext&lt;/p&gt;

&lt;p&gt;GitHub honestly replies: "This commit does not exist in the remote repository."&lt;/p&gt;

&lt;p&gt;One day, three submodules fell into this trap simultaneously (&lt;code&gt;design-docs&lt;/code&gt;, &lt;code&gt;engine&lt;/code&gt;, &lt;code&gt;user-docs&lt;/code&gt;). The cause was identical—small commits were made right inside the submodule folders, the gitlink in &lt;code&gt;Pipeline&lt;/code&gt; was synchronized, but the local commits never left the developer's laptop.&lt;/p&gt;

&lt;p&gt;There was also a colorful nuance: the fourth submodule, &lt;code&gt;ude_promotion&lt;/code&gt; (which hosts the site), broke so badly that a normal push couldn't fix it—its local branch fell so far behind &lt;code&gt;upstream&lt;/code&gt; that an interactive &lt;code&gt;rebase&lt;/code&gt; was required before pushing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dual CI: One Code in Two Personalities
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsujfsuswchmb3fzfe52.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsujfsuswchmb3fzfe52.jpg" alt="Multi Repo Gitlinks Dual Ci Part 2" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second reckoning came from CI. The &lt;code&gt;engine&lt;/code&gt; repository (our core) had two personalities. On the one hand, it was a standalone repository with its own build process, which checked out only this repository. On the other—it was a submodule inside &lt;code&gt;Pipeline&lt;/code&gt;, which was tested in the context of the entire parent project.&lt;/p&gt;

&lt;p&gt;These two personalities inevitably clashed, and this happened in two different stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The First Strike: Missing Dependencies&lt;/strong&gt;&lt;br&gt;
The simplest case. The &lt;code&gt;test_integration_scripts.py&lt;/code&gt; test tried to reach the &lt;code&gt;verify_pages/check_links&lt;/code&gt; modules, which existed only in the parent &lt;code&gt;Pipeline&lt;/code&gt; repository. During a standalone checkout of &lt;code&gt;engine&lt;/code&gt;, these files simply weren't on the disk. We fixed it roughly but effectively—we added a single line to the &lt;code&gt;ci.yml&lt;/code&gt; configuration, strictly ignoring this test from the standalone run (&lt;code&gt;pytest --ignore=...&lt;/code&gt;). But this was a one-off patch, not a systemic solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fatal Directory Ascent&lt;/strong&gt;&lt;br&gt;
Soon the bug returned in a new guise. One by one, tests started failing that used a classic trick to find parent artifacts:&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="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
    ext&lt;/p&gt;

&lt;p&gt;In a nested run, this code ascended exactly to the root of the parent repository. But in the standalone build of &lt;code&gt;engine&lt;/code&gt;, it jumped &lt;strong&gt;above&lt;/strong&gt; the root of the repository, straight into the system filesystem of the CI runner, and crashed with a hard &lt;code&gt;AssertionError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was after the second occurrence of this bug that we realized point patches no longer worked. We needed a general convention.&lt;/p&gt;

&lt;p&gt;We introduced an elegant marker hack: tests began checking for the presence of our system marker directory (let's call it &lt;code&gt;.workspace_config&lt;/code&gt;) at the ascended level. If the directory is there—it means we are in a nested context, and the tests run. If it's not there (standalone)—we politely use &lt;code&gt;pytest.mark.skipif&lt;/code&gt;, and the tests are quietly skipped without failing with a red status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxveedmgxcrku3hk85ck3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxveedmgxcrku3hk85ck3.jpg" alt="Multi Repo Gitlinks Dual Ci Part 3" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Multi-repo architecture does not give you magical "loose coupling" for free on day one.&lt;/p&gt;

&lt;p&gt;It demands flawless push synchronization discipline and forces you to design code so that it is ready to work in several different execution contexts simultaneously. These lessons cost us broken pipelines, but without them, the system would not have survived further growth.&lt;/p&gt;

&lt;p&gt;But the problems with Git and dual CI turned out to be just a prelude. The real mysticism began when the network intervened, and our pipeline started crashing because of a "ghost" in the wires. How we investigated an elusive network bug—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/multi-repo-gitlinks-dual-ci" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/multi-repo-gitlinks-dual-ci&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>submodules</category>
      <category>ci</category>
      <category>pytest</category>
    </item>
    <item>
      <title>Escaping the Cloud: How Five Repositories Stopped Sharing One Minute Limit</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 22 Sep 2026 04:13:31 +0000</pubDate>
      <link>https://dev.to/flude_team/escaping-the-cloud-how-five-repositories-stopped-sharing-one-minute-limit-5h0p</link>
      <guid>https://dev.to/flude_team/escaping-the-cloud-how-five-repositories-stopped-sharing-one-minute-limit-5h0p</guid>
      <description>&lt;p&gt;Splitting the monorepo granted each of our five components an independent continuous integration pipeline. Architecturally, we achieved perfect component isolation. The problem hid in the strict limits of free GitHub Actions minutes. A unified build line used to efficiently compile everything in a single run. Five separate pipelines began burning through our starting two thousand minutes like wildfire by launching independent jobs for every tiny commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Physical Machine for Five Clients
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa41v8ns02ptlrla9qb5k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa41v8ns02ptlrla9qb5k.jpg" alt="One VM, Five Repositories" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We flatly refused to buy extra cloud minutes and deployed our own self-hosted runner on a standard Windows office laptop. Inside ran an Ubuntu virtual machine powered by VirtualBox. This single virtual machine methodically took turns servicing tasks for all five repositories.&lt;/p&gt;

&lt;p&gt;Official GitHub documentation heavily &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security" rel="noopener noreferrer"&gt;advises against&lt;/a&gt; attaching local runners to public projects due to remote code execution risks via malicious pull requests. Our repositories are completely private, keeping us safe from outside attacks. We nevertheless implemented paranoid hygiene to guarantee build reproducibility. A single runner processes code from five different projects, and leftover file debris from a previous build could easily ruin the next one.&lt;/p&gt;

&lt;p&gt;We isolated the guest system through classic NAT (&lt;code&gt;nic1=nat&lt;/code&gt;), blocking its access to the host's local network. Before every new job, a script ruthlessly rolled the virtual machine back to a clean snapshot. The runner itself launched with the &lt;code&gt;--ephemeral&lt;/code&gt; flag for one-time registration, while an additional &lt;code&gt;Unregister-OrphanedRunner.ps1&lt;/code&gt; script scrubbed hanging records via the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Price of Genuine Isolation with a Custom Supervisor
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcfw32dy5lndh0d3htr0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcfw32dy5lndh0d3htr0.jpg" alt="Clean VM snapshot" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many wonder why we wrote a custom polling script when GitHub offers built-in &lt;em&gt;Organization Runners&lt;/em&gt;. The standard mechanism certainly allows sharing runner pools across multiple repositories. It completely fails at managing the lifecycle of the virtual environments themselves.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--ephemeral&lt;/code&gt; flag successfully removes a runner from the pool after task completion. It does nothing to restore the filesystem to its original pristine state. To wrap every single task in a strict cycle of snapshot rollbacks and VM launches, we still needed an external controller. The custom REST API polling script became a mandatory tax for ensuring a clean environment for every build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Watchdog Monitoring Itself
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v3xlg1m84rpwoi0qll8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v3xlg1m84rpwoi0qll8.jpg" alt="Watchdog looking in mirror" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The single server became a critical point of failure for the entire project. Any freeze in the supervisor instantly paralyzed code checks and documentation publishing. For the first few weeks the script ran unsupervised and regularly demanded manual restarts whenever VirtualBox spawned zombie processes. We wrote a separate watchdog script tasked with automatically restarting the frozen controller.&lt;/p&gt;

&lt;p&gt;The logic was primitive: if the &lt;code&gt;supervisor.log&lt;/code&gt; file stopped updating and the &lt;code&gt;VBoxHeadless&lt;/code&gt; process wasn't running, the watchdog pulled the plug. We shot ourselves in the foot on day one. The watchdog diligently wrote its own diagnostic logs into the very same &lt;code&gt;supervisor.log&lt;/code&gt; file. It constantly updated the file modification time, reliably blocking its own check condition.&lt;/p&gt;

&lt;p&gt;We quickly separated the logs into different files, and the system appeared to run smoothly. The following morning the watchdog lost its mind and began systematically killing a perfectly healthy supervisor for no apparent reason. This sent us down a two-day detective investigation into a network ghost in the machine, which we will cover in the next part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/escaping-to-self-hosted-runners" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/escaping-to-self-hosted-runners&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>selfhosted</category>
      <category>story</category>
    </item>
    <item>
      <title>The Phantom That Wasn't There: How Unreliable grep Made Us Chase Ghosts</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 18 Sep 2026 04:58:58 +0000</pubDate>
      <link>https://dev.to/flude_team/the-phantom-that-wasnt-there-how-unreliable-grep-made-us-chase-ghosts-29fn</link>
      <guid>https://dev.to/flude_team/the-phantom-that-wasnt-there-how-unreliable-grep-made-us-chase-ghosts-29fn</guid>
      <description>&lt;p&gt;One of the hardest stages in developing a documentation generator is alignment with legacy tools. When you migrate from an old, decades-proven proprietary generator to your own custom pipeline, you are obligated to prove that you haven't lost a single class.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8jw5divy9yzyx8ao50h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8jw5divy9yzyx8ao50h.jpg" alt="The Phantom Bug Grep" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We wrote an automated system that compared thousands of entities generated by the old tool with what our new Flude-based pipeline found. And one day, we stumbled upon a terrifying anomaly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anomaly in the Matrix
&lt;/h2&gt;

&lt;p&gt;The alignment script happily reported: Flude found and generated documentation for a struct named &lt;code&gt;GraphicsAsset&lt;/code&gt; and its smart pointer &lt;code&gt;GraphicsAssetPtr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem was that in the old, reference documentation, these entities didn't exist. At all.&lt;/p&gt;

&lt;p&gt;According to our logic, if the legacy generator didn't find the class, it's highly likely this class simply doesn't exist, and our parser made a mistake somewhere, "gluing" someone else's names together. We had to check the source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero Results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6r1onq44qs8nc7r0gqd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6r1onq44qs8nc7r0gqd.jpg" alt="Zero grep results" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We did what any engineer would do—we opened a terminal and ran a quick global text search (&lt;code&gt;grep&lt;/code&gt; / &lt;code&gt;ripgrep&lt;/code&gt;) across the entire multi-gigabyte codebase of our enterprise C++ product:&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="s2"&gt;"GraphicsAsset"&lt;/span&gt; ./sdk_sources/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;0 matches&lt;/strong&gt;. Absolute emptiness.&lt;/p&gt;

&lt;p&gt;We checked with case-insensitivity, we searched for the smart pointer. Nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Panic: The Hallucinating Parser
&lt;/h2&gt;

&lt;p&gt;It was a moment of genuine horror. The codebase didn't contain the string &lt;code&gt;GraphicsAsset&lt;/code&gt;. But somehow, our parser hadn't just found this name; it had built a beautiful HTML page for it, complete with a list of methods, properties, and inheritance!&lt;/p&gt;

&lt;p&gt;The conclusion seemed obvious: our parser had gone insane. It had started fabricating C++ classes out of thin air. Perhaps the logic for expanding complex macros had broken, or Doxygen's cache had become so corrupted that it started stitching pieces of different files into a single "Frankenstein".&lt;/p&gt;

&lt;p&gt;We were already preparing to open the debugger and rewrite the token-linking logic in the core.&lt;/p&gt;

&lt;h2&gt;
  
  
  Autopsy of the Cache
&lt;/h2&gt;

&lt;p&gt;Fortunately, before tearing apart the core codebase, we decided to look at the raw intermediate data. We went to the directory where Doxygen dumped its low-level XML, and searched there.&lt;/p&gt;

&lt;p&gt;Suddenly, sitting quietly on the disk, was a file named &lt;code&gt;struct_graphics_asset.xml&lt;/code&gt;. We opened it.&lt;/p&gt;

&lt;p&gt;Inside was perfectly formed XML. It didn't look like a hallucination. Moreover, in the &lt;code&gt;&amp;lt;location&amp;gt;&lt;/code&gt; tag, the parser pedantically pointed out exactly where it got this class:&lt;br&gt;
&lt;code&gt;Core/Include/Graphics/MaterialConfig.h:3006-3061&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Phantom Takes Flesh
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftvx3l7dpk9cjqar1jgzm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftvx3l7dpk9cjqar1jgzm.jpg" alt="MaterialConfig.h macro" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We opened the &lt;code&gt;MaterialConfig.h&lt;/code&gt; file and scrolled down to line 3006.&lt;/p&gt;

&lt;p&gt;And there, in the very heart of a massive legacy header, hid a completely real, living struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;CORE_ABSTRACT&lt;/span&gt; &lt;span class="n"&gt;GRAPHICS_EXPORT&lt;/span&gt; &lt;span class="n"&gt;GraphicsAsset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... dozens of lines of valid C++ code&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It had always been there. The legacy generator simply couldn't parse it because of complex macros, but our Flude could.&lt;/p&gt;

&lt;p&gt;So why did the global &lt;code&gt;rg&lt;/code&gt; yield zero matches? In enterprise monoliths, there are always plenty of reasons. Perhaps the &lt;code&gt;Core/Include&lt;/code&gt; path was listed in a tricky &lt;code&gt;.gitignore&lt;/code&gt;, and the smart &lt;code&gt;ripgrep&lt;/code&gt; ignored it. Perhaps the file had an exotic Windows encoding that broke the search. Perhaps it was a mounted submodule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A global text search offered an absence of evidence as evidence of absence.&lt;/p&gt;

&lt;p&gt;This case taught us a crucial rule: &lt;strong&gt;a text search is not a C++ parser&lt;/strong&gt;. Trying to analyze the structure of a C++ monolith with regular expressions or &lt;code&gt;grep&lt;/code&gt; is a path to disaster and false conclusions.&lt;/p&gt;

&lt;p&gt;If you doubt your parser, never use text search as the arbiter. Trust only the raw dumps of the compiler itself or the Abstract Syntax Tree (AST). And most importantly: do not try to "fix" a bug in the core until you prove its existence in the actual source of truth.&lt;/p&gt;

&lt;p&gt;But parsing and cache problems paled in comparison to our infrastructural challenges. From day one, our project was built as an ecosystem of several independent repositories. How this architecture turned into Git traps and nearly broke our Continuous Integration—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/the-phantom-bug-grep" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/the-phantom-bug-grep&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>c</category>
      <category>debugging</category>
      <category>grep</category>
      <category>parsing</category>
    </item>
    <item>
      <title>The Leak That Won't Die: How We Broke Python's tempfile</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 15 Sep 2026 03:52:53 +0000</pubDate>
      <link>https://dev.to/flude_team/the-leak-that-wont-die-how-we-broke-pythons-tempfile-95o</link>
      <guid>https://dev.to/flude_team/the-leak-that-wont-die-how-we-broke-pythons-tempfile-95o</guid>
      <description>&lt;p&gt;Production crashes always happen suddenly. This time, the alarm was raised by the core C++ SDK developers. Their scheduled nightly build failed on the CI runner with a classic diagnosis: &lt;code&gt;No space left on device&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzqqegw2r4vml7rdc0f3o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzqqegw2r4vml7rdc0f3o.jpg" alt="The Leak That Wont Die" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We ran out of disk space. A quick post-mortem revealed that the &lt;code&gt;/tmp&lt;/code&gt; folder was bursting with thousands of temporary directories. Their names all started the same way: &lt;code&gt;ude_xml_&lt;/code&gt;. It was our code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture of the Crash
&lt;/h2&gt;

&lt;p&gt;As we've mentioned before, our engine handles Doxygen in a tricky way. We feed it source code, it spits out a massive XML dump, and we parse that dump. For every run, the engine created a temporary folder using &lt;code&gt;tempfile.mkdtemp()&lt;/code&gt;. A dynamically generated &lt;code&gt;Doxyfile&lt;/code&gt; was placed there, and Doxygen would dump hundreds of megabytes of XML into it.&lt;/p&gt;

&lt;p&gt;The plan was simple: at the very end of the pipeline, a &lt;code&gt;cleanup()&lt;/code&gt; function would run, executing an honest &lt;code&gt;shutil.rmtree()&lt;/code&gt; on that folder. But the plan had a flaw. If the parsing failed with an exception, the pipeline aborted, &lt;code&gt;cleanup()&lt;/code&gt; was never called, and a folder filled with megabytes of garbage was left stranded in &lt;code&gt;/tmp&lt;/code&gt; forever.&lt;/p&gt;

&lt;p&gt;Complaints started pouring in from teams building massive projects. The error was flaky. We tried wrapping the call in a &lt;code&gt;try/finally&lt;/code&gt; block, but the engine's logic was smeared across dozens of classes. The leak persisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  A False Sense of Security: Trusting the GC
&lt;/h2&gt;

&lt;p&gt;How do you solve the problem of forgotten resource cleanup in Python? "Delegate the cleanup to the Garbage Collector (GC)!" we thought.&lt;/p&gt;

&lt;p&gt;Instead of &lt;code&gt;mkdtemp&lt;/code&gt;, we rewrote the logic to use the &lt;code&gt;tempfile.TemporaryDirectory&lt;/code&gt; object. Its main advantage is that when the object is deleted from memory, its finalizer (&lt;code&gt;__del__&lt;/code&gt;) automatically triggers the folder's deletion on disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0e73026d4rcra6lho5ii.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0e73026d4rcra6lho5ii.jpg" alt="Python Garbage Collector Overload" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To ensure the folder lived exactly as long as the engine needed it, we saved a reference to the object in a global list inside the manager class:&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;temp_dir_obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ude_xml_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_active_temp_dirs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_dir_obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It seemed like elegant, "pythonic" code. The object lives in the manager's memory and is automatically deleted when the script finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fatal Mistake
&lt;/h2&gt;

&lt;p&gt;We rolled out this "fix" to production. For a few days, the complaints stopped. We patted ourselves on the back and closed the issue. And then the CI runners started crashing again.&lt;/p&gt;

&lt;p&gt;We missed one "tiny" detail. Hoping that objects are &lt;em&gt;always&lt;/em&gt; deleted, and that the Garbage Collector works &lt;em&gt;immediately&lt;/em&gt;, is a ticking time bomb.&lt;/p&gt;

&lt;p&gt;In the case of giant C++ projects like Flude, the engine parsed dozens of modules sequentially. A new directory was created for each module. Because of a complex web of circular references in our parsers (e.g., an AST node referenced its parent class, and the class held a list of its children), the engine's objects weren't deleted immediately. They hung in memory, waiting for a full garbage collection cycle. And, of course, &lt;code&gt;_active_temp_dirs&lt;/code&gt; stayed in memory too.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TemporaryDirectory&lt;/code&gt; objects piled up in RAM by the hundreds. But the worst part was this: when the GC finally got to them and called &lt;code&gt;cleanup()&lt;/code&gt;, it triggered a &lt;strong&gt;massive synchronous deletion of files from the disk&lt;/strong&gt;, blocking the main execution thread for several minutes. Because of this, other services on the server began failing due to timeouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Fix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy8w88ac6ynkbv3cev6hb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy8w88ac6ynkbv3cev6hb.jpg" alt="Context Manager containment" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The conclusion of our investigation was grim: using finalizers to free heavy system resources is an anti-pattern. We couldn't control &lt;em&gt;when&lt;/em&gt; the GC would trigger, which meant we couldn't control the disk I/O load.&lt;/p&gt;

&lt;p&gt;we had to rewrite the pipeline's architecture so that the lifecycle of temporary folders was managed by strict &lt;code&gt;with&lt;/code&gt; context managers, not the GC.&lt;/p&gt;

&lt;p&gt;We introduced a strict rule: creating the folder and calling &lt;code&gt;cleanup()&lt;/code&gt; must happen in the same place. If any part of the engine needs these files, it accesses them inside a &lt;code&gt;with&lt;/code&gt; block, guaranteeing that upon exiting the block (even via an exception), the folder is deleted immediately.&lt;/p&gt;

&lt;p&gt;To protect against regressions, we also added a "janitor"—a mechanism that forcibly checks &lt;code&gt;/tmp&lt;/code&gt; before a build starts and cleans up any orphaned garbage.&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Orphaned temporary directory found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;td&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Caller failed to invoke cleanup(). Force-cleaning now.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We learned a harsh lesson. Shifting the responsibility for system resources to Python's garbage collector is a direct path to disaster.&lt;/p&gt;

&lt;p&gt;But our victory over the garbage was just the beginning. While we were wrestling with Python, the real enemy was lurking in the very heart of our infrastructure. We thought we had tamed Doxygen, but it soon took its revenge, forcing us to rewrite the entire parser from scratch. How a simple C++ construct drove the documentation generator insane—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/the-leak-that-wont-die" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/the-leak-that-wont-die&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>memoryleak</category>
      <category>garbagecollection</category>
    </item>
    <item>
      <title>The Illusion of Speed: A Two-Level Cache Can't Speed Up What Doxygen Can't Skip</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:17:18 +0000</pubDate>
      <link>https://dev.to/flude_team/the-illusion-of-speed-a-two-level-cache-cant-speed-up-what-doxygen-cant-skip-3560</link>
      <guid>https://dev.to/flude_team/the-illusion-of-speed-a-two-level-cache-cant-speed-up-what-doxygen-cant-skip-3560</guid>
      <description>&lt;p&gt;Every single full documentation rebuild ruthlessly executed &lt;code&gt;doxygen.exe&lt;/code&gt; and parsed its massive XML output. The tool simply didn't care if you fixed a single typo in the comments or rewrote half the SDK. On real projects this translated into minutes of agonizing waiting after every push, even though the vast majority of the work could have been safely skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Fingerprints for Cache Control
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95hk147xblykamc7jxin.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95hk147xblykamc7jxin.jpg" alt="Two cache levels: XML and IR" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We built the caching scheme around two independent fingerprints to strictly divide responsibilities. The &lt;code&gt;xml_fingerprint&lt;/code&gt; determines whether the Doxygen run can be skipped entirely. We compute it as a content hash spanning the binary, the effective &lt;code&gt;Doxyfile&lt;/code&gt;, and the source tree. Modification time checks were thrown out immediately to prevent fresh CI checkouts from falsely invalidating the cache. The &lt;code&gt;OUTPUT_DIRECTORY&lt;/code&gt; parameter also had to be excluded from the hash to avoid a circular dependency between the fingerprint and the generated files.&lt;/p&gt;

&lt;p&gt;The second fingerprint, &lt;code&gt;ir_fingerprint&lt;/code&gt;, decides the fate of parsing the generated XML into a &lt;code&gt;ProjectCatalog&lt;/code&gt; structure. It incorporates the first hash but adds parser configuration like &lt;code&gt;exclude_swig_internals&lt;/code&gt; and the logic hash. Any minor tweak to the parsing rules instantly invalidates the saved internal representation (IR). We managed to catch a potential state desync bug before writing any code thanks to a couple of independent AI review sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries of Compute Optimization
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgofs4jr4nuyuhxlmlg60.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgofs4jr4nuyuhxlmlg60.jpg" alt="Cache invalidation risk" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The compute time cache abruptly stops at the IR boundary. The actual transformation of the &lt;code&gt;ProjectCatalog&lt;/code&gt; object into final Markdown is calculated from scratch every time. Observant readers might spot a &lt;code&gt;BuildCacheManager&lt;/code&gt; class in &lt;code&gt;storage.py&lt;/code&gt; that handles a rendering cache. It operates purely on file I/O to protect local Hugo livereload from triggering falsely.&lt;/p&gt;

&lt;p&gt;We dropped the idea of caching the render computation for very pragmatic reasons. Rendering a single page is heavily tied to global state, such as the &lt;code&gt;sidebar.toml&lt;/code&gt; navigation tree and cross-links. Altering the taxonomy structure breaks the layout of dozens of files simultaneously, making invalidation tracking incredibly complex. Furthermore, Doxygen processing takes long minutes, while traversing the ready IR tree takes mere seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Speed Remains an Illusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqrf5shkuzwae2bxqxtz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqrf5shkuzwae2bxqxtz.jpg" alt="Monolithic rebuild" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our two-level cache only knows how to do one thing—completely skip a build step when fingerprints match. Doxygen physically lacks an incremental parsing mode and cannot run any faster. Any altered character in the sources forces us to endure a full parser run. True incrementality, where only the modified file gets recomputed, is fundamentally incompatible with a monolithic XML dump.&lt;/p&gt;

&lt;p&gt;In the architectural plans for the next major version we have already documented dropping Doxygen in favor of direct AST construction via &lt;code&gt;libclang&lt;/code&gt; or &lt;code&gt;tree-sitter&lt;/code&gt;—the choice between the two hasn't been made yet; the requirements treat them as equal candidates of the same technical direction. Incrementality becomes a natural property of the tool there, instead of something bolted on top with fingerprints—but that same shift will force us to throw away and rewrite the existing caching logic entirely. For now, we had to face the harsh limits of the free GitHub Actions cloud, which pushed us to build our own self-hosted runner. In the next episode we cover how we escaped to our own hardware.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/illusion-of-speed-caching" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/illusion-of-speed-caching&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>caching</category>
      <category>doxygen</category>
      <category>flude</category>
    </item>
    <item>
      <title>Flude.guide: The Name We Had to Find Between Udemy, a Debugger, and nude.com</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 08 Sep 2026 03:38:44 +0000</pubDate>
      <link>https://dev.to/flude_team/fludeguide-the-name-we-had-to-find-between-udemy-a-debugger-and-nudecom-5cee</link>
      <guid>https://dev.to/flude_team/fludeguide-the-name-we-had-to-find-between-udemy-a-debugger-and-nudecom-5cee</guid>
      <description>&lt;p&gt;When the time finally arrived to launch this blog, an uncomfortable reality surfaced. The internal working title "UDE" was completely dead on arrival. We faced problems far beyond simple domain availability. Someone else already owned &lt;code&gt;ude.com&lt;/code&gt; and &lt;code&gt;ude.io&lt;/code&gt;, but the fatal blow came from trademark law. The abbreviation is already a registered ® trademark—Universal Debug Engine, a well-known microcontroller debugging tool from Germany's PLS. Launching another developer tool under the exact same name guarantees a swift cease-and-desist letter. We preferred shipping software over arguing with corporate lawyers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three filters running in parallel
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fldphj41zbznj9n6dvsv1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fldphj41zbznj9n6dvsv1.jpg" alt="Naming search: from UDE to Flude" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We started hunting for a four- or five-letter replacement built around the "ude" substring. The evaluation process was brutal—and immediate. Sound, SEO potential, and legal safety were checked simultaneously. Any candidate could trip an alarm on any axis. One strike instantly eliminated the word from contention.&lt;/p&gt;

&lt;p&gt;Candidates like "nude", "dude", and "jude" crashed out on the sound check right away. Several variants barely registered as real English words. The archaic Scots term "blude" would confuse anyone outside of Edinburgh. Common words like "nude" or "dude" carry bizarre baggage that a strict technical reference manual desperately wants to avoid.&lt;/p&gt;

&lt;p&gt;The word "Etude" survived slightly longer. It sounds pleasant and bypasses direct trademark violations. The fatal flaw hides in its marketability. A massive global cosmetics brand already dominates search results for that term. Trying to build a presence under that banner means fighting through endless irrelevant noise. The original "UDE" title suffered from a similar chronic condition. Even ignoring the legal threat, the abbreviation looks far too much like the Udemy course platform.&lt;/p&gt;

&lt;p&gt;We eventually landed on "Flude". It possessed no magical qualities or overwhelming strengths. It simply stood as the first candidate that survived all three filters without breaking apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claiming the right zone
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fivuh38i7blt2xdytduvi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fivuh38i7blt2xdytduvi.jpg" alt="Choosing the TLD" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the name locked in, we needed a home on the internet. Premium extensions usually sit trapped in squatter portfolios. Buying a catchy short domain for thousands of dollars makes zero sense for a young project. We refused to negotiate with domain brokers.&lt;/p&gt;

&lt;p&gt;A quick search showed availability in the &lt;code&gt;.dev&lt;/code&gt; space at standard registrar pricing. It sounds reasonable, except that suffix heavily implies writing code and libraries. We needed a space dedicated to final technical documentation. The &lt;code&gt;.guide&lt;/code&gt; extension sat wide open, cost barely anything, and literally described our product. The choice made itself. We secured a flawless match between our brand name and its actual function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maturing alongside the infrastructure
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5aq8cupywakal9uj79z7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5aq8cupywakal9uj79z7.jpg" alt="Renaming Cloudflare Projects" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We locked down the domain while the brand was still baking internally. The new identity eventually cemented itself deep inside our infrastructure. We purged the outdated "ude-" prefix from all Cloudflare Pages projects. A massive single-day rename swept through both the user documentation and the engine repository.&lt;/p&gt;

&lt;p&gt;Next time, we will dive into the illusion of speed. We will demonstrate why a clever two-layer cache of XML and syntax trees completely fails to accelerate a bottleneck baked deep inside Doxygen itself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/flude-guide" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/flude-guide&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>branding</category>
      <category>cloudflare</category>
      <category>story</category>
      <category>flude</category>
    </item>
    <item>
      <title>Breaking Down Our Pipeline: From a Symlink Hack to Five Independent Deployments</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 04 Sep 2026 08:01:20 +0000</pubDate>
      <link>https://dev.to/flude_team/breaking-down-our-pipeline-from-a-symlink-hack-to-five-independent-deployments-1mjc</link>
      <guid>https://dev.to/flude_team/breaking-down-our-pipeline-from-a-symlink-hack-to-five-independent-deployments-1mjc</guid>
      <description>&lt;p&gt;Flude's documentation pipeline survived for years the same way most internal tooling does. It just worked. Nobody in their right mind wanted to touch it. The &lt;code&gt;engine&lt;/code&gt; package generated its API reference using a clunky symlink hack we mentioned in the &lt;a href="https://dev.to/en/blog/escaping-to-self-hosted-runners/"&gt;escape-to-GitHub-Actions episode&lt;/a&gt;. Platform constraints forced our hand here. Free GitHub Pages limits site publishing to public repositories only. The engine's source code absolutely had to stay private. We bypassed this by running &lt;code&gt;ln -s ../engine ./user-docs/engine&lt;/code&gt; during the CI build. The public &lt;code&gt;user-docs&lt;/code&gt; repo quietly built a site out of someone else's code. Flude processed those files on the fly to push the final static result out the door.&lt;/p&gt;

&lt;p&gt;That duct-tape setup actually got the job done. It also created a concrete wall of architectural coupling. Publishing a single line of API documentation required the private engine to leak its source into a public neighbor. The &lt;code&gt;user-docs&lt;/code&gt; project got stuck carrying the weight of an external build process. We couldn't build or test the engine's docs in isolation. The entire toolchain only triggered from inside the documentation repo. Every single run pulled down megabytes of fresh code. The &lt;code&gt;design-docs&lt;/code&gt; project suffered from the exact same disease on a smaller scale. Its GitHub Pages deployment ran centrally out of the giant &lt;code&gt;Pipeline&lt;/code&gt; umbrella repo. It relied on an SSH deploy key to punch through into a completely different repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding a host that asks fewer questions
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fox5o3pr7phj0xz0h7r1j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fox5o3pr7phj0xz0h7r1j.jpg" alt="Five repositories instead of one" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We weighed two escape routes for the engine. One option involved shipping the parsed intermediate representation as a versioned artifact like a wheel or GitHub Release. Downstream projects could grab that package and render the docs locally. That sounds clean on paper. In reality, it introduces a nightmare of schema versioning we desperately wanted to avoid. The brute-force approach won out in the end. The engine now parses, renders, and deploys its own reference from its own CI pipeline. Zero intermediate contracts. Zero artifact publishing rituals.&lt;/p&gt;

&lt;p&gt;We still had to bypass the old GitHub Pages restriction. The platform demanded a public repo—so we ditched the platform. Cloudflare Pages offers a free tier that deploys finished build artifacts while completely ignoring repository visibility. The engine finally publishes its own reference while staying hidden from the world. The actual migration required nothing more than copying &lt;code&gt;Doxyfile&lt;/code&gt;, &lt;code&gt;hugo-site&lt;/code&gt;, and &lt;code&gt;sidebar.toml&lt;/code&gt; directly into the engine repository. The generated &lt;code&gt;content/&lt;/code&gt; and &lt;code&gt;public/&lt;/code&gt; directories went straight into &lt;code&gt;.gitignore&lt;/code&gt;. They no longer haunt our commit history as static fixtures.&lt;/p&gt;

&lt;p&gt;We rolled this exact logic down the rest of the chain. Both &lt;code&gt;design-docs&lt;/code&gt; and &lt;code&gt;user-docs&lt;/code&gt; migrated to Cloudflare Pages. Each project received a dedicated &lt;code&gt;deploy.yml&lt;/code&gt; workflow. GitHub Actions provides native read access to the runner's repository, making deploy keys obsolete. The old &lt;code&gt;cloudflare/pages-action@v1&lt;/code&gt; had already been archived by the community. We skipped the legacy tooling and moved all five repositories straight to the supported &lt;code&gt;wrangler-action&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One token to rule them all
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zrthg6vl4mvgmmc1gv9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zrthg6vl4mvgmmc1gv9.jpg" alt="Token Rotation" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our perfectly clean plan slammed into a brick wall of reality right about here. Every deployment requires a &lt;code&gt;CF_API_TOKEN&lt;/code&gt;. The textbook reaction is generating a narrowly scoped token with least privilege for each repository. Cloudflare violently disagrees with this approach. You cannot scope an API token to a single Pages project. They grant permissions across the entire account—or nothing at all. Any "narrow" token we issued would instantly gain full access to our entire infrastructure. Worse yet, Cloudflare displays the token value exactly once during creation. If you miss it, you have to roll the credential entirely.&lt;/p&gt;

&lt;p&gt;The rollout process turned into a timed sprint. We roll the token once. Then we distribute the new string across five repositories within a few frantic minutes. Everything happens through an interactive prompt. Secrets never touch a script, a file on disk, or a Slack message. Five independent repositories now share a single token—and a single rotation window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auditing the dead code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxp5erooa7zs6cxia6euz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxp5erooa7zs6cxia6euz.jpg" alt="CI Bill Audit" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the deployments turned green, we had to ask ourselves a tough question. How much of this machinery was actually doing anything useful? The audit unearthed a pile of historical garbage. A &lt;code&gt;repository_dispatch&lt;/code&gt; trigger from every submodule used to restart integration tests in the umbrella repo. It fired before the gitlink even bumped. The CI was aggressively testing a state that hadn't changed. We gutted that nonsense and replaced it with a simple six-hour schedule on the bump job itself. We lost absolutely nothing in speed because those PRs never auto-merged anyway.&lt;/p&gt;

&lt;p&gt;The language test matrix in the umbrella repo turned out to be a carbon copy of the engine's internal CI. The original version even enforced a stricter coverage threshold. A job designed to verify the built site was quietly testing an architecture that died months ago. It merged Hugo's output with a VitePress tree to check cross-links. We had long since split them into separate Cloudflare Pages projects on different domains. The only connection left is a standard external link. You never spot these ghosts reading a clean architecture diagram. They only surface when you dig through the raw runner logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Severing the final tie
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagqoqa5zqxzt77ea1yop.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagqoqa5zqxzt77ea1yop.jpg" alt="Cutting the Last Dependency" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One tiny cross-repository hook remained. The failure-alert step in every workflow dynamically pulled a shared Telegram notifier from &lt;code&gt;Pipeline&lt;/code&gt; at runtime. That single step required another authentication token. A supposedly independent repository was still reaching back to daddy for help. We copy-pasted that action file into every single project. Maintaining five copies instead of one shared source is the real cost of independence.&lt;/p&gt;

&lt;p&gt;You are reading this blog on a site built with the exact same pattern. It uses self-deployment with zero external deploy keys. It runs its own copy of the notifier. It lives on that exact same Cloudflare architecture.&lt;/p&gt;

&lt;p&gt;Next time, we'll explain how we hunted down a public name for this documentation. The original working title turned out to be locked behind corporate lawyers, sending us down a rabbit hole of weird domain names.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/decomposing-the-pipeline-monorepo" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/decomposing-the-pipeline-monorepo&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>cicd</category>
      <category>cloudflare</category>
      <category>story</category>
    </item>
    <item>
      <title>The 'if not 0:' Easter Egg: How Our AI Audit Found Dead Code in the Orchestrator</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 01 Sep 2026 05:12:56 +0000</pubDate>
      <link>https://dev.to/flude_team/the-if-not-0-easter-egg-how-our-ai-audit-found-dead-code-in-the-orchestrator-2ii0</link>
      <guid>https://dev.to/flude_team/the-if-not-0-easter-egg-how-our-ai-audit-found-dead-code-in-the-orchestrator-2ii0</guid>
      <description>&lt;p&gt;We keep exposing the internals of our project without embellishment. Recently, we launched an autonomous AI audit across the entire engine core. The agent was tasked with cleaning up type hints, fixing docstrings, and deleting dead code. Living humans rarely get around to this kind of mundane technical hygiene.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Find
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famfnwzhty0unbo5vpanb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famfnwzhty0unbo5vpanb.jpg" alt="An Easter egg in the code" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent systematically combed through the files and stumbled inside &lt;code&gt;ude/orchestrator.py&lt;/code&gt;. Inside the &lt;code&gt;load_sidebar_toml&lt;/code&gt; function, it unearthed a fascinating piece of logic. We stared at the screen and couldn't quite believe our eyes.&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;toml_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sidebar.toml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;toml_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UdeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Required sidebar.toml not found in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Every document directory must contain a sidebar.toml file.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero in Python is interpreted as falsy. The expression &lt;code&gt;not 0&lt;/code&gt; always evaluates to true. The outer condition simply forced an extra indentation level and was guaranteed to execute every single time. It was a useless wrapper that stole a line in the stack trace from anyone trying to debug the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Got There
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd3t5518t98xy921spyhy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd3t5518t98xy921spyhy.jpg" alt="Dead Code in Pull Request" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A quick &lt;code&gt;git blame&lt;/code&gt; revealed the author and the creation time. The line sneaked in via PR #56 ("Feature/section 2.1 orchestrator library api"), part of a massive orchestrator refactoring. The developer temporarily disabled the block using &lt;code&gt;if 0:&lt;/code&gt; for a quick local debugging session. They flipped the logic back with a negation, committed the changes, and forgot to remove the wrapper before the merge. The test suite happily passed. The behavior of the code remained completely unchanged.&lt;/p&gt;

&lt;p&gt;This construct didn't break any functionality or open security vulnerabilities. It simply added pure cognitive noise. Everyone reading this file would freeze for a second, trying to decipher the sacred meaning behind the check. That extra second of confusion accumulates into hours of wasted time across thousands of lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgsi3ew1iw5vmwcfeiyy4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgsi3ew1iw5vmwcfeiyy4.jpg" alt="A minimal fix with zero behavior change" width="800" height="447"&gt;&lt;/a&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;toml_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sidebar.toml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;toml_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UdeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Required sidebar.toml not found in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Every document directory must contain a sidebar.toml file.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We deleted the wrapper and shifted the code to the left. The full test suite ran clean—731 passed, 0 failed, coverage holding at 98.12%. The system's behavior hasn't shifted a millimeter. Regular automated auditing pays off exactly through these tiny cleanups. It sweeps out harmless garbage in bulk while you focus on complex challenges. Speaking of complex challenges. Next time, we'll break down exactly how we sliced our massive pipeline monorepo into five fully independent deployments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/stale-conditional-easter-egg" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/stale-conditional-easter-egg&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postmortem</category>
      <category>aiagents</category>
      <category>codequality</category>
      <category>python</category>
    </item>
  </channel>
</rss>
