<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[ITNEXT - Medium]]></title>
        <description><![CDATA[ITNEXT is a platform for IT developers &amp; software engineers to share knowledge, connect, collaborate, learn and experience next-gen technologies. - Medium]]></description>
        <link>https://itnext.io?source=rss----5b301f10ddcd---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>ITNEXT - Medium</title>
            <link>https://itnext.io?source=rss----5b301f10ddcd---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 16 Jul 2026 04:51:07 GMT</lastBuildDate>
        <atom:link href="https://itnext.io/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Argo CD App-of-apps and Configuration as Data]]></title>
            <link>https://itnext.io/argo-cd-app-of-apps-and-configuration-as-data-81930902f4d5?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/81930902f4d5</guid>
            <category><![CDATA[configuration-as-data]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[confighub]]></category>
            <category><![CDATA[argo-cd]]></category>
            <dc:creator><![CDATA[Chanwit Kaewkasi]]></dc:creator>
            <pubDate>Wed, 15 Jul 2026 17:22:05 GMT</pubDate>
            <atom:updated>2026-07-15T17:22:04.332Z</atom:updated>
            <content:encoded><![CDATA[<p>App-of-Apps is how a lot of teams run many Argo CD applications from one place — but what it does, and how it looks, changes once the configuration is stored as data. Everything below comes from a working setup: a kind cluster running Argo CD, a root Application that pulls an app-of-apps unit from ConfigHub over OCI, three child Applications under it, and a guestbook workload deployed to a per-environment <strong>vcluster</strong> from its own Space.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PH_ixYHYFca8Q8JrILP-AA.png" /></figure><p>If you run Argo CD past a handful of applications, you’ve probably built an app-of-apps. You create a root Application whose source is a directory of more Application manifests, and those point at the real workloads. It’s a reasonable way to bootstrap a cluster, or a set of clusters, from one place. When the directory got large, you may have switched to ApplicationSets and let a generator produce the child Application objects instead of maintaining them by hand.</p><p>I’m not writing this to argue against the pattern. It solves a real problem. But it’s worth looking at why you end up reaching for it, because the reasons line up almost exactly with the capabilities we’ve been building into <a href="https://hub.confighub.com">ConfigHub</a>: querying configuration across a fleet, propagating values between related pieces, and checking and gating changes before they’re applied.</p><h3>What you’re actually asking app-of-apps to do</h3><p>Once you have more than a few applications, you stop caring about them one at a time and start needing to operate on the group. Concretely, that means:</p><ul><li>Seeing all the apps in a set together, not app by app.</li><li>Sharing values across them — the destination cluster and namespace, the registry, a region, a common set of labels or policy resources — without restating those values in every child.</li><li>Expressing ordering and dependencies between them, and deciding what happens to the children when the root changes or is deleted.</li><li>Having one place to bootstrap and review the whole thing.</li></ul><p>App-of-apps gives you a way to do each of these on top of Git. Grouping becomes a directory. Shared values get duplicated across the children, or threaded through an ApplicationSet generator. Ordering becomes sync-wave annotations. The single place to review is the root Application.</p><p>People reach for app-of-apps when the apps are heterogeneous and <em>sometimes there are thousands of them</em>. With the Git version, changing one of those apps means the usual commit, push, and review, even when you only wanted to find it and edit a value. In a real setup the manifests for each environment often live in separate repos, so there’s no single place to see or manage them.</p><p>In ConfigHub the apps are stored as data you can query across Spaces, which makes them easier to find and change.</p><h3>The same app-of-apps, backed by data</h3><p>To make this concrete, here’s the setup I ran. Four Spaces in ConfigHub:</p><ul><li>guestbook-aoa holds two Units: root (the root Application CR) and app-of-apps (the three child Applications).</li><li>guestbook-dev, guestbook-staging, and guestbook-prod each hold one guestbook Unit — the actual workload, a Deployment and a Service. All of them are <a href="https://docs.confighub.com/background/concepts/variant/">Variants</a> of the guestbook-base Space.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5IHPL7-JkPqYsd39EORV-A.png" /><figcaption>Component View in ConfigHub. Each box (a Component) represents a Space that goes to each environment, dev, staging, prod, respectively.</figcaption></figure><p>The shape of the pattern is unchanged. What’s different is where the manifests live: instead of a Git directory, each piece is a Unit that Argo pulls over OCI. The root Application points at the OCI address of the app-of-apps unit:</p><pre>apiVersion: argoproj.io/v1alpha1<br>kind: Application<br>metadata:<br>  name: root<br>  namespace: argocd<br>spec:<br>  project: default<br>  source:<br>    repoURL: oci://oci.hub.confighub.com/unit/guestbook-aoa/app-of-apps<br>    targetRevision: latest<br>    path: &quot;.&quot;<br>  destination:<br>    server: https://kubernetes.default.svc<br>    namespace: argocd<br>  syncPolicy:<br>    automated: { prune: true, selfHeal: true }</pre><p>And the app-of-apps unit contains the three children, each pointing at the guestbook unit in its environment’s Space and at its own cluster:</p><pre>apiVersion: argoproj.io/v1alpha1<br>kind: Application<br>metadata:<br>  name: guestbook-dev<br>  namespace: argocd<br>spec:<br>  project: default<br>  source:<br>    repoURL: oci://oci.hub.confighub.com/unit/guestbook-dev/guestbook<br>    targetRevision: latest<br>    path: &quot;.&quot;<br>  destination:<br>    name: dev-cluster<br>    namespace: guestbook<br>  syncPolicy:<br>    automated: { prune: true, selfHeal: true }<br>    syncOptions:<br>      - CreateNamespace=true<br>---<br># Two other Applications here<br># ... guestbook-staging -&gt; staging-cluster, guestbook-prod -&gt; prod-cluster</pre><p>Creating the units is two commands:</p><pre>cub unit create --space guestbook-aoa app-of-apps app-of-apps.yaml<br>cub unit create --space guestbook-aoa root root.yaml</pre><p>Argo CD doesn’t know or care that the source is ConfigHub rather than Git — it sees an OCI repo. Which means the app-of-apps mechanics all still work: root syncs, children appear, children sync, workloads deploy. The difference is what you can do with the configuration before and around that sync loop.</p><h3>Keeping the two systems readable together</h3><p>ConfigHub and Argo CD name different things, and a single change flows through both, so it’s worth being deliberate about how the names line up. The convention I settled on: <strong>a Space’s OCI worker and target are named after the Argo CD cluster that Space’s config is deployed to.</strong> Term by term:</p><ul><li>A <strong>Space</strong> (a bucket of Units, one per environment) maps to an environment, or a set of Applications — here guestbook-dev, guestbook-staging, guestbook-prod, and guestbook-aoa.</li><li>A <strong>Unit</strong> (versioned KRM config data) is the manifests an Application syncs (its spec.source) — the guestbook Unit becomes the guestbook-dev Application’s source.</li><li>The app-of-apps Unit is the directory of child Applications, fanning out to three; the root Unit is the root Application CR, the one you bootstrap.</li><li>A <strong>Worker</strong> plus <strong>Target</strong> (OCI) has no Argo counterpart — it’s ConfigHub’s publishing mechanism — and I name them after the destination cluster: dev-cluster, staging-cluster, prod-cluster, in-cluster.</li><li>A Unit’s OCI address oci://…/unit/&lt;space&gt;/&lt;unit&gt; is the Application’s spec.source.repoURL, the pull source.</li><li>The live <strong>Cluster</strong>, registered in Argo and addressed by name, must equal the child’s destination.name.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zdy8znxJww5JthkRpvbIqA.png" /><figcaption>Cluster definitions in ArgoCD whose names match those of Targets.</figcaption></figure><p>So guestbook-dev’s target is dev-cluster, which is also the child Application’s destination.name and the cluster registered in Argo CD — followed end to end. The guestbook-aoa Space deploys to the host cluster Argo CD itself runs in, so its target is in-cluster, Argo CD’s own name for that cluster. Targets have no Argo CD equivalent — which is why naming them after the destination cluster is what ties the two systems together at a glance.</p><h3>The set of apps, as a query</h3><p>With the units published, looking at the set of apps is a query over data that already exists:</p><pre>$ cub unit list --space &quot;guestbook-*&quot; --where &quot;Labels.Component = &#39;guestbook&#39;&quot;<br>SPACE              UNIT          TYPE                 GATED  UNAPPLIED<br>guestbook-dev      guestbook-ui  apps/v1/Deployment   no     no<br>guestbook-staging  guestbook-ui  apps/v1/Deployment   no     no<br>guestbook-prod     guestbook-ui  apps/v1/Deployment   yes    yes</pre><p>Nothing generates that list. The Units are the data, and the set is a where clause over them. Each Unit is materialized — plain YAML with real values, no templates — and it’s exactly what Argo pulls and syncs:</p><pre>apiVersion: apps/v1<br>kind: Deployment<br>metadata:<br>  name: guestbook-ui<br>spec:<br>  replicas: 2<br>  revisionHistoryLimit: 3<br>  selector:<br>    matchLabels:<br>      app: guestbook-ui<br>  template:<br>    metadata:<br>      labels:<br>        app: guestbook-ui<br>    spec:<br>      containers:<br>      - image: gcr.io/google-samples/gb-frontend:v5<br>        name: guestbook-ui<br>        ports:<br>        - containerPort: 80<br>---<br>apiVersion: v1<br>kind: Service<br>metadata:<br>  name: guestbook-ui<br>spec:<br>  ports:<br>  - port: 80<br>    targetPort: 80<br>  selector:<br>    app: guestbook-ui</pre><p>Most of what follows comes down to that difference: the thing you query and change is the same thing that gets deployed.</p><h3>Checking changes before they’re applied</h3><p>This is the capability I’d miss most going back to a plain app-of-apps. Normally a bad child config syncs and the violation turns up afterward, in the cluster, if an admission controller or a scan catches it at all. Catching it earlier means rendering every child in CI and running your checks there, which brings back the per-repo commit, push, and render steps.</p><p>In ConfigHub, we have a concept called Triggers, and validation and policy tools like Kyverno, OPA Gatekeeper, Kubescape, and others can all be built as Triggers. They run on every change to the configuration data, before it’s released. When a check fails, the trigger attaches an Apply Gate, or a warning if you’d rather not block. Across the same set of apps:</p><pre>$ cub unit list --space &quot;guestbook-*&quot; --where &quot;GatedCount &gt; 0&quot;<br>SPACE           UNIT          GATE          REASON<br>guestbook-prod  guestbook-ui  no-critical   container &#39;guestbook-ui&#39; runs as root (PodSecurity restricted)</pre><p>The check runs when the data changes, not when Argo decides to sync it. In the setup above, a gated guestbook-prod unit simply never gets a new latest published to oci://…/unit/guestbook-prod/guestbook, so Argo keeps syncing the last good version — the gate blocks release regardless of whether the change came from a person, from CI, or from an agent.</p><h3>Seeing what a change affects</h3><p>A template-based app-of-apps can only show you the source you edited, not the set of live configs it expands to. Because ConfigHub stores the materialized data along with the upstream and downstream relationships between Units, you can look at what a change reaches before releasing it:</p><pre>$ cub unit tree --node guestbook-base --downstream<br>guestbook-base<br>|- guestbook-dev/guestbook-ui<br>|- guestbook-staging/guestbook-ui<br>`- guestbook-prod/guestbook-ui   [gated]</pre><p>That’s a review an app-of-apps doesn’t give you: not the template you changed, but the live targets it expands to and which of them is currently blocked.</p><h3>Bootstrapping</h3><p>The one place a Git-based app-of-apps and this setup look identical is the very first step: something has to put the root Application into the cluster. Here it’s one line, straight from ConfigHub:</p><pre>cub unit data --space guestbook-aoa root | kubectl apply -f -<br>kubectl get applications -n argocd</pre><p>Argo CD takes over from there: it pulls app-of-apps over OCI, creates the three child Applications, each pulls its guestbook unit and deploys into its vcluster, creating the guestbook namespace on the way. End state:</p><pre>NAME                SYNC STATUS   HEALTH STATUS<br>root                Synced        Healthy<br>guestbook-dev       Synced        Healthy<br>guestbook-staging   Synced        Healthy<br>guestbook-prod      Synced        Healthy</pre><h3>Wrapping up</h3><p>App-of-apps is a sensible way to get grouping, shared values, dependencies, and a single point of control out of Git and templates, which weren’t built to provide them. What the setup above shows is that you don’t have to choose between the pattern and Configuration as Data: the root, the fan-out, and the children all still work when Argo CD pulls them from ConfigHub over OCI. The things app-of-apps makes awkward — sharing a value without duplicating it, checking the whole set before it syncs, turning on a policy everywhere, seeing what a change affects — are the things you get directly when the configuration behind those Applications is stored as data with relationships between the pieces.</p><p>If you already run an app-of-apps, the shared values and the fleet-wide policy rollout are the two things I’d try moving into ConfigHub first. You can do it for a single set of apps without touching the rest — the setup above is one root and three children on a laptop-sized kind cluster — which makes it easy to compare against what you have now.</p><p>ConfigHub is in preview — <a href="https://auth.confighub.com/sign-up">you can try it here</a>. If you do, I’d like to hear how it lines up with your current Argo CD setup; you can find me on <a href="https://www.linkedin.com/in/chanwit">LinkedIn</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=81930902f4d5" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/argo-cd-app-of-apps-and-configuration-as-data-81930902f4d5">Argo CD App-of-apps and Configuration as Data</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I Shipped 341 MB of JSON Through Google Apps Script — and Lived to Tell the Tale]]></title>
            <link>https://itnext.io/how-i-shipped-341-mb-of-json-through-google-apps-script-and-lived-to-tell-the-tale-ea364effc8b2?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/ea364effc8b2</guid>
            <category><![CDATA[data-pipeline]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[google-apps-script]]></category>
            <category><![CDATA[google-workspace]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Dmitry Kostyuk]]></dc:creator>
            <pubDate>Wed, 15 Jul 2026 17:20:13 GMT</pubDate>
            <atom:updated>2026-07-15T17:20:12.108Z</atom:updated>
            <content:encoded><![CDATA[<p>Google Apps Script is easily my favorite runtime, so much so that <a href="https://www.amazon.com/dp/B0FGTTQMZG">I wrote a book about it</a>. Yet, being a low-code environment it comes with its quirks, quotas and limitations. In a recent project I needed to process 300 spreadsheets totalling over 5 million cells in over 80,000 rows. I could eithr abandon the platform and spin up a “real” backend, or get creative. Really creative.</p><p>I got creative. And what emerged is an architecture I now believe every serious Apps Script developer should have in their toolbox: a <strong>two-half pipeline</strong> that compresses, chunks, and reassembles massive datasets by moving every heavy computation out of Apps Script entirely — into a local Node.js preprocessing step and the user’s own browser. Apps Script itself becomes the thinnest possible proxy: a stateless, sub-second string-returning bridge between Google Drive and the client.</p><p>This is the deep-dive story of that pipeline. Here’s what we’ll cover:</p><ul><li>The four killer constraints that make the naive approach impossible</li><li>The complete compress-chunk-upload-reassemble flow</li><li>the Vite + clasp + gws toolchain that makes it all work</li><li>Real measured numbers from production</li><li>The trade-offs (plus improvements) you’ll need to weigh if you adopt this pattern yourself.</li></ul><h4>The Four Walls You Will Hit</h4><p>Before we talk solutions, let’s put our finger on the problems. If you’ve ever tried to serve a large dataset with Apps Script, you already know the proble. Maybe you’ve tried to orchestrate multiple executions through google.script.run; and maybe that as enough of a work around, and maybe not (spoiler alert: not in this project). Here&#39;s what I&#39;ve had to deal with.</p><p><strong>1. The 6-minute execution budget.</strong> Every Apps Script execution has a hard wall clock limit — six minutes for consumer accounts, thirty for Workspace plans. Parsing millions of cells from hundreds of spreadsheets within a single server function? Not happening. I explored the classic workaround — splitting work across multiple executions with triggers and PropertiesService — way <a href="https://blog.wurkspaces.dev/bypassing-the-maximum-script-runtime-in-google-apps-script-e510aa9ae6da">back in 2021</a>. The pipeline we’re building here takes a more radical approach: move the heavy lifting out of Apps Script entirely.</p><p><strong>2. </strong><strong>google.script.run only speaks ECMAScript primitives.</strong> The bridge between your browser client and the Apps Script V8 runtime — that magical google.script.run.withSuccessHandler() we all rely on — can only pass strings, arrays, objects, numbers, and booleans. Native Apps Script objects like Blobs, Drive Files, or Calendars? They die on the bridge. This means your return values must survive JSON serialization through the IFRAME sandbox&#39;s postMessage channel. Binary data? Forget it.</p><p><strong>3. There’s an undocumented but very real payload ceiling.</strong> Google doesn’t publish a precise byte limit for google.script.run return values, but anyone who&#39;s pushed the envelope knows the score: payloads above tens of megabytes trigger &quot;Argument too large&quot; exceptions, or worse — they silently return undefined. Community testing and production scars put the practical ceiling somewhere in the 20–30 MB range. A separate 50 MB blob upload limit through HtmlService is documented, but that&#39;s a different mechanism entirely.</p><p><strong>4. </strong><strong>CacheService caps at 100 KB per key-value pair.</strong> You might think, &quot;Fine, I&#39;ll cache the parsed data server-side.&quot; Nope. CacheService is designed for configuration snippets and short-lived tokens, not megabyte-scale datasets. It cannot serve as your datastore.</p><p>These four constraints are <strong>non-negotiable</strong>. They are the laws of physics in the Apps Script universe. The pipeline we’re about to walk through treats them not as obstacles to overcome but as boundary conditions to route around.</p><h4>The Two-Halves Architecture</h4><p>The core insight is surprisingly simple once you state it: <strong>move all the work that doesn’t fit inside Apps Script’s quotas to the two places that have none — the developer’s machine and the user’s browser.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r-lVwxiTPJvFIIkmxyZ5ew.png" /></figure><h4>Half One: The Local Preprocessing Pipeline (Node.js, Unbounded)</h4><p>This runs on your machine or a CI runner, well outside Google’s execution sandbox. It:</p><ul><li>Reads the master aggregated JSON snapshot — hundreds of megabytes — straight from disk.</li><li>Compresses it with raw DEFLATE at maximum compression level using <strong>fflate</strong>, a pure-JavaScript library weighing roughly 8 kB that runs identically in Node.js and the browser.</li><li>Encodes the binary stream to Base64 so it can survive the google.script.run string channel.</li><li>Slices that Base64 string into fixed-size chunks (13 MB each, chosen through empirical testing — more on that below).</li><li>Uploads each chunk as a plain text file to a designated Google Drive folder via the gws CLI.</li><li>Writes a manifest file mapping chunk indices to their Drive file IDs.</li></ul><h4>Half Two: The Google Apps Script Runtime (V8 Engine, Heavily Bounded)</h4><p>Deployed as a Sheets-bound or standalone project serving an HtmlService web application. The server code is almost absurdly thin:</p><ul><li>The manifest is imported at build time as a static JSON asset bundled into the IIFE.</li><li>One function returns the manifest. One function per chunk reads a Drive file by ID and returns its text content as a plain string.</li><li><strong>It does not parse, decompress, aggregate, or transform the data in any way.</strong> It is a pure Drive-to-string proxy.</li></ul><h4>The Client Browser (Alpine.js SPA)</h4><ul><li>Fetches the manifest, then downloads all chunks in parallel through runGas calls.</li><li>Concatenates chunks in memory, decodes Base64, decompresses with fflate (the same library used to compress), parses the JSON, and indexes the resulting dataset with <strong>non-reactive closure caching</strong> — ensuring searches stay instantaneous even across millions of cells.</li></ul><p>That’s the architecture. Now let’s look at the toolchain that builds it.</p><h4>How the Code Gets Built and Deployed</h4><p>None of this toolchain was wired together by hand. It all comes from the <a href="https://github.com/WildH0g/apps-script-engine-template"><strong>Apps Script Engine Template</strong></a> — a reusable project starter I built and open-sourced. One command — npx apps-script-engine my-project — and you get a fully scaffolded project with Vite, clasp, environment management, and a build pipeline that produces exactly what Apps Script needs: a single-file client bundle for HtmlService, an IIFE bundle for the server (with npm dependencies like fflate baked in), and thin wrapper functions so the GAS runtime can call your exported functions.</p><p>I <a href="https://blog.wurkspaces.dev/kickstart-your-apps-script-projects-with-the-pinnacle-of-my-development-the-apps-script-84b1f764f297">first open-sourced this template back in August 2024</a>, and it has grown — through performance optimizations that yielded <a href="https://blog.wurkspaces.dev/make-apps-script-fast-again-with-apps-script-engine-76d1be291565">10× speed improvements</a> and, as of <a href="https://blog.wurkspaces.dev/apps-script-engine-v2-0-is-here-your-workflow-just-got-a-typescript-power-up-fe359bfd2b28">v2.0, full TypeScript support</a> — into the complete toolkit you’re looking at now.</p><p>The key result for this pipeline: you get to npm install fflate and import { decompressSync } from &#39;fflate&#39; in your Apps Script HTML code, and it works in production. The template handles the bundling, the IIFE wrapping, and the export hoisting automatically. Deployment is a single command:</p><pre>npm run build:addon:push<br>  → npm run build:ui     (Vite single-file: Alpine.js + Tailwind → index.html)<br>  → npm run build:gas    (Vite IIFE: server.js + fflate + manifest → server.iife.js + exports.js)<br>  → npm run push         (clasp push -f)</pre><p>That’s it. The template is what makes this architecture <strong>reproducible</strong> — you scaffold, you write your server functions, and you build. The internals of AST traversal and IIFE wrapping are handled for you.</p><h4>The Data Movement Pipeline: Step by Step</h4><p>This is where the magic really happens. Let’s walk through each stage with actual code.</p><h4>Step 1 — Extraction from Google Sheets (External, Upstream)</h4><p>The master dataset originates from an external extraction process that walks a folder hierarchy of Google Sheets, reads their data ranges programmatically via the Google Sheets API, and aggregates everything into a single JSON snapshot.</p><p><strong>This extraction is deliberately kept outside the web application and outside the build pipeline.</strong> It’s an I/O- and CPU-intensive task — opening hundreds of spreadsheets, reading millions of cells, normalizing structure — that would never fit inside the 6-minute budget. By hoisting it into an unbounded execution environment (a Node.js script on a developer machine, or a scheduled Cloud Function), the web application never touches a single source spreadsheet cell at runtime. It only ever reads the pre-compiled, pre-compressed binary artifact.</p><p>The output is a JSON file committed to version control as the authoritative snapshot.</p><h4>Step 2 — Compression</h4><pre>const rawData = fs.readFileSync(&quot;outputs/master_data_combined.json&quot;, &quot;utf8&quot;);<br>const u8 = fflate.strToU8(rawData);<br>const compressed = fflate.deflateSync(u8, { level: 9 });</pre><p><strong>fflate</strong> is the unsung hero here. It’s a pure-JavaScript compression library — roughly 8 kB, zero native dependencies, identical behavior in Node.js and in the browser. This symmetry is critical: the same library compresses during the offline build and decompresses in the user’s browser at runtime. No format compatibility issues, no environment-specific APIs like Node’s zlib or the browser&#39;s CompressionStream.</p><p>We use raw DEFLATE at compression level 9 (maximum). The “raw” part means we omit the gzip header and CRC trailer, saving a handful of bytes. fflate.decompressSync on the client autodetects the format, so this choice is transparent.</p><p>Real measured compression from a 341 MB JSON snapshot: the compressed binary stream comes out to roughly <strong>85 MB</strong> — a compression ratio of about 4×. Clinical and structured data with high entropy compresses less aggressively than repetitive log data, but 4× is enough to make chunking feasible.</p><h4>Step 3 — Base64 Encoding</h4><pre>const b64 = Buffer.from(compressed).toString(&quot;base64&quot;);</pre><p>This is the tax we pay for staying purely within Apps Script. The compressed binary stream must cross the google.script.run bridge, which serializes everything as JSON through the IFRAME sandbox. Binary types like Uint8Array or ArrayBuffer are either rejected or silently corrupted. So we encode to Base64 — converting arbitrary binary into a plain ASCII string safe for JSON transport.</p><p>The cost is a fixed <strong>33% expansion</strong>: 85 MB of compressed binary becomes roughly 113 MB of Base64 text. We accept this because introducing a Cloud Run sidecar or a CDN-hosted file would defeat the purpose of keeping the entire runtime surface within a Sheets sidebar. The architecture pays the Base64 tax to avoid paying the infrastructure tax.</p><h4>Step 4 — Chunking</h4><pre>const CHUNK_SIZE = 13 * 1024 * 1024; // 13 megabytes per chunk<br>const chunksCount = Math.ceil(b64.length / CHUNK_SIZE);</pre><p>The 113 MB Base64 string is sliced into consecutive 13 MB substrings. For our dataset, that’s 9 chunks: 8 full-size at ~13.6 MB each and one tail chunk at ~9.7 MB.</p><p>The <strong>13 MB threshold</strong> wasn’t pulled from thin air. It satisfies three constraints simultaneously:</p><ul><li><strong>Stays well under the practical </strong><strong>google.script.run payload ceiling.</strong> Production experience shows payloads above 20–30 MB frequently trigger serialization failures. Thirteen megabytes leaves comfortable headroom.</li><li><strong>Keeps each server function sub-second.</strong> DriveApp.getFileById().getBlob().getDataAsString() on a 13 MB text file completes in well under one second. All nine parallel calls easily stay within the 6-minute limit.</li><li><strong>Enables full concurrency.</strong> Nine independent chunks fetched via Promise.all means wall-clock download time is bounded by the slowest single chunk, not the sum of all nine.</li></ul><p>Each chunk is written to disk in two formats: .js files (ES modules for Vite dev server mock mode) and .txt files (the actual upload payloads for Google Drive).</p><p>A manifest file ties it all together:</p><pre>{<br>  &quot;root_folder&quot;: &quot;&lt;drive_folder_id&gt;&quot;,<br>  &quot;extracted_at&quot;: &quot;2026-07-09T09:02:22.435Z&quot;,<br>  &quot;chunks&quot;: [<br>    {<br>      &quot;index&quot;: 0,<br>      &quot;drive_id&quot;: &quot;1Qlb...&quot;,<br>      &quot;local_path&quot;: &quot;outputs/chunks/chunk_0.js&quot;<br>    },<br>    {<br>      &quot;index&quot;: 1,<br>      &quot;drive_id&quot;: &quot;1FGA...&quot;,<br>      &quot;local_path&quot;: &quot;outputs/chunks/chunk_1.js&quot;<br>    }<br>  ]<br>}</pre><h4>Step 5 — Upload to Google Drive via gws CLI</h4><pre>gws drive +upload outputs/chunks/chunk_0.txt \<br>  --parent 1wUx-2z4AilM-uEL3i-ZNvStJzhf9Cc7O \<br>  --name chunk_0.txt</pre><p><strong>gws</strong> (Google Workspace CLI, v0.22.5) is an open-source command-line tool from <a href="https://github.com/googleworkspace/cli">github.com/googleworkspace/cli</a>. It wraps the Google Workspace REST APIs behind a clean, consistent interface: gws &lt;service&gt; &lt;resource&gt; &lt;method&gt; [flags]. The +upload convenience helper auto-detects MIME type, constructs a multipart upload to files.create, sets the parent folder and filename, and returns the created file&#39;s metadata as JSON — including the Drive file ID.</p><p>Since our pipeline script runs in Node.js, we invoke gws via child_process.spawnSync, parse the JSON response, and write the id field back into the manifest:</p><pre>function runGws(args) {<br>  const result = spawnSync(&quot;gws&quot;, args, { encoding: &quot;utf8&quot; });<br>  if (result.status !== 0) {<br>    console.error(`Error running gws ${args.join(&quot; &quot;)}:`, result.stderr);<br>    return null;<br>  }<br>  return JSON.parse(result.stdout);<br>}</pre><p>For local development, npm run db:deploy:local runs the entire compress → encode → slice → write flow but skips the upload. Chunks are marked with drive_id: &quot;local_only&quot; in the manifest, and the Vite dev server&#39;s custom middleware plugin serves the local .txt files over HTTP. The client-side code path — fetch chunk text → concat → atob → decompressSync → JSON.parse — is <strong>identical</strong> in local development and production. Only the data source changes.</p><h4>Step 6 — Recovery from Drive: The Thin Server</h4><p>Here’s the entire server-side data path — two functions:</p><pre>import manifest from &quot;../../../outputs/dlv_chunks_manifest.json&quot;;<br><br>export function getDlvChunksManifest() {<br>  return manifest;<br>}<br><br>export function getCompressedDlvData(chunkIndex) {<br>  const chunk = manifest.chunks.find((c) =&gt; c.index === chunkIndex);<br>  if (!chunk || !chunk.drive_id) {<br>    throw new Error(`Chunk ${chunkIndex} not found.`);<br>  }<br>  if (chunk.drive_id === &quot;local_only&quot;) {<br>    throw new Error(<br>      `Chunk ${chunkIndex} has local_only placeholder. Deploy data first.`,<br>    );<br>  }<br>  const file = DriveApp.getFileById(chunk.drive_id);<br>  return file.getBlob().getDataAsString();<br>}</pre><p>Let me emphasize what’s happening here — and what’s <strong>not</strong> happening. The manifest is imported at build time and bundled into the IIFE. No runtime API call needed to discover what chunks exist. DriveApp.getFileById().getBlob().getDataAsString() reads the entire file and returns a plain string — the one type google.script.run can legally return. For a 13 MB text file, this call completes in well under one second. The function is idempotent and stateless: read file, return string. No parsing, no transformation, no caching, no mutation. It is trivially auditable and virtually immune to exceeding the 6-minute budget.</p><h4>Step 7 — Stitching and Decompression in the Browser</h4><p>The client orchestrates reconstruction in six stages:</p><pre>// 1. Fetch the manifest (one cheap call)<br>const manifest = await runGas(&quot;getDlvChunksManifest&quot;);<br><br>// 2. Fetch all chunks in parallel<br>const chunkPromises = manifest.chunks.map((c) =&gt;<br>  runGas(&quot;getCompressedDlvData&quot;, [c.index]),<br>);<br>const chunkResults = await Promise.all(chunkPromises);<br><br>// 3. Stitch - string concatenation restores the unified Base64 stream<br>const compressed = chunkResults.join(&quot;&quot;);<br><br>// 4. Decompress<br>const jsonStr = decompressBase64(compressed);<br><br>// 5. Parse into JavaScript objects<br>const rawData = JSON.parse(jsonStr).data;<br><br>// 6. Flatten the hierarchical structure into a flat array</pre><p>The decompressBase64() function is a four-stage synchronous pipeline:</p><pre>function decompressBase64(b64) {<br>  // Stage 1: Base64 decode via the browser&#39;s built-in atob()<br>  const binString = atob(b64);<br><br>  // Stage 2: Manually copy char codes into a Uint8Array<br>  const bytes = new Uint8Array(binString.length);<br>  for (let i = 0; i &lt; binString.length; i++) {<br>    bytes[i] = binString.charCodeAt(i);<br>  }<br><br>  // Stage 3: DEFLATE decompress via fflate (the same library used to compress)<br>  const decompressed = fflate.decompressSync(bytes);<br><br>  // Stage 4: UTF-8 decode back to a JavaScript string<br>  return fflate.strFromU8(decompressed);<br>}</pre><p>A few behavioral notes worth highlighting:</p><ul><li><strong>Parallelism is real.</strong> Promise.all dispatches all chunk fetches concurrently. Each google.script.run call is a separate IFRAME → server → Drive round-trip, and they overlap rather than serialize. Wall-clock download time is bounded by the slowest chunk, not the sum.</li><li><strong>Stitching is a simple </strong><strong>.join(&#39;&#39;).</strong> Because Base64 was sliced at arbitrary character boundaries, per-chunk decoding isn&#39;t possible — the entire stream must be reassembled first. But .join(&#39;&#39;) is O(N) and near-instant even for ~113 MB strings in modern engines.</li><li><strong>The </strong><strong>charCodeAt loop</strong> (Stage 2) is the CPU bottleneck. Converting an ~85 MB binary string to a Uint8Array requires iterating every byte. It takes several hundred milliseconds.</li><li><strong>decompressSync blocks the main thread.</strong> For an 85 MB compressed payload decompressing to ~341 MB, expect a 1–3 second UI freeze. We&#39;ll discuss fixes below.</li></ul><h4>Step 8 — Non-Reactive Closure Caching</h4><p>Once decompressed, the dataset lives as a read-only JavaScript array. Alpine.js’s reactive proxy is powerful, but re-rendering DOM on every state change across millions of cells would be catastrophic. The solution: a <strong>non-reactive closure cache</strong> with a cheap selection signature:</p><pre>const cache = {<br>  maps: null,<br>  headerIndex: new Map(),<br>  mergedKey: null,<br>  mergedRows: null,<br>  displayKey: null,<br>  displayRows: null,<br>};</pre><p>If the signature (composite string of active IDs, count, and search query) matches the cache key, the pre-computed array reference is returned instantly. Only on a cache miss does the application run the indexing and mapping algorithms, which use pre-compiled header index maps for O(1) lookups instead of O(N) scans over millions of cells.</p><p>This is the final link: the pipeline got the data into the browser; the cache keeps it fast once there.</p><h4>The gws CLI: A Closer Look</h4><p>I want to spend a moment on gws because it&#39;s one of those tools that deserves more attention than it gets. The Google Workspace CLI is open-source, community-maintained, and explicitly labeled &quot;not an officially supported Google product&quot; — but for our purposes, it&#39;s exactly what we need.</p><p>Its command structure is beautifully uniform:</p><pre>gws &lt;service&gt; &lt;resource&gt; [sub-resource] &lt;method&gt; [flags]</pre><p>So gws drive files list, gws sheets spreadsheets get, gws gmail users messages list — all follow the same pattern. Authentication supports multiple strategies: a pre-obtained OAuth token via environment variable, a credentials JSON file, client ID/secret, or interactive browser-based login via gws auth login.</p><p>The +upload helper is the killer feature for our pipeline. One command replaces multiple lines of API construction that would otherwise be needed. The trade-off — invoking gws as a child process via spawnSync — adds negligible overhead for a script that runs infrequently.</p><h4>Real Measured Numbers</h4><p>Theory is nice. Here are the actual numbers from a production 341 MB master JSON snapshot:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ee13ae6e80bacdcd84c6eeaba5e58623/href">https://medium.com/media/ee13ae6e80bacdcd84c6eeaba5e58623/href</a></iframe><h4>What This Architecture Achieves</h4><p>Let’s count the wins explicitly, because there are several.</p><p><strong>It bypasses the 6-minute execution wall.</strong> No single server function performs heavy work. Each is a sub-second Drive read. All CPU-intensive work runs offline in Node.js or in the user’s browser — neither of which has a 6-minute budget.</p><p><strong>It bypasses </strong><strong>google.script.run type and size limits.</strong> Everything crossing the bridge is a plain ECMAScript string. The 13 MB chunk size stays comfortably below the undocumented serialization ceiling. Base64 encoding ensures binary data survives JSON serialization intact.</p><p><strong>It bypasses the 100 KB </strong><strong>CacheService ceiling.</strong> The dataset lives in Google Drive files, not in CacheService. Drive imposes no meaningful per-file size limit for text files, and it acts as a free, durable, permission-scoped content delivery mechanism. No separate hosting. No database server. No CDN.</p><p><strong>It bypasses Google Sheets throughput limits at runtime.</strong> The source data is never read from spreadsheets at runtime. The only Sheets interaction is reading a small configuration sheet (typically under 10 KB).</p><p><strong>It remains a genuinely low-code application.</strong> There is no Cloud Run instance, no database server, no API gateway, no custom hosting. The entire runtime surface is a Google Sheets sidebar served by HtmlService. A non-technical user launches it from a spreadsheet menu. The &quot;infrastructure&quot; is a single Drive folder.</p><p>Deployment is two commands:</p><ul><li>npm run db:deploy — compress, chunk, upload data to Drive</li><li>npm run build:addon:push — compile UI + server, push to Apps Script via clasp</li></ul><h4>The Trade-offs (Every Architecture Has Them)</h4><p>I wouldn’t be doing my job if I didn’t tell you where this hurts.</p><h4>Base64 Expansion Tax (+33%)</h4><p>Every byte crossing the bridge is 33% larger than it needs to be. The alternative — transmitting raw binary — simply isn’t available through the Apps Script IFRAME bridge. You could eliminate this tax by introducing a Cloud Run sidecar or a CDN-hosted file, but that defeats the “zero-infrastructure” design goal.</p><h4>All-or-Nothing Reassembly</h4><p>Because Base64 was sliced at arbitrary character boundaries, the entire stream must be present before atob() and decompressSync() can run. Promise.all fails fast — if any single chunk download fails, the whole load fails. There&#39;s no partial recovery and no progressive rendering.</p><h4>Synchronous Decompression Freezes the Browser</h4><p>fflate.decompressSync() blocks the main thread for 1–3 seconds. The application yields before CSV export (30 ms setTimeout to paint a spinner) but does not currently yield during decompression. It&#39;s janky. It&#39;s fixable. But it&#39;s there.</p><h4>Deploy is Atomic and Manual</h4><p>npm run db:deploy clears all previous chunks, recompresses, and re-uploads from scratch. A network interruption mid-upload leaves orphaned files and an incomplete manifest. The fix is to re-run — there&#39;s no transactional upload, no resume, no rollback.</p><h4>Manifest Baked at Build Time</h4><p>The manifest is statically imported into the server bundle. When new data is deployed, the Apps Script project must be rebuilt and pushed too. Data updates and code deployments are coupled. This is a deliberate simplicity trade-off, but it’s a trade-off.</p><h4>No Incremental Updates</h4><p>Adding one row changes all chunks because the entire dataset is recompressed from scratch and the Base64 stream is sliced anew. Simple and safe, but wasteful if only a fraction of the data changed.</p><h4>Memory Footprint</h4><p>The full decompressed dataset (~341 MB as JavaScript objects) lives entirely in browser memory. Modern desktop browsers handle this fine. Mobile or older hardware? Not so much. The non-reactive cache prevents re-allocation storms, but the baseline footprint can’t be reduced without switching to a lazy-loading or indexed query-backend model.</p><h4>What I’d Improve Next</h4><p>Every project has a v2 wishlist. Here’s mine.</p><h4>1. Decouple the Manifest from the Source Code</h4><p>Store the manifest as a standalone file on Drive and have the server read it at runtime:</p><pre>export function getDlvChunksManifest() {<br>  const manifestFile = DriveApp.getFileById(MANIFEST_FILE_ID);<br>  const jsonText = manifestFile.getBlob().getDataAsString();<br>  return JSON.parse(jsonText);<br>}</pre><p>This decouples data deployment from code deployment entirely. New chunks can be uploaded without touching the Apps Script project. The cost is one extra DriveApp call on startup — sub-100 ms for a ~1 KB file.</p><h4>2. Offload Deployment to a Cloud Function + Cloud Scheduler</h4><p>Package the compression and upload logic into a Google Cloud Function triggered by Cloud Scheduler. A service account with Drive write access handles everything on Google’s infrastructure — no developer machine involved. Combined with a decoupled manifest, data updates become fully automated: new data appears → Cloud Function compresses and uploads → runtime manifest on Drive is updated → browser clients pick up new chunks on next page load.</p><p>Cloud Functions have a 9-minute timeout (15 minutes for gen2), comfortably fitting the full cycle. The free tier includes 2 million invocations per month.</p><h4>3. Asynchronous Decompression via Web Worker</h4><p>Move fflate decompression into a Web Worker to keep the main thread responsive:</p><pre>const worker = new Worker(&quot;/decompress.worker.js&quot;);<br>worker.postMessage(compressedBytes);<br>worker.onmessage = (e) =&gt; {<br>  const jsonStr = e.data;<br>  // continue with parsing and indexing<br>};</pre><p>This eliminates the 1–3 second UI freeze during decompression. The trade-off is additional complexity in worker bundling — fflate must be available in the worker scope.</p><h4>4. CI/CD Pipeline Integration</h4><p>Integrate into GitHub Actions: on push to main with changes to the data snapshot → trigger the Cloud Function. On push with changes to gas/src/ → run build:addon:push. GitHub Environments manage dev/UAT/production clasp configurations, with credentials in CI secrets.</p><h4>5. Incremental Updates via Chunk-Aware Architecture</h4><p>Instead of slicing the Base64 stream at byte boundaries, chunk at the logical level — groups of spreadsheets, date ranges, or alphabetical shards. Each logical chunk is independently compressed and uploaded with a version identifier. The client compares local version tags and only downloads changed chunks. This enables partial updates at the cost of less uniform chunk sizes and more complex version management.t</p><h4>The Bigger Lesson</h4><p>This architecture exists because of constraints. But here’s the thing I want you to take away: <strong>those constraints didn’t limit what we built — they defined it.</strong> The four walls of Apps Script — 6-minute execution, string-only return types, a practical payload ceiling, and a tiny cache — forced an architecture that is, in retrospect, better than the “just spin up a backend” alternative. It’s simpler to deploy. It has fewer moving parts. It costs nothing to run. This isn’t the first time I’ve pushed Google Apps Script far past its intended boundaries — I once built an <a href="https://blog.wurkspaces.dev/how-apps-script-became-the-ultimate-llm-fine-tuning-tool-a8e91de9f2f5">entire LLM fine-tuning pipeline</a> using Apps Script as the orchestration layer for Vertex AI. The constraints didn’t stop that project either. They shaped it. And the pattern generalizes beyond GAS: if you ever need to serve large datasets through an API gateway with tight quotas, the compress-chunk-reassemble pipeline is now in your toolkit.</p><p>The toolchain — Vite’s IIFE bundling, the AST extraction plugin, clasp, and gws — bridges the gap between modern JavaScript development and the constrained Apps Script runtime. You get npm dependencies, environment switching, and automated deployment without sacrificing the low-code, Sheets-sidebar deployment model that makes Apps Script so compelling in the first place.</p><p>That’s the whole story. I’d love to hear how you’re handling large datasets in your own Apps Script projects — or if you’ve hit the same walls and found different ways around them. Drop a comment below.</p><p><strong>Have you pushed the </strong><strong>google.script.run payload ceiling yourself? What broke first?</strong></p><h4>Related Articles</h4><p>If this deep-dive resonated with you, here are a few more from the toolbox:</p><ul><li><a href="https://blog.wurkspaces.dev/kickstart-your-apps-script-projects-with-the-pinnacle-of-my-development-the-apps-script-84b1f764f297"><strong>Kickstart Your Apps Script Projects with the Pinnacle of My Development — The Apps Script Engine Template</strong></a> — The original announcement of the template that makes this entire architecture reproducible. Covers the philosophy, the scaffold, and the “why” behind the build pipeline.</li><li><a href="https://blog.wurkspaces.dev/make-apps-script-fast-again-with-apps-script-engine-76d1be291565"><strong>Make Apps Script Fast Again! With Apps Script Engine</strong></a> — How the template enables 10× performance gains in Apps Script web apps by eliminating callback hell, bundling npm dependencies, and providing a proper local dev environment.</li><li><a href="https://blog.wurkspaces.dev/apps-script-engine-v2-0-is-here-your-workflow-just-got-a-typescript-power-up-fe359bfd2b28"><strong>Apps Script Engine v2.0 is Here! Your Workflow Just Got a TypeScript Power-Up</strong></a> — The migration from Jest to Vitest, the addition of full TypeScript support, and what v2.0 means for production-grade Apps Script development.</li><li><a href="https://blog.wurkspaces.dev/how-apps-script-became-the-ultimate-llm-fine-tuning-tool-a8e91de9f2f5"><strong>How Apps Script Became the Ultimate LLM Fine-Tuning Tool</strong></a> — Another case study in pushing Apps Script far beyond its intended limits: using Google Sheets as a human-in-the-loop interface for Vertex AI fine-tuning pipelines.</li><li><a href="https://blog.wurkspaces.dev/bypassing-the-maximum-script-runtime-in-google-apps-script-e510aa9ae6da"><strong>Beat the Clock! Dodging the Maximum Script Runtime in Google Apps Script</strong></a> — The classic approach to the 6-minute problem: chunking work across multiple script executions with triggers, timers, and PropertiesService. A useful contrast to the offline-compression strategy in this article.</li></ul><p>Happy building. 🚀</p><h4>About Me</h4><p><a href="https://cutt.ly/NrBo2qDL">Get my book</a> <strong>Kickstart Google Apps Script</strong>. I am an AI-first Google Cloud and Workspace developer and a Workspace Google Developer Expert (GDE), CTO and co-founder of <a href="https://mentormatic.com">Mentormatic</a>, and founder of <a href="https://wurkspaceds.dev">Wurkpaces.dev</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea364effc8b2" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/how-i-shipped-341-mb-of-json-through-google-apps-script-and-lived-to-tell-the-tale-ea364effc8b2">How I Shipped 341 MB of JSON Through Google Apps Script — and Lived to Tell the Tale</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[We Built Google Maps for Kubernetes.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://itnext.io/we-built-google-maps-for-kubernetes-caae4d80f8e8?source=rss----5b301f10ddcd---4"><img src="https://cdn-images-1.medium.com/max/2600/1*LZE1CaeMJGT38oPO94W06g.png" width="2974"></a></p><p class="medium-feed-snippet">The first time you look at a map of Europe, you don&#x2019;t need a legend to know Italy is the boot-shaped one.</p><p class="medium-feed-link"><a href="https://itnext.io/we-built-google-maps-for-kubernetes-caae4d80f8e8?source=rss----5b301f10ddcd---4">Continue reading on ITNEXT »</a></p></div>]]></description>
            <link>https://itnext.io/we-built-google-maps-for-kubernetes-caae4d80f8e8?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/caae4d80f8e8</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[devops-tool]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Guillermo Quiros]]></dc:creator>
            <pubDate>Wed, 15 Jul 2026 10:17:20 GMT</pubDate>
            <atom:updated>2026-07-15T10:17:19.411Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Where AI Fits in the SDLC — and Why “Assisting” and “Executing” Are Different Problems]]></title>
            <link>https://itnext.io/where-ai-fits-in-the-sdlc-and-why-assisting-and-executing-are-different-problems-594400bdeb94?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/594400bdeb94</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Hossein Yousefi]]></dc:creator>
            <pubDate>Wed, 15 Jul 2026 10:15:45 GMT</pubDate>
            <atom:updated>2026-07-15T10:15:43.734Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7uqeX3Djyu6E9iekwtnBdw.png" /></figure><p>Most engineering teams have already answered the first question about AI: does it help write and understand code? Yes. Copilot-style assistants are now a normal part of how developers work.</p><p>The harder question comes next, and fewer teams have answered it yet: what happens when you want AI involved not just in <em>writing</em> code, but in <em>running</em> the process around it — checking release scope, verifying gates, drafting a change request, summarizing an incident, deciding whether a sprint is actually ready to start?</p><p>That’s a different kind of problem. It’s not about model quality. It’s about operations: who’s allowed to trigger this, which systems can it touch, what needs a human sign-off, and can you reconstruct exactly what happened afterward. This is the layer NopsAI is built for, so it’s worth being specific about what that actually means in practice.</p><h3>AI touches every stage of the SDLC — the tooling around it doesn’t catch up evenly</h3><p>Across planning, development, testing, release, and operations, the pattern is similar: an LLM can summarize, draft, or flag something useful. Backlog grooming, PR review, test-gap analysis, release notes, incident write-ups — all reasonable jobs for a model.</p><p>The complication is that each of these stages has different systems of record, different people with access, and different consequences if something goes wrong. A model that can <em>reason</em> about release readiness isn’t the same as a system that can <em>safely execute</em> the checks, pause for approval, and log what it did — and most teams find that gap only after they’ve already wired an AI step into something that touches production.</p><h3>The existing tool categories, and where each one actually stops</h3><p>It’s easy to lump “AI + automation” tools together, but they solve different layers, and it’s worth being precise about that instead of pretending they overlap more than they do:</p><p><strong>AI coding assistants</strong> (Copilot and similar) operate at the repository level — code generation, explanation, PR-adjacent work. Their job is developer productivity, not cross-system orchestration, and they’re not trying to be anything else.</p><p><strong>CI/CD platforms</strong> (GitHub Actions, GitLab CI, Jenkins) are the right home for deterministic build/test/deploy work. They’re mature, they’re native at container and pod execution, and there’s no reason to replace them. What they’re generally <em>not</em> built for is per-step LLM profiles, MCP tool access, or caller-scoped authorization before a step runs — that’s just outside their design center.</p><p><strong>Workflow automation platforms</strong> (n8n, Zapier, Make) are good at connecting services quickly through triggers and nodes. They handle scripts-and-AI-steps-in-one-flow reasonably natively, but GitOps-owned configuration, Kubernetes runtime pools, and run-level evidence tend to be custom work bolted on, not native to the platform.</p><p><strong>Agent frameworks</strong> (LangGraph, LangChain, CrewAI) give engineers the building blocks — tools, memory, state, routing, human-in-the-loop patterns. They’re strong on agent roles and reusable steps, but identity/SSO, GitOps configuration, and caller-scoped AAA before dispatch generally aren’t what these frameworks are trying to solve; that’s left to whoever builds around them.</p><p>None of this is a knock on any of these tools — it’s just that “can an LLM be part of this workflow” and “can this workflow be governed, repeatable, and audited” are separate questions, and most of the current landscape answers the first one, not the second.</p><h3>The actual bottleneck</h3><p>Here’s where most teams get stuck, and it’s rarely the model. Turning “an LLM can do this” into “this is safe to run against our systems” surfaces a set of operational problems that coding assistants, CI/CD, workflow tools, and agent frameworks each solve a piece of — but usually not all of, and usually not consistently across a team.</p><p><strong>Problem: everyone has the same AI access.</strong> Once an agent is wired into Jira, GitHub, and ServiceNow, it’s tempting to give it broad access to all three and let it figure out what it needs per task. In practice, that means a summarization step and a “file a change request” step end up with identical tool access, so a bad prompt or a bad plan can reach further than the task actually required.</p><p>The fix isn’t “trust the model more” — it’s scoping access at the step level. A pipeline, step, or task gets an approved LLM profile and an approved MCP tool profile assigned to it specifically. This is one of the things NopsAI’s pipeline model does directly: the summarization step and the change-request step don’t inherit each other’s access just because they’re in the same run.</p><p><strong>Problem: no one owns “who’s allowed to trigger this.”</strong> Once a workflow can touch production — approve a release, close an incident, modify a runbook — “does this pipeline exist” isn’t the right question anymore. The right question is “is <em>this specific caller</em> allowed to run <em>this specific step</em> against <em>this specific system</em>, right now.” Most tools check the first and skip the second.</p><p>NopsAI checks authorization against the calling identity before a step dispatches, not just at the pipeline level — so access control follows the caller, not just the workflow definition.</p><p><strong>Problem: pipeline config lives in a UI, not in review.</strong> A workflow-automation canvas is fast to build in and easy to quietly change — often by whoever last had edit access, with no review trail. That’s fine for an internal automation nobody depends on; it’s a problem the moment the workflow touches a release or an incident.</p><p>NopsAI keeps pipeline definitions GitOps-owned, so a change to what the pipeline does goes through the same review path as any other code change.</p><p><strong>Problem: sensitive work runs wherever there’s spare capacity.</strong> Source code, credentials, infrastructure state, and production data increasingly pass through AI steps. Treating every one of those steps as “just another API call to a chat model” ignores that the execution environment itself matters — where it runs, what it can reach, what’s logged.</p><p>NopsAI dispatches steps to Docker or Kubernetes runners chosen deliberately per step, rather than treating every AI task as an undifferentiated external call.</p><p><strong>Problem: AI can quietly become the thing that ships the change.</strong> A model drafting a release note is low-risk. A model whose draft gets auto-applied to a production system with no one checking is a different thing entirely — and it’s an easy line to cross by accident once a workflow is “basically working.”</p><p>NopsAI treats approval as a step in the pipeline’s own DAG — a human gate the AI-generated output has to pass through before a production-affecting action happens, not a side process someone has to remember to run.</p><p><strong>Problem: the evidence ends up in five different places.</strong> The CI/CD result is in GitHub. The approval is in ServiceNow. The reasoning is in a chat log someone may or may not have saved. Reconstructing “what happened and why” after the fact means stitching together systems that were never designed to be stitched together.</p><p>NopsAI attaches trigger, resolved context, tool calls, model usage, approvals, and outputs to the run itself — one record, not a reconstruction project.</p><p>Taken together, this is what “governed AI execution” actually means in practice — not a slogan, but six specific gaps that show up the moment AI moves from answering questions to running operational work.</p><p>This is what NopsAI is, concretely — a self-hosted, Git-aware platform where those six things (scoped AI access, caller-checked authorization, GitOps-owned config, controlled runtime, in-pipeline approval, and run-owned evidence) aren’t separate add-ons, but the operating model the pipeline is built around from the start, across planning, releases, incidents, security, reporting, and operations. What that looks like in an actual workflow:</p><p><strong>Sprint planning.</strong> Today, a scrum master manually checks Jira backlog and velocity, reads Confluence for open design questions, and cross-checks Monday.com for dependencies — before the meeting even starts. With a scheduled NopsAI pipeline, that context gets pulled automatically, a fast LLM profile summarizes story readiness, and a planning brief is ready before anyone opens a tab. The failure mode this addresses isn’t “AI can’t summarize a backlog” — it’s that planning currently starts with manual research instead of decisions, and half-ready stories quietly enter the sprint because nobody caught it in time.</p><p><strong>Release preparation.</strong> Today, a release manager filters Jira by fix version, checks GitHub for merged PRs, checks CI/CD gate results, updates the Confluence runbook, and files a ServiceNow change for CAB approval — by hand, differently each time depending on who’s doing it. A governed pipeline runs the same checks every time: verify scope against merged PRs, validate CI/CD gates, pause before production, generate release notes and the change request together, and keep it all as one record. The value isn’t that an LLM can write release notes — it’s that scope mismatches, unmerged PRs, and failed gates get caught by the same checks every single release, instead of depending on whoever happens to be running it that week.</p><h3>Where this genuinely doesn’t matter much yet</h3><p>Worth saying plainly: if your team’s main use of AI is code generation inside a normal CI/CD pipeline, none of this is necessary. A coding assistant plus GitHub Actions is a complete, sensible setup for a lot of teams, and adding a governance layer on top of that is solving a problem you don’t have yet.</p><p>This starts to matter once a few things are true at the same time: a workflow crosses multiple systems (Jira, GitHub, ServiceNow, Confluence, whatever your stack is), different people should have different access to trigger or approve it, AI tool access needs to be scoped rather than blanket, some action in the workflow needs a human sign-off before it’s real, and someone — security, compliance, an auditor, your future self during an incident postmortem — needs to be able to reconstruct exactly what happened and why.</p><h3>How it relates to what you already have</h3><p>None of this requires ripping anything out. A Git event can trigger a NopsAI pipeline; that pipeline can check CI/CD status and report back to the repo. Copilot keeps doing code-level work; NopsAI governs the release-readiness process around the feature it produced. An n8n flow or a LangGraph agent can still be the thing that gets invoked — NopsAI is the layer that decides whether it’s allowed to run, what it can access, and what gets recorded when it does.</p><h3>Starting point</h3><p>The realistic way to evaluate this isn’t a generic demo — it’s picking one workflow that’s already repetitive, already crosses a few tools, and already causes friction around evidence or approval: sprint-readiness prep, release prep, incident follow-up, QA-blocker triage, or security-remediation coordination are common starting points. Define the trigger, the systems it touches, which steps are deterministic versus AI-assisted, what needs approval, and what evidence needs to be kept — and you have something concrete to test against, rather than a hypothetical.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=594400bdeb94" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/where-ai-fits-in-the-sdlc-and-why-assisting-and-executing-are-different-problems-594400bdeb94">Where AI Fits in the SDLC — and Why “Assisting” and “Executing” Are Different Problems</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Microsoft AI Builds Coding Models Optimized for GitHub Copilot]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://itnext.io/how-microsoft-ai-builds-coding-models-optimized-for-github-copilot-a4a9782c9278?source=rss----5b301f10ddcd---4"><img src="https://cdn-images-1.medium.com/max/1536/1*h6m44qJyfPvldahnoiTLlg.png" width="1536"></a></p><p class="medium-feed-snippet">How Microsoft AI built MAI-Code-1-Flash for GitHub Copilot: the sparse Mixture of Experts architecture, training pipeline, and benchmarks&#x2026;</p><p class="medium-feed-link"><a href="https://itnext.io/how-microsoft-ai-builds-coding-models-optimized-for-github-copilot-a4a9782c9278?source=rss----5b301f10ddcd---4">Continue reading on ITNEXT »</a></p></div>]]></description>
            <link>https://itnext.io/how-microsoft-ai-builds-coding-models-optimized-for-github-copilot-a4a9782c9278?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/a4a9782c9278</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[github-copilot]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Dave R - Microsoft Azure & AI MVP☁️]]></dc:creator>
            <pubDate>Wed, 15 Jul 2026 10:13:36 GMT</pubDate>
            <atom:updated>2026-07-15T10:13:34.620Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Vercel Reinvents React Native, Sub-Second OTA Updates, and an X Button Finally Doing Its Job]]></title>
            <link>https://itnext.io/vercel-reinvents-react-native-sub-second-ota-updates-and-an-x-button-finally-doing-its-job-acc2e4076605?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/acc2e4076605</guid>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[react-native]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <dc:creator><![CDATA[The React Native Rewind]]></dc:creator>
            <pubDate>Tue, 14 Jul 2026 22:52:15 GMT</pubDate>
            <atom:updated>2026-07-14T22:52:13.693Z</atom:updated>
            <content:encoded><![CDATA[<p>You’re reading The React Native Rewind <a href="https://reactnativerewind.com/issues/vercel-reinvents-react-native-sub-second-ota-updates-and-an-x-button-finally-doing-its-job">#49</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PdKN7EPLGbFCIp8aAdITVg.jpeg" /></figure><h3>Did Vercel Just Reinvent React Native?</h3><p>If we all released Software at the pace of Vercel we would be acquired, sunset, and open-sourced by Friday.</p><p>Anyway, they’ve shipped again.</p><p>It’s called <a href="https://github.com/vercel-labs/native">native</a> from Vercel Labs.</p><p>Not react “native”.</p><p>Not “native” base.</p><p>Just native.</p><p>Mononymous, like <a href="https://en.wikipedia.org/wiki/Cher">Cher</a>.</p><p>Native SDK is a toolkit for building native desktop apps.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*g3dlQ17F23NHVgS_VFmqvw.jpeg" /></figure><p>You write views in declarative markup and logic in TypeScript or <a href="https://ziglang.org/">Zig</a>, and the SDK’s own engine draws every pixel into real OS windows.</p><p>No browser, no WebView, and no JavaScript runtime anywhere in the binary.</p><p>Read that again, because you write TypeScript and it ships without a JavaScript engine.</p><p>Hold that thought.</p><p>First, here’s an entire working UI:</p><pre>&lt;column gap=&quot;12&quot; padding=&quot;16&quot;&gt;<br>  &lt;row gap=&quot;8&quot; cross=&quot;center&quot;&gt;<br>    &lt;text grow=&quot;1&quot;&gt;Counter&lt;/text&gt;<br>    &lt;button size=&quot;sm&quot; variant=&quot;ghost&quot; on-press=&quot;reset&quot;&gt;<br>      Reset<br>    &lt;/button&gt;<br>  &lt;/row&gt;<br>  &lt;row gap=&quot;8&quot; main=&quot;center&quot; cross=&quot;center&quot; grow=&quot;1&quot;&gt;<br>    &lt;button variant=&quot;secondary&quot; on-press=&quot;decrement&quot;&gt;-&lt;/button&gt;<br>    &lt;text&gt;{count}&lt;/text&gt;<br>    &lt;button variant=&quot;primary&quot; on-press=&quot;increment&quot;&gt;+&lt;/button&gt;<br>  &lt;/row&gt;<br>  &lt;status-bar&gt;total: {count}&lt;/status-bar&gt;<br>&lt;/column&gt;</pre><p>Here’s where it stops looking like React Native.</p><p>No useState. No useEffect. Nobody in a GitHub thread is asking why it fired twice.</p><p>The markup is a dumb template: it can read values {count} and shout messages on-press=&quot;increment&quot;, and that&#39;s all it&#39;s allowed to do.</p><p>Every message from every corner of the UI funnels into one function, update, which takes the current state and the message and returns the next state. The whole window re-renders from that.</p><p>If you’ve used useReducer or Redux, it&#39;s that, as the entire application.</p><pre>// src/core.ts, compiled to NATIVE CODE at build time<br>export type Msg =<br>  | { readonly kind: &quot;increment&quot; }<br>  | { readonly kind: &quot;decrement&quot; }<br>  | { readonly kind: &quot;reset&quot; };<br><br>export function update(model: Model, msg: Msg): Model {<br>  switch (msg.kind) {<br>    case &quot;increment&quot;:<br>      return { ...model, count: model.count + 1 };<br>    case &quot;decrement&quot;:<br>      return { ...model, count: model.count - 1 };<br>    case &quot;reset&quot;:<br>      return { ...model, count: 0 };<br>  }<br>}</pre><p>So where did the JavaScript engine go?</p><p>At build time, a TypeScript-to-native transpiler compiles that core straight into the executable.</p><p>Node is required to build, then vanishes from the binary like Keyser Söze.</p><p>It was never really there.</p><p>What’s left is a small example calculator that is 3.6 MB and opens in roughly 100 milliseconds, according to their own macOS measurements.</p><p>You could email it. The calculator. Through Gmail.</p><p>About Zig: it’s the low-level systems language the entire toolkit is built in, think C with better error messages. You never have to touch it, but if you want to, native init my_app --template zig-core scaffolds the identical counter with its core written in Zig.</p><p>It’s not a blank-slate toolkit either.</p><p>A catalogue of 45-plus built-in components such as buttons, tabs, dialogues, charts, tables, virtual lists, and a markdown renderer ships with considered typography and spacing, so the scaffolded app looks intentional the first time the window opens.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*Ej_hqhROI6j72Whe1TdnzA.jpeg" /></figure><p>Every app embeds an automation server.</p><p>The agent that wrote your app can launch it, read the live widget tree the way it would read a DOM, click the button it just invented, assert on the real state, and screenshot the window to confirm the app isn’t lying about any of it.</p><p>Then, buried in the Web Content docs, the funniest architecture decision of 2026.</p><p>Native SDK includes a <a href="https://native-sdk.dev/bridge">Bridge</a>. Yes… a Bridge.</p><p>JavaScript in the WebView calls native handlers by sending asynchronous JSON messages, capped at 16 KiB. JavaScript on one side. Native on the other. Serialised JSON in the middle.</p><p>This community spent half a decade escaping that exact architecture, and Vercel has rebuilt it from memory like a traumatised architect.</p><p>Let’s take full inventory, actually.</p><p>Declarative views? Check.</p><p>Logic driving a native renderer? Check.</p><p>An asynchronous serialised JSON bridge to JavaScript? Check.</p><p>iOS and Android targets? <a href="https://native-sdk.dev/platform-support#mobile">Experimental, but check</a>.</p><p>Congratulations to Vercel on inventing React Native 0.59, an architecture so historic that this community holds conferences about having survived it.</p><p>👉 <a href="https://native-sdk.dev/">Native SDK</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*XTiIDhBHf-FW8mzeItkA5g.jpeg" /></figure><h3>Let Your Agent Use the App It Just Built</h3><p>Your coding agent writes a login screen, tells you it’s done, and moves on.</p><p>It never actually opened the app.</p><p>The <a href="https://docs.maestro.dev/get-started/maestro-mcp?utm_source=rewind&amp;utm_campaign=newsletter">Maestro MCP</a> fixes that.</p><p>It hands your agent an iOS simulator, Android emulator, or physical Android device to drive.</p><p>Your agent reads the screen hierarchy, taps through the flow, grabs screenshots, and confirms the feature it just wrote actually works. Then it saves a repeatable end-to-end test, so the same flow doesn’t quietly break next week.</p><p>The Maestro Viewer embeds the live device right inside your coding agent, showing every command as it runs.</p><p>It all ships inside the Maestro CLI.</p><p>Point Claude Code, Cursor, Codex, Copilot, or Gemini at it. For Claude Code, it’s one line:</p><pre>mcp add maestro -- maestro mcp</pre><p>Your agent stops saying “this should work” and starts checking.</p><p>👉 <a href="https://docs.maestro.dev/get-started/maestro-mcp?utm_source=rewind&amp;utm_campaign=partnerbanner">Maestro MCP</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/1*cG9USuMPcDLo6KKXUOKb6A.jpeg" /></figure><h3>Your OTA Update Is Out for Delivery</h3><p><a href="https://www.zepto.com/">Zepto</a> (<a href="https://x.com/ZeptoNow?lang=en">@ZeptoNow</a>) is the Indian quick-commerce company famous for delivering groceries in ten minutes.</p><p>At some point, an engineering team in Bengaluru looked at OTA platforms re-shipping the entire JavaScript bundle for a one-line fix, and treated it like a delivery arriving in eleven minutes.</p><p>A production Hermes bundle runs about 20 MB, conservatively.</p><p>Push a one-character fix to a million installs, and your CDN moves 20 terabytes: roughly $1,700 of data at <a href="https://aws.amazon.com/es/cloudfront/">CloudFront</a> list price, spent shipping 19.999 MB of JavaScript to phones that already had it, over metered mobile data.</p><p><a href="https://www.youtube.com/watch?v=hBoAD6sYDYo">OTA at this scale earned its own talk at React Universe Conf 2025</a>.</p><p>So Zepto built <a href="https://github.com/zepto-labs/react-native-delta">Delta</a>.</p><p>Delta ships the diff instead.</p><p>Binary delta patching (<em>computing the byte-level difference between two files</em>) means the device downloads only the bytes that changed between the bundle it has and the bundle it needs.</p><p>The <a href="https://github.com/zepto-labs/delta-cli">delta-cli</a> computes the patch between releases and uploads it to your own S3 bucket, while <a href="https://github.com/zepto-labs/delta-server">delta-server</a> (<em>a self-hosted stack of Lambdas behind CloudFront</em>) decides which patch each device receives, keyed on the appId, jsVersion, and bundleVersion from a manifest.json baked into your app at build time.</p><p>On the device, the SDK checks for an update, downloads the patch, verifies it, applies it with <a href="https://github.com/mendsley/bsdiff">bspatch</a> (<em>the applying half of bsdiff, a binary diffing tool</em>) to reconstruct the full bundle, and swaps it in on the next launch.</p><p>The swap itself is one override:</p><pre>// MainApplication.kt<br>class MainApplication : Application(),<br>  ReactApplication, IDeltaDelegate {<br>    override val reactNativeHost: ReactNativeHost =<br>    object : DefaultReactNativeHost(this) {<br>      // Delta decides which bundle boots<br>      override fun getJSBundleFile(): String {<br>        return Delta.getJSBundlePath()<br>      }<br>    }<br>  override fun onCreate() {<br>    super.onCreate()<br>    Delta.initialize(this)<br>  }<br>}</pre><p>According to their own production numbers, Zepto saw 80% of users on the latest version within 24 hours, across 15 million update requests per day, with a P90 (<em>P90 is the value that 90% of all measurements fall at or below</em>) patch download time of 305 milliseconds.</p><p>305 milliseconds.</p><p>Quicker than your ex moving on to someone who actually has their shit together.</p><p>Somewhere in Bengaluru, a rider is doing 60 through traffic with a bag of onions, and he is now only the second-fastest thing at the company.</p><p>👉 <a href="https://github.com/zepto-labs/react-native-delta">react-native-delta</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*Av822IvbZqHkwXjgCauP5g.jpeg" /></figure><h3>React Native Finally Learns to Let Go</h3><p>The user starts uploading a 40 MB video on mobile data, changes their mind, and taps the X.</p><p>That X is your job.</p><p>And fetch has no cancel method.</p><p>No .stop(), no .nevermind().</p><p>That upload finishes in the background, burns through 40 MB of the user’s data plan, and your cancel button is just a placeholder icon.</p><p>An <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController">AbortController</a> is the only way to kill an in-flight fetch.</p><p>You pass its signal into the request, and the X calls abort(). The promise is rejected, the connection dies, and the upload stops transferring bytes.</p><p>Your app moves on with its life.</p><p>It’s the web platform’s institutionalised form of ghosting.</p><p>AbortController has worked in React Native for years, but it was never React Native&#39;s code.</p><p>Hermes doesn’t ship browser APIs, so at startup, React Native bolts the web globals on in a file called setUpXHR.js, and the AbortController it uses is an npm package <a href="https://www.npmjs.com/package/abort-controller">abort-controller</a> by Toru Nagashima <a href="https://x.com/mysticatea">(@mysticatea)</a>.</p><p>A package last published 7 years ago.</p><p>Which means it predates half the modern API.</p><p>AbortSignal.timeout(), AbortSignal.any(), the static AbortSignal.abort(reason), and signal.throwIfAborted() simply did not exist in React Native.</p><p><a href="https://x.com/tell_me_mur/status/2074067839011700858">Spotted doing the rounds on X</a> this week, because a core web primitive quietly frozen in 2019 is exactly the kind of thing this ecosystem finds under the sofa.</p><p>Now, <a href="https://github.com/react/react-native/commit/6f3375a140b10cffe9bed3dd72a017ece97bbbba">a commit from @retyui</a> has landed on main.</p><p>The unmaintained package (<em>and its </em><a href="https://www.npmjs.com/package/event-target-shim"><em>event-target-shim</em></a><em> sub-dependency</em>) are gone.</p><p>In its place, React Native forked the source in-tree, wired it up to React Native’s own EventTarget, and added all four missing methods. The Hermes team did the same thing to the promise package years ago, so there&#39;s precedent for adopting abandoned dependencies and raising them as your own.</p><p>The payoff is that the patterns you already use on the web now JustWork™:</p><pre>const controller = new AbortController();<br><br>fetch(url, {<br>  // Cancel manually OR after 5s, whichever first<br>  signal: AbortSignal.any([<br>    controller.signal,<br>    AbortSignal.timeout(5000),<br>  ]),<br>});<br>controller.abort(); // the X, doing its one job</pre><p>AbortSignal.timeout() gives every request a deadline without a hand-rolled setTimeout, and AbortSignal.any() merges signals so one fetch can be cancelled by whichever fires first.</p><p>It landed on main against 0.87, so it will be available in the next React Native release.</p><p>And somewhere out there, an X is about to do something for the first time in its life.</p><p>👉 <a href="https://github.com/react/react-native/commit/6f3375a140b10cffe9bed3dd72a017ece97bbbba">Abort Controller</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*wOUhuvbPL5M-9S5TxfhuVQ.gif" /></figure><p>Authored by <a href="https://www.linkedin.com/in/lukebrandonfarrell/">Luke Farrell</a> and edited by <a href="http://www.linkedin.com/in/francisco-rios-nino">Fran Ríos</a>.</p><p>If you’re enjoying this newsletter, why not help us grow? Share it with your friends, family, and even that one coworker who always “forgets” to update dependencies. Use this link: <a href="https://thereactnativerewind.com/">https://thereactnativerewind.com/</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=acc2e4076605" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/vercel-reinvents-react-native-sub-second-ota-updates-and-an-x-button-finally-doing-its-job-acc2e4076605">Vercel Reinvents React Native, Sub-Second OTA Updates, and an X Button Finally Doing Its Job</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stop Using Task.Run for Background Jobs in .NET — Use Hangfire Instead]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://itnext.io/stop-using-task-run-for-background-jobs-in-net-use-hangfire-instead-15dc1b8cc61f?source=rss----5b301f10ddcd---4"><img src="https://cdn-images-1.medium.com/max/1536/1*Z8WRX4ToHYDZjQ7HTLrtpw.png" width="1536"></a></p><p class="medium-feed-snippet">Build reliable background jobs, retries, schedules, and dashboards in ASP.NET Core without turning your app into a thread-management&#x2026;</p><p class="medium-feed-link"><a href="https://itnext.io/stop-using-task-run-for-background-jobs-in-net-use-hangfire-instead-15dc1b8cc61f?source=rss----5b301f10ddcd---4">Continue reading on ITNEXT »</a></p></div>]]></description>
            <link>https://itnext.io/stop-using-task-run-for-background-jobs-in-net-use-hangfire-instead-15dc1b8cc61f?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/15dc1b8cc61f</guid>
            <category><![CDATA[csharp]]></category>
            <category><![CDATA[clean-code]]></category>
            <category><![CDATA[dotnet]]></category>
            <category><![CDATA[aspnetcore]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Hossein Kohzadi]]></dc:creator>
            <pubDate>Tue, 14 Jul 2026 21:08:39 GMT</pubDate>
            <atom:updated>2026-07-14T21:08:37.507Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Your AI Voice Agent Is a Black Box. Here’s How to Open It.]]></title>
            <link>https://itnext.io/your-ai-voice-agent-is-a-black-box-heres-how-to-open-it-516e51fbab56?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/516e51fbab56</guid>
            <category><![CDATA[langchain]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[opentelemetry]]></category>
            <dc:creator><![CDATA[Dima Statz]]></dc:creator>
            <pubDate>Tue, 14 Jul 2026 21:06:19 GMT</pubDate>
            <atom:updated>2026-07-14T21:06:17.814Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>LangChain and LangSmith trace your agent when it types. The moment it speaks on a phone call, they see tokens, not audio. Here’s how to get structured signals out of a raw recording</blockquote><blockquote>⭐ <strong>Github Source</strong>: <a href="https://github.com/dimastatz/audiotrace">https://github.com/dimastatz/audiotrace</a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9hGKZL3JUknROf--3qbfAg.png" /></figure><p>When your AI agent types, you can see everything it does. LangChain traces every step, LangSmith replays every run, OpenTelemetry hangs spans off each call. You know what the model saw, what it said, how long it took, and what it cost. The moment that same agent picks up a phone, the lights go out. A voice agent’s entire interaction lives inside audio recordings. The transcript, the customer’s mood, the awkward four-second silence, the moment it talked over the caller, the point where the conversation went sideways — all of it is in there.</p><p>But to your existing observability stack, that file is opaque. LangSmith sees the tokens you fed the LLM; it does not see the audio that reached a human ear. So most teams do the only thing they can: they listen to a handful of calls by hand and hope the sample is representative. That doesn’t scale, and it misses the thing that makes voice agents hard — <strong>their behavior drifts.</strong> You tweak a prompt, swap a model, change a TTS voice, and the agent gets subtly slower, colder, or starts missing intents. No unit test catches it, because the regression lives in the audio.</p><p>This series is about closing that gap. In this first post I’ll lay out the mental model; the next two get hands-on with a tricky signal-extraction problem and with wiring voice signals into CI.</p><h4>The artifact is richer than you think</h4><p>Here’s what’s actually recoverable from a single call recording:</p><p>- <strong>**Transcript**</strong> — what was said, by whom, with timestamps.</p><p>- <strong>**Quality**</strong> — silence gaps, interruptions, speaking pace, pitch variance.</p><p>- <strong>**Sentiment**</strong> — the caller’s mood, and where it shifted.</p><p>- <strong>**Latency**</strong> — how long each stage (STT, LLM, TTS) took to respond.</p><p>- <strong>**Cost**</strong> — what the call cost, attributed per stage.</p><p>- <strong>**Events**</strong> — the detected intent, whether the caller dropped off, and compliance flags.</p><p>That’s a lot of signal locked inside one file. The reason teams rebuild this from scratch at every company is that prying it loose means bolting together speech recognition, speaker separation, audio analysis, a sentiment model, and a pricing sheet — and then maintaining all of it.</p><h4>Two ways to pull meaning out of audio</h4><p>The key insight that makes this tracable: there are really two different kinds of questions you can ask of audio, and they want two different tools.</p><p><strong>1. Measure it — classical signal processing.</strong> Deterministic math run straight on the waveform: energy, pitch, the length of a silence. Cheap, exact, no training data. It shines for physical questions:</p><p>- How long was the pause?</p><p>- How fast did someone speak?</p><p>- Is this voice high-pitched or low?</p><p>You <em>*measure*</em> the answer instead of guessing at it.</p><p><strong>2. Estimate it — learned models.</strong> Statistical systems like Whisper or a sentiment classifier that have ingested enormous amounts of data and <em>*estimate* </em>an answer. They own everything that turns on meaning rather than physics:</p><p>- What words were said?</p><p>- Who is speaking?</p><p>- Is the caller upset?</p><p>No hand-written rule survives real speech here — you need a model. Most of the craft is knowing which question belongs to which bucket: reach for a model to <strong>**estimate meaning**</strong>, for signal processing to <strong>**measure physics**</strong>. (In the next post you’ll see that when a model isn’t available, a measurement can sometimes stand in for it — that turns out to be a surprisingly useful trick.)</p><h4>One report, split along that line</h4><p>I packaged this into a small open-source library called [AudioTrace](https://github.com/dimastatz/audiotrace). You hand it a recording; it hands back one structured, typed report — split along exactly that measure-vs-estimate line. The acoustic layer (silence, pace, pitch) is signal processing; the semantic layer (transcript, sentiment, intent) is models.</p><blockquote>pip install audiotrace</blockquote><blockquote>import audiotrace</blockquote><blockquote>report = audiotrace.analyze(</blockquote><blockquote>audio=”call_recording.wav”,</blockquote><blockquote>metadata={“agent_version”: “v2.1”, “provider”: “vapi”},</blockquote><blockquote>)</blockquote><blockquote>print(report.quality.overall_score) # 0.87</blockquote><blockquote>print(report.quality.speaking_pace_wpm) # 168.0</blockquote><blockquote>print(report.sentiment.caller_frustration) # False</blockquote><blockquote>print(report.latency.total_ms) # 4200</blockquote><blockquote>print(report.events.drop_off) # False</blockquote><blockquote>print(report.cost.total_usd) # 0.063</blockquote><p>The return value is a Pydantic `CallReport`, so it’s typed, validated, and trivial to serialize. You can emit it as OpenTelemetry spans, hang it off your LangChain and LangSmith traces, or assert on it in a CI check — which is exactly where this series is headed.</p><h4>One decision shaped everything: it runs locally</h4><p>Call recordings are about as sensitive as data gets. So AudioTrace runs entirely on your machine — no audio leaves the box, and the open models download once. Privacy here shouldn’t be an upgrade you pay for; it should be the default.</p><h4>What’s next</h4><p>The two-layer model sounds tidy, but the interesting part is what happens when the “right” tool isn’t available. In the next post I’ll walk through a concrete example: labeling <strong>**who is speaking**</strong> without the gated model everyone reaches for — and why a few dozen lines of pitch measurement beat it for the common case.</p><blockquote>⭐ The repo is at https://github.com/dimastatz/audiotrace.</blockquote><blockquote>Issues and PRs welcome — it’s early, and provider integrations are exactly the kind of contribution that helps most.</blockquote><p>Keep building!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=516e51fbab56" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/your-ai-voice-agent-is-a-black-box-heres-how-to-open-it-516e51fbab56">Your AI Voice Agent Is a Black Box. Here’s How to Open It.</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Safer API Drift Healer: Deterministic Field Matching in V0.4]]></title>
            <link>https://itnext.io/building-a-safer-api-drift-healer-deterministic-field-matching-in-v0-4-4471a77fbc39?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/4471a77fbc39</guid>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[open-api]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[github]]></category>
            <category><![CDATA[api-testing]]></category>
            <dc:creator><![CDATA[Burak Karakoyunlu]]></dc:creator>
            <pubDate>Tue, 14 Jul 2026 21:04:37 GMT</pubDate>
            <atom:updated>2026-07-14T21:04:35.899Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uw77xMOcXWMAEACjG-YOkQ.png" /></figure><p><em>A local-first QA prototype that scores field mappings, rejects risky candidates, validates accepted patches locally, and opens a reviewable pull request only after PASS.</em></p><p>V0.3 of API Drift Healer successfully proved a concept: it could handle a narrow contract-drift case (like mapping userEmail to email_address), patch the outdated field, rerun the test, and open a pull request, but <em>only</em> if the healed test passed locally.</p><p>However, a critical question remained unresolved: <strong>How should the tool actually decide if a field mapping is safe enough to attempt in the first place?</strong> Mapping userEmail to email_address based on an OpenAPI update seems completely reasonable. But what happens when the tool encounters pairs like displayName to email_address, firstName to last_name, or userId to customer_id? Just because two fields share the same string type or contain loosely related words doesn&#39;t mean they represent the same underlying data.</p><p>This is exactly what V0.4 addresses. The tool now introduces a deterministic field matcher designed to rigorously evaluate a candidate rename long before it touches your test files. The original rule has not changed: <strong>No PASS, No PR.</strong> But V0.4 adds another requirement: <strong>No strong match, no patch.</strong></p><h3>What Changed in V0.4</h3><p>V0.3 proved the basic workflow:</p><ul><li>detect a failing test</li><li>patch one outdated field</li><li>rerun the healed test</li><li>generate a report</li><li>open a pull request only after PASS</li></ul><p>V0.4 adds a dedicated matching engine with:</p><ul><li>field-name normalization</li><li>rule-based field concepts</li><li>qualifier conflict detection</li><li>runtime value-type checks</li><li>OpenAPI type checks</li><li>runtime value-format detection</li><li>OpenAPI format checks</li><li>deterministic scoring</li><li>structured SAFE PATCH or REJECT decisions</li><li>48 automated tests</li></ul><p>The real improvement is not that the tool can accept more mappings.</p><p>It is that the tool can explain why it accepts one mapping and rejects another.</p><h3>Recreating the Contract Drift</h3><p>The demo still starts with a small and familiar mismatch.</p><p>The local API expects:</p><pre>{<br>  &quot;name&quot;: &quot;Test User&quot;,<br>  &quot;email_address&quot;: &quot;qa_user@example.com&quot;<br>}</pre><p>The OpenAPI contract defines email_address as required:</p><pre>required:<br>  - email_address</pre><p>It also defines its type and format:</p><pre>email_address:<br>  type: string<br>  format: email</pre><p>But the YAML test case still sends:</p><pre>body:<br>  name: Test User<br>  userEmail: qa_user@example.com</pre><p>The test runner sends the outdated payload and receives:</p><pre>[FAIL] Expected 201, got 400<br>Error: missing required field: email_address</pre><p>Despite the [FAIL] output, the API is actually running fine, the endpoint works, and the expected status code logic is still correct. The only real problem is that our request payload is simply outdated. Instead of a manual debugging session, this exact failure now becomes the perfect input for the V0.4 deterministic matching flow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8vJHafkYZyith_oevXXVLQ.png" /><figcaption>The original YAML test still sends userEmail, while the API requires email_address.</figcaption></figure><h3>The V0.4 Safe Healing Flow</h3><p>The local flow now looks like this:</p><pre>Original test FAIL<br>        ↓<br>OpenAPI drift detected<br>        ↓<br>Candidate field rename found<br>        ↓<br>Field names normalized<br>        ↓<br>Field concepts checked<br>        ↓<br>Qualifier conflicts checked<br>        ↓<br>Type and format checked<br>        ↓<br>Deterministic score calculated<br>        ↓<br>SAFE PATCH or REJECT<br>        ↓<br>Healed test generated only when accepted<br>        ↓<br>Healed test PASS<br>        ↓<br>Report generated<br>        ↓<br>Optional GitHub PR</pre><p>Changing one YAML key is easy.</p><p>The difficult part is deciding whether that change should be attempted at all.</p><p>V0.4 moves that decision into a separate module:</p><p>field_matcher.py</p><p>The matcher always returns a structured result.</p><p>Its final decision is one of two values:</p><pre>SAFE PATCH</pre><p>or:</p><pre>REJECT</pre><p>The same input produces the same result.</p><p>There is no LLM deciding which field should be renamed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3BNBYhURWO2BobWq4FTHZQ.png" /><figcaption>V0.4 evaluates the candidate mapping before generating a healed test.</figcaption></figure><h3>Normalizing Different Field Styles</h3><p>APIs and test files do not always use the same naming style.</p><p>The same field may appear as:</p><ul><li>userEmail</li><li>user_email</li><li>user-email</li><li>UserEmail</li><li>user email</li></ul><p>The matcher first converts field names into normalized tokens.</p><p>For example:</p><pre>normalize_field_name(&quot;userEmail&quot;)</pre><p>returns:</p><pre>[&quot;user&quot;, &quot;email&quot;]</pre><p>And:</p><pre>normalize_field_name(&quot;email_address&quot;)</pre><p>returns:</p><pre>[&quot;email&quot;, &quot;address&quot;]</pre><p>This allows the matcher to compare structured parts of a name instead of comparing only the raw strings.</p><p>The current normalizer handles:</p><ul><li>camelCase</li><li>snake_case</li><li>kebab-case</li><li>PascalCase</li><li>spaces</li><li>common acronym patterns</li></ul><p>Normalization is only the first signal.</p><p>Two fields can still share a token without representing the same value.</p><h3>Rule-Based Field Concepts</h3><p>The matcher uses a small and explicit concept dictionary.</p><p>Current concepts include:</p><ul><li>email</li><li>phone</li><li>name</li><li>id</li><li>date</li></ul><p>For example:</p><p>userEmail</p><p>and:</p><p>email_address</p><p>both contain the concept:</p><p>email</p><p>That supports the mapping.</p><p>But:</p><p>displayName</p><p>and:</p><p>email_address</p><p>do not share a concept.</p><p>Both values may be strings, but they represent different data.</p><p>This is rule-based concept matching.</p><p>It is not an embedding model and it does not use an LLM.</p><p>The concept list is intentionally small in V0.4. It can be extended later as more field patterns are tested.</p><h3>Qualifier Conflicts</h3><p>Fields may share the same main concept and still describe different values.</p><p>Consider:</p><p>firstName -&gt; last_name</p><p>Both fields contain the name concept.</p><p>A simple matcher may treat that as a good sign.</p><p>But their qualifiers conflict:</p><pre>first -&gt; last</pre><p>V0.4 treats this as a hard conflict and rejects the candidate.</p><p>The same guard can reject mappings such as:</p><ul><li>primaryEmail -&gt; secondary_email</li><li>createdAt -&gt; updated_at</li><li>userId -&gt; customer_id</li></ul><p>A high similarity score cannot override a hard qualifier conflict.</p><p>This rule matters because string similarity alone can make bad mappings look more convincing than they really are.</p><h3>OpenAPI Type and Format Checks</h3><p>Field names are not the only source of evidence.</p><p>The matcher also checks:</p><ul><li>the runtime value</li><li>the detected runtime type</li><li>the OpenAPI target type</li><li>the detected runtime format</li><li>the OpenAPI target format</li></ul><p>For the safe email example, the source value is:</p><pre>qa_user@example.com</pre><p>The runtime type is:</p><pre>string</pre><p>The target OpenAPI field also expects:</p><pre>type: string<br>format: email</pre><p>The runtime value looks like an email, and the target field expects an email.</p><p>Both signals support the candidate mapping.</p><p>Now consider:</p><pre>displayName = &quot;Test User&quot;</pre><p>The value is still a string.</p><p>But it does not look like an email.</p><p>If the target schema expects:</p><pre>type: string<br>format: email</pre><p>the matcher detects a format conflict.</p><p>This is why checking only the Python type is not enough.</p><p>V0.4 currently detects formats such as:</p><ul><li>email</li><li>UUID</li><li>date-time</li><li>phone</li><li>plain or unknown string</li></ul><p>The format checks use deterministic rules and heuristics.</p><p>They are safety signals, not semantic proof.</p><p>Runtime format detection also depends on the quality of the test data. Placeholder values or intentionally invalid fixtures may cause a reasonable mapping to be rejected.</p><p>For this prototype, rejecting an uncertain candidate is preferable to applying the wrong patch.</p><h3>Deterministic Scoring</h3><p>After collecting the matching signals, the matcher calculates a deterministic score.</p><p>The current acceptance threshold is:</p><pre>0.700</pre><p>The score considers signals such as:</p><ul><li>shared field concepts</li><li>type compatibility</li><li>format compatibility</li><li>normalized string similarity</li><li>qualifier conflicts</li><li>strong matching anchors</li></ul><p>However, the numerical score is not the only rule.</p><p>A candidate must also:</p><ul><li>have a strong matching anchor</li><li>avoid hard qualifier conflicts</li><li>avoid hard format conflicts</li></ul><p>A candidate can therefore be rejected even when some of its individual signals look positive.</p><p>The current threshold is a conservative prototype value.</p><p>It will need further calibration against a larger set of real field mappings and rejected examples.</p><p>The score should be treated as an explainable decision signal, not a universal probability of correctness.</p><h3>Example 1: Accepting an Email Rename</h3><p>Candidate:</p><pre>userEmail -&gt; email_address</pre><p>Decision:</p><pre>Score: 0.772<br>Threshold: 0.700<br>Confidence: High<br>Decision: SAFE PATCH</pre><p>Supporting evidence includes:</p><ul><li>shared concept: email</li><li>source value type matches the OpenAPI target type</li><li>detected email format matches the OpenAPI target format</li><li>no hard qualifier conflict</li><li>no hard format conflict</li></ul><p>The healer generates:</p><pre>body:<br>  name: Test User<br>  email_address: qa_user@example.com</pre><p>It then runs the healed test:</p><pre>[PASS] Expected 201, got 201</pre><p>The mapping passes the current matcher rules and the resulting test passes locally.</p><p>That makes it eligible for the report and optional PR flow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Lv3V6M7pup7Pxwt2eHgaNQ.png" /><figcaption>The email mapping passes the matcher with a score of 0.772, and the healed test returns 201.</figcaption></figure><h3>Example 2: Rejecting Name-to-Email</h3><p>Candidate:</p><pre>displayName -&gt; email_address</pre><p>Decision:</p><pre>Score: 0.158<br>Threshold: 0.700<br>Confidence: Low<br>Decision: REJECT</pre><p>Reasons include:</p><ul><li>no shared field concept</li><li>source value does not match the target email format</li><li>no strong matching anchor</li><li>score is below the threshold</li><li>hard format conflict detected</li></ul><p>The tool does not create a healed file.</p><p>It does not patch the test.</p><p>It does not create a branch or pull request.</p><p>This is an important part of V0.4.</p><p>The matcher is designed to reject uncertain candidates, not to force every failure into a generated fix.</p><h3>Example 3: Rejecting First Name to Last Name</h3><p>Candidate:</p><pre>firstName -&gt; last_name</pre><p>The fields share the name concept.</p><p>Their normalized strings are also somewhat related.</p><p>But the matcher detects:</p><pre>Qualifier conflict: first -&gt; last</pre><p>Final result:</p><pre>Score: 0.587<br>Decision: REJECT</pre><p>The shared concept is not enough to override the qualifier conflict.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XrvMrO3kQuv4hRe4xmJC9Q.png" /><figcaption>V0.4 rejects mappings with incompatible formats or conflicting qualifiers, even when the fields share a type or concept.</figcaption></figure><h3>Testing the Safety Rules</h3><p>A matching engine should not only prove that accepted cases work.</p><p>It should also prove that risky cases are rejected.</p><p>V0.4 currently includes:</p><pre>45 matcher unit tests<br>+<br>3 healer integration tests<br>=<br>48 automated tests</pre><p>The matcher tests cover:</p><ul><li>field-name normalization</li><li>field concepts</li><li>qualifier conflicts</li><li>runtime type detection</li><li>runtime format detection</li><li>OpenAPI type compatibility</li><li>OpenAPI format compatibility</li><li>deterministic scoring</li><li>accepted mappings</li><li>rejected mappings</li></ul><p>The integration tests verify the full healer behavior.</p><p>Safe case:</p><pre>userEmail -&gt; email_address<br>Result: healed file created</pre><p>Unsafe format case:</p><pre>displayName -&gt; email_address<br>Result: rejected, no healed file</pre><p>Unsafe qualifier case:</p><pre>firstName -&gt; last_name<br>Result: rejected, no healed file</pre><p>Current test result:</p><pre>Ran 48 tests<br><br>OK</pre><p>These rejection tests are as important as the successful case.</p><p>A matcher that only tests its happy path is mostly testing its optimism.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fhCxOzQnMXIk5NEeWAzoUg.png" /><figcaption>The V0.4 matcher and healer flow currently pass 48 automated tests, including unsafe-mapping rejection cases.</figcaption></figure><h3>Applying the Patch</h3><p>Once a candidate passes the matching checks, the actual patch is intentionally simple:</p><pre>request_body[new_field] = request_body.pop(old_field)</pre><p>For this demo, it changes:</p><pre>userEmail: qa_user@example.com</pre><p>into:</p><pre>email_address: qa_user@example.com</pre><p>The output is written to:</p><p>api_test_case.healed.yaml</p><p>This file is generated output.</p><p>It can be recreated from the original YAML test during each healer run.</p><p>The complexity belongs in the decision, not in the assignment.</p><h3>The Main Guardrail</h3><p>The tool continues only when all current safety conditions are satisfied:</p><pre>Original test fails<br>+<br>Exactly one required OpenAPI field is missing<br>+<br>Exactly one request field is invalid<br>+<br>Matcher returns SAFE PATCH<br>+<br>Healed test passes locally</pre><p>If the candidate is rejected, the process stops.</p><p>If the healed test still fails, the process stops.</p><p>If multiple required fields or invalid fields are found, the process stops.</p><p>No branch.</p><p>No PR.</p><p>No pretend-success.</p><p>Rejecting an uncertain mapping is cheaper than confidently patching the wrong field.</p><p>And the original rule still applies:</p><blockquote><strong><em>No PASS, No PR.</em></strong></blockquote><h3>Making the Decision Explainable</h3><p>A patch alone is not enough.</p><p>The reviewer should be able to understand:</p><ul><li>what failed</li><li>what changed</li><li>why the mapping was accepted</li><li>how strong the matching evidence was</li><li>whether the healed test passed</li></ul><p>V0.4 generates:</p><p>heal_report.md</p><p>The report includes:</p><ul><li>root cause</li><li>applied fix</li><li>original status</li><li>healed status</li><li>match score</li><li>safety threshold</li><li>confidence</li><li>final decision</li><li>matching evidence</li></ul><p>Example:</p><pre>Match score: 0.772<br>Safety threshold: 0.700<br>Confidence: High<br>Decision: SAFE PATCH</pre><p>The evidence section may include:</p><pre>Shared concept: email<br>Value type matches the OpenAPI target type<br>Email format matches the OpenAPI target format<br>Normalized string similarity: 0.435<br>No hard conflict found</pre><p>The report does not simply say that a patch was generated.</p><p>It shows why the candidate passed the current checks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AbDqIM4p2pg1Q4AwjUMvZQ.png" /><figcaption>The V0.4 report exposes the score, threshold, confidence, decision, and matching evidence.</figcaption></figure><h3>Local Report Mode and PR Mode</h3><p>By default, the tool runs locally:</p><pre>python auto_healer.py</pre><p>Local mode:</p><ul><li>runs the failing test</li><li>evaluates the candidate mapping</li><li>creates a healed test only when accepted</li><li>runs the healed test</li><li>generates the report</li><li>skips GitHub PR creation</li></ul><p>PR mode must be enabled explicitly.</p><p>On macOS or Linux:</p><pre>CREATE_PR=true python auto_healer.py</pre><p>On Windows PowerShell:</p><pre>$env:CREATE_PR=&quot;true&quot;<br>python auto_healer.py</pre><p>Before creating a branch, the tool checks that the Git working tree is clean.</p><p>If unrelated local changes are present, the PR flow stops.</p><p>Generated fixes should not be mixed with unrelated work from the developer’s local environment.</p><h3>The Human-Reviewable Pull Request</h3><p>When PR mode is enabled and the healed test passes, the tool can open a GitHub pull request.</p><p>The PR includes:</p><ul><li>root cause</li><li>applied field rename</li><li>validation result</li><li>confidence</li><li>match score</li><li>safety threshold</li><li>matching evidence</li></ul><p>The tool does not push directly to main.</p><p>It does not merge automatically.</p><p>A human reviewer keeps control over the final decision.</p><p>The V0.4 implementation was also developed through a protected feature-branch workflow and released as:</p><pre>v0.4.0</pre><h3>Current Limits</h3><p>API Drift Healer is still a focused prototype.</p><p>The current flow handles:</p><pre>1 missing required field<br>+<br>1 invalid existing field</pre><p>It does not yet solve multiple field mappings in the same request.</p><p>For example, a request containing:</p><pre>userEmail<br>phoneNumber<br>firstName</pre><p>while OpenAPI requires:</p><pre>email_address<br>phone_number<br>first_name</pre><p>needs a more advanced matching process.</p><p>A future version should:</p><ol><li>compare every invalid source field with every missing target field</li><li>calculate a candidate score matrix</li><li>remove candidates with hard conflicts</li><li>find the safest one-to-one global assignment</li><li>reject the full patch when the result remains ambiguous</li></ol><p>The current prototype also has limited support for:</p><ul><li>nested objects</li><li>arrays</li><li>optional fields</li><li>newly added required fields</li><li>removed fields</li><li>type changes</li><li>multiple endpoints</li><li>placeholder values with unrealistic formats</li></ul><p>The goal is not to automatically rewrite everything.</p><p>The goal is to inspect broadly and automatically change only candidates that pass strict, explainable checks.</p><h3>Roadmap</h3><p>The next major steps are:</p><ul><li>multiple-field candidate matching</li><li>one-to-one global field assignment</li><li>nested object and array support</li><li>CLI arguments and configurable safety modes</li><li>Postman collection adapter</li><li>VS Code .http adapter</li><li>pytest and requests adapter</li><li>GitHub Actions integration</li></ul><p>The core matcher should remain independent from the input format.</p><p>A YAML test, Postman collection, .http file, or Python test should eventually be converted into the same internal model.</p><p>The same drift engine could then analyze each format without duplicating the matching logic.</p><h3>Where AI Fits Later</h3><p>The matcher remains deterministic.</p><p>That is intentional.</p><p>For this workflow, I do not want an LLM deciding whether a test field should be silently rewritten.</p><p>A safer architecture is:</p><pre>Deterministic code decides.<br>Tests validate.<br>AI explains.<br>Humans review.</pre><p>AI may later summarize matching evidence, explain rejected candidates, or help a reviewer understand a complex drift report.</p><p>But it should not bypass deterministic safety checks.</p><p>Tests still need to prove that the accepted patch works.</p><p>Letting an LLM freely edit integration tests is how YAML becomes modern art.</p><h3>Final Thoughts</h3><p>The field replacement itself is not the interesting part.</p><p>Changing userEmail to email_address takes one line of Python.</p><p>The real work happens before and after that line:</p><ul><li>detecting the contract drift</li><li>evaluating the candidate mapping</li><li>rejecting conflicting or weak matches</li><li>applying the accepted patch</li><li>running the healed test</li><li>explaining the decision</li><li>opening a pull request only after PASS</li><li>keeping a human in the loop</li></ul><p>That is what V0.4 was built to prove.</p><p>V0.3 showed that a generated patch could be validated locally. V0.4 focuses on the decision that comes before the patch: whether the candidate mapping should be attempted at all.</p><p>Trust does not come from automation alone.</p><p>It comes from automation that can explain why it made a change and show that the updated test passes.</p><p>For API Drift Healer, that trust still starts with one rule:</p><blockquote><strong><em>No PASS, No PR.</em></strong></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4471a77fbc39" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/building-a-safer-api-drift-healer-deterministic-field-matching-in-v0-4-4471a77fbc39">Building a Safer API Drift Healer: Deterministic Field Matching in V0.4</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building tools to manage cross-cutting concerns in Kubernetes]]></title>
            <link>https://itnext.io/building-tools-to-manage-cross-cutting-concerns-in-kubernetes-by-using-confighub-6bd64afcdd69?source=rss----5b301f10ddcd---4</link>
            <guid isPermaLink="false">https://medium.com/p/6bd64afcdd69</guid>
            <category><![CDATA[config-as-code]]></category>
            <category><![CDATA[config-as-data]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Brian Grant]]></dc:creator>
            <pubDate>Mon, 13 Jul 2026 15:16:05 GMT</pubDate>
            <atom:updated>2026-07-14T21:18:45.645Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W5wsjHgClkt-m3b5jIAU0Q.png" /><figcaption>Robots operating on many similar machines</figcaption></figure><p>I previously wrote about <a href="https://itnext.io/configuration-as-code-is-a-liability-for-security-d0e53727cd17">some Kubernetes namespace policy management tools</a> I built on ConfigHub: <a href="https://github.com/confighub/examples/tree/main/namespace-manager">namespace-manager</a>, <a href="https://github.com/confighub/examples/tree/main/rbac-manager-for-agents">rbac-manager</a>, <a href="https://github.com/confighub/examples/tree/main/network-policy-manager">network-policy-manager</a>. Next, I built some workload-oriented management tools.</p><p>Workloads have some interfaces that are specific to them, such as command-line flags, environment variables, <a href="https://itnext.io/the-challenge-of-configmap-rollouts-in-kubernetes-1431398c0866">application configuration</a>, probe endpoints, and so on. The configuration of those properties is best performed with the schemas and constraints for those interfaces in hand. I’ll write another post about that.</p><p>Workloads also have security and operational <a href="https://itnext.io/the-tension-between-flexibility-and-simplicity-in-infrastructure-as-code-6cec841e3d16">aspects that are cross-cutting</a> and at least partly determined by the deployment environment, such as operating-system capabilities, computational resources, scaling automation, network access, and observability integrations. The workload may have some requirements of these aspects, but their configuration is not unique to the workload and is often at least partly determined by factors outside the workload implementation, such as the choice of autoscaling tool or observability tool, as well as the <a href="https://itnext.io/kubernetes-and-application-platforms-7a36aa5c3c08">application or infrastructure platform</a>, which is Kubernetes for this discussion. Typically the security posture is determined at the organization level, as well, or at least at a larger scope than just one workload.</p><p>With <a href="https://itnext.io/what-is-configuration-as-data-210b0c4be324">configuration as data</a>, not all changes to configuration need to be implemented in a single <a href="https://itnext.io/fundamental-challenges-with-infrastructure-as-code-imply-the-language-doesnt-matter-41030475c296">monolithic generator</a>. They can be decoupled and performed by interoperable tools, just as when building tools <a href="https://itnext.io/configuration-needs-an-api-b36f08b92551">against APIs</a>.</p><p>To demonstrate this, I built a suite of tools, in one day, each with its own agent skills:</p><ul><li><a href="https://github.com/confighub/examples/tree/main/workload-manager"><strong>workload-manager</strong></a>: ensures workloads follow best practices, such as resource specifications, probes, pod disruption budgets, and security context</li><li><a href="https://github.com/confighub/examples/tree/main/scheduling-manager"><strong>scheduling-manager</strong></a>: checks and sets node selectors, node affinity, and tolerations according to some pre-defined profiles</li><li><a href="https://github.com/confighub/examples/tree/main/observability-manager"><strong>observability-manager</strong></a>: manages <a href="https://prometheus-operator.dev/docs/developer/getting-started/#using-servicemonitors">Prometheus ServiceMonitor</a> resources and <a href="https://opentelemetry.io/docs/collector/">Open Telemetry collector</a> sidecar containers</li><li><a href="https://github.com/confighub/examples/tree/main/autoscale-manager"><strong>autoscale-manager</strong></a>: checks and edits HorizontalPodAutoscaler and/or <a href="https://keda.sh/">KEDA</a> resources, and can convert HPA to KEDA</li></ul><p>All of these tools can enforce desired postures, as well, by installing <a href="https://docs.confighub.com/background/entities/trigger/">ConfigHub Triggers</a> via guardrails commands.</p><p>As with the policy tools, these tools can provide lists of the relevant resources across clusters, snapshot summaries, and detailed lists of findings.</p><p>For example, from the workload manager:</p><pre>CLUSTER                                 WORKLOADS  PDBS  UNITS  GATED  UNAPPLIED<br>dev-cluster                             19         0     33     0      21<br>prod-cluster                            19         0     38     0      24<br><br>2 clusters, 38 workloads, 0 pdbs, 71 units (0 gated, 45 unapplied)<br><br>SEVERITY  ANALYZER   CLUSTER          NAMESPACE  KIND         NAME     MESSAGE<br>...<br>HIGH      security   prod-cluster     appvote    Deployment   vote     container &quot;vote&quot; does not set runAsNonRoot: true<br>HIGH      security   prod-cluster     appvote    Deployment   worker   container &quot;worker&quot; allowPrivilegeEscalation is not false<br>HIGH      security   prod-cluster     appvote    Deployment   worker   container &quot;worker&quot; does not set runAsNonRoot: true<br>...<br>MEDIUM    probes     dev-cluster      appchat    Deployment   backend  container &quot;backend&quot; no readiness probe<br>MEDIUM    resources  dev-cluster      appchat    Deployment   backend  container &quot;backend&quot; no cpu limit<br>MEDIUM    resources  dev-cluster      appchat    Deployment   backend  container &quot;backend&quot; no cpu request<br>MEDIUM    security   dev-cluster      appchat    Deployment   backend  container &quot;backend&quot; readOnlyRootFilesystem is not true<br>...<br>LOW       hygiene    dev-cluster      appvote    Deployment   redis    container &quot;redis&quot; terminationMessagePolicy is not FallbackToLogsOnError<br>...<br>39 workloads (0 pass, 21 warn, 18 fail)<br>...<br>  dev-cluster/appchat Deployment/backend:<br>    [hygiene] container &quot;backend&quot; terminationMessagePolicy is not FallbackToLogsOnError<br>    [probes] container &quot;backend&quot; no liveness probe<br>    [probes] container &quot;backend&quot; no readiness probe<br>    [resources] container &quot;backend&quot; no cpu limit<br>    [resources] container &quot;backend&quot; no cpu request<br>    [security] automountServiceAccountToken is not false<br>    [security] container &quot;backend&quot; allowPrivilegeEscalation is not false<br>    [security] container &quot;backend&quot; does not drop ALL capabilities<br>    [security] container &quot;backend&quot; does not set runAsNonRoot: true<br>    [security] container &quot;backend&quot; readOnlyRootFilesystem is not true<br>    [security] container &quot;backend&quot; seccompProfile is not RuntimeDefault<br>...</pre><p>(terminationMessagePolicy is a <a href="https://github.com/kubernetes/kubernetes/issues/139">pet feature of mine</a>. I find it useful.)</p><p>If this were a typical policy tool and you were using helm, cdk8s, tanka, or other configuration as code tool, then you’d have to track down the right repo and configuration files, render the configuration and feed it to the policy tool, ask your AI agent to implement fixes for the correct environment(s), review the changes, and render the configuration again, for all potentially affected environments, and feed it to a validation tool.</p><p>But this is configuration as data, so those checks ran against fully materialized configuration and you can just change the configuration directly with the tools.</p><p>Want a PodDisruptionBudget?</p><pre>% ./bin/cub-workload ensure-pdb appchat-prod/frontend --max-unavailable &quot;10%&quot; -o table<br>Dry run — would create PDB Unit appchat-prod/frontend-pdb:<br><br>apiVersion: policy/v1<br>kind: PodDisruptionBudget<br>metadata:<br>  name: frontend-pdb<br>  namespace: appchat<br>spec:<br>  maxUnavailable: 10%<br>  selector:<br>    matchLabels:<br>      app: frontend<br><br>Re-run with --commit --change-desc &quot;…&quot; to create the Unit. It is not applied until you apply it.</pre><p>That automatically matched the namespace and labels of the specified workload, similar to kubectl expose, which makes it even simpler than kubectl create pdb. For a single workload, an AI agent could also author the resource from whole cloth, especially if it had a sample to start from, as in <a href="https://github.com/confighub/confighub-skills/blob/main/references/yaml-patterns.md#poddisruptionbudget">our agent skills</a> or in our <a href="https://github.com/confighub/installer/blob/main/packages/kubernetes-resources/bases/default/hello-pdb.yaml">package of resource samples</a>.</p><p>Want spreading via pod anti-affinity? ./bin/cub-workload ensure-spread appchat-prod/frontend. CPU and memory resources? set-resources. Probes? set-probes . Security context and disabled service account token? harden . There are lots more features I don’t have space to show in this post. There’s no need to write YAML by hand when tools are perfectly capable of doing it. Remember, these tools are operating on automatically versioned storage, not operating on the clusters directly.</p><p>To be agent-friendly, the commands execute in dry-run mode and output JSON by default. Agents can execute the commands and generate summary tables regarding the current state and changes they made. With approval, they can “commit” changes by performing the same operations without dry run. The changed configuration will be validated automatically, and humans can still review and approve the changes before releasing them to the clusters, and can revert or adjust them as needed.</p><p>I’m also experimenting with writing higher-level intent to the resources using annotations. I’ll write about that in a future post once I have more experience with it.</p><p>Under the hood, the tools work through a combination of ConfigHub <a href="https://docs.confighub.com/background/concepts/filters/">filters</a>, <a href="https://docs.confighub.com/guide/functions/">functions</a>, such as set-container-resources and set-pod-container-security-context-defaults, client-side analysis, client-side mutations using the <a href="https://github.com/confighub/sdk/tree/main/function-impl#readme">function SDK</a>, saved function <a href="https://docs.confighub.com/background/entities/invocation/">invocations</a>, triggers, and other ConfigHub features.</p><p>Because the configuration data is authoritative, these tools can interoperate with other tools that read and write the configuration data, including other ConfigHub functions, such as set-container-image-reference and set-replicas, and AI agents editing the configuration. Tools can be single-purpose or multi-purpose, as desired, but they don’t need to be monolithic, covering every field of Kubernetes Deployment, StatefulSet, and DaemonSet resources. The tools can also be versatile. They can analyze, summarize, generate, backfill, promote, validate, and enforce.</p><p>And because we <a href="https://itnext.io/configuration-belongs-in-a-database-be6d31f46d3d">store configuration in a database</a> instead it being sprawled across many git repositories owned by different teams, the tools don’t need to clone lots of repositories, make ad hoc edits, commit and push changes, and create lots of PRs. Instead, they can call APIs. I’ll write another post about the “initiative” system that we’re working on for changes that span multiple teams.</p><p>I’m sure the tools still have rough edges and need more work to make them usable — I’ve barely had time to try them yet, but the point is that we CAN build and use tools if we’re not using configuration as code. The tools can operate on variants of the same application across clusters, and across multiple different applications, not just a single application, using standard APIs. Composable, interoperable tools are more reusable than all-encompassing templates.</p><p>That last point is pretty important. Yes, for one application, you could use a heavily parameterized <a href="https://github.com/companyinfo/helm-charts/blob/main/charts/helmet/templates/_hpa.yaml">template library</a> to create, say, a HorizontalPodAutoscaler, and add <a href="https://github.com/companyinfo/helm-charts/blob/main/charts/helmet/values.yaml#L441">some values</a> to drive it. But every Helm chart exposes different input values, so there’s no standard API that one could build tooling around.</p><p>The common alternative automated mutation methods are less transparent and more complicated operationally:</p><ul><li>kustomize overlays</li><li>mutating dynamic admission control</li><li>Operators (e.g., to inject sidecar containers or <a href="https://www.reddit.com/r/kubernetes/comments/1ultrqo/wow_so_poddisruptionbudget_pdb_is_exactly_what/">create PDBs</a>)</li></ul><p>ConfigHub makes it possible to directly modify configuration while maintaining multiple similar-but-different variants of the same configuration through <a href="https://itnext.io/how-are-variants-managed-in-confighub-061bc2f89cff">mechanisms that help keep them in sync</a>. That’s one of the key challenges we’re solving with ConfigHub.</p><p>I think the approach of building tools on top of configuration as data has a lot of promise. Try the tools and let me know what you think. Or build your own and let me know how they turn out.</p><p>What cross-cutting customizations do you frequently need to make to Kubernetes workload configurations? What are common changes that aren’t supported by helm charts you use? How do you override them? What common operational changes do you need to make beyond just replicas and resources? How often do people in your organization “break glass” and change clusters directly? What are the reasons? Have you migrated from HPA to KEDA, or Ingress to Gateway, or Nginx Ingress to Traefik? Do you have operations, platform, or security teams that need to modify configurations owned by application teams? What kubectl commands or other tools should we adapt to ConfigHub?</p><p>Reply here, or send me a message on <a href="https://www.linkedin.com/in/bgrant0607/">LinkedIn</a>, <a href="https://x.com/bgrant0607">X/Twitter</a>, or <a href="https://bsky.app/profile/bgrant0607.bsky.social">Bluesky</a>, where I plan to crosspost this.</p><p>You could also <a href="https://auth.confighub.com/sign-up">try out ConfigHub</a>, which is now in preview.</p><p>If you found this interesting, you may be interested in other posts in my <a href="https://medium.com/@bgrant0607/list/kubernetes-8b0b8930195b">Kubernetes series</a> or my <a href="https://medium.com/@bgrant0607/list/infrastructure-as-code-and-declarative-configuration-8c441ae74836">IaC / declarative configuration series</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6bd64afcdd69" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/building-tools-to-manage-cross-cutting-concerns-in-kubernetes-by-using-confighub-6bd64afcdd69">Building tools to manage cross-cutting concerns in Kubernetes</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>