<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>topicgitopsdelivery &amp;mdash; heyimusa</title>
    <link>https://heyimusa.blog/tag:topicgitopsdelivery</link>
    <description></description>
    <pubDate>Tue, 01 Sep 2026 06:46:24 +0000</pubDate>
    <item>
      <title>Refactoring is bonsai work, but the shears are tests</title>
      <link>https://heyimusa.blog/refactoring-is-bonsai-work</link>
      <description>&lt;![CDATA[A mature codebase rarely announces that it needs refactoring. It makes a small change feel oddly expensive. A new route requires touching three modules whose names no longer describe their responsibilities. A dependency update turns into archaeology. An incident fix needs a second person on the call because nobody is certain which branch of the conditional protects the customer path.&#xA;&#xA;That is the moment I find the bonsai metaphor useful, with one important correction: the job is not to make code look pretty. The job is to preserve a living system while shaping it so the next change has somewhere safe to go.&#xA;&#xA;!--more--&#xA;&#xA;The metaphor is useful because growth is the problem&#xA;&#xA;Bonsai is not a miniature tree that happens by accident. Its shape comes from repeated, deliberate interventions: pruning, wiring, repotting, and deciding which growth to keep. The tree stays alive, but it does not stay untouched.&#xA;&#xA;Software has the same tension. A healthy service keeps changing because product requirements, dependencies, traffic, and operators change. Left alone, the code can still compile and serve requests while its internal shape becomes costly to understand. The first symptom is not necessarily a defect. Often it is hesitation: the team starts treating a local edit as if it might wake something elsewhere.&#xA;&#xA;Martin Fowler&#39;s definition adds the part that is easy to lose: refactoring changes internal structure to make software easier to understand and cheaper to modify, without changing observable behavior. That excludes feature work, a migration that changes a contract, and a rewrite dressed up as cleanup. Those can all be worthwhile. They simply need different planning and different evidence.&#xA;&#xA;Calling everything &#34;refactoring&#34; is how a small maintenance task acquires an unlimited blast radius.&#xA;&#xA;A bonsai is not a weekly haircut&#xA;&#xA;The weak version of the metaphor produces cosmetic work: rename a few variables, reorder imports, split a long file, declare victory. Those changes can help, but they do not automatically make future changes safer.&#xA;&#xA;A better question is: what recurring change does this shape make expensive?&#xA;&#xA;If every new HTTP endpoint requires copying authorization checks, the useful work might be extracting an explicit policy boundary. If a feature flag has spread through handlers, workers, and templates, the issue is probably lifecycle ownership rather than the length of any one function. If an on-call engineer cannot identify the request path during an outage, improving observability or module boundaries may matter more than satisfying a linter.&#xA;&#xA;Refactoring earns its keep when a real next change has exposed a structural constraint.&#xA;&#xA;The shears are tests, not confidence&#xA;&#xA;There is a dangerous part of the gardening analogy. Pruning a branch is visible. A behavior regression may be invisible until production traffic finds it.&#xA;&#xA;That is why tests are not an optional polish step around refactoring. They are the evidence that the system still behaves as intended after an internal change. The refactoring literature repeatedly emphasizes small, safe mechanics. In a 2020 case study on an oversized class, Clare Sudbery describes keeping the code compiling and the tests running at every step, with coverage in place before the refactor starts.&#xA;&#xA;For a service, I would want the narrowest test that protects the behavior being moved, plus the checks that make the deployment credible:&#xA;&#xA;a unit or component test for the rule being extracted;&#xA;a contract or integration test where the code crosses a boundary; and&#xA;where proportionate, a post-deployment synthetic probe or smoke check for the important path.&#xA;&#xA;The exact stack differs. If the available evidence cannot support the claim that the intended behavior stayed stable, describe the change more modestly or add the missing check.&#xA;&#xA;This also gives a useful stop condition. When a proposed cleanup cannot be protected by a reasonably scoped test, the first task may be characterization: document and test what the system does today, including the awkward behavior nobody wants to rediscover during an incident. Only then is the pruning safe.&#xA;&#xA;AI makes overgrowth faster&#xA;&#xA;AI-assisted coding changes the economics here. It can produce a working branch, a duplicate helper, or another configuration path in seconds. That is useful when the team is deliberate. It also means structural debt can arrive at a rate that manual coding rarely achieved.&#xA;&#xA;A 2025 paper, The Shift from Writing to Pruning Software, proposes a bonsai-inspired interface for AI-generated code. Its core observation is sensible: when generation becomes cheap, the human task shifts toward navigating, evaluating, and reshaping a larger body of code. The paper proposes an interface; it is not evidence that the approach improves production reliability. Still, the direction matches a practical concern: generation can reduce the cost of adding code without reducing the cost of owning it.&#xA;&#xA;The response should not be &#34;never use AI.&#34; It should be to give generated code the same ownership questions as handwritten code:&#xA;&#xA;What responsibility does this new code own?&#xA;Which existing path does it duplicate or bypass?&#xA;What behavior proves it belongs here?&#xA;If it needs to change later, where would an engineer look first?&#xA;&#xA;If those questions have no good answer, the code is not ready just because it passes the current build.&#xA;&#xA;An infrastructure engineer prunes more than application code&#xA;&#xA;For an infrastructure or platform engineer, the tree extends beyond a service repository: delivery pipelines; infrastructure definitions and environment overlays; identity, network, and secret boundaries; runtime configuration; observability; and the runbooks and recovery paths that connect them.&#xA;&#xA;The same failure mode appears at every layer: a workaround becomes a pattern, a copied pipeline step diverges, an exception disappears into an overlay, or an alert keeps firing because no one owns the decision it is meant to support. Declarative infrastructure can accumulate structural debt just as application code can.&#xA;&#xA;That makes refactoring part of the platform engineer&#39;s job, not a side activity reserved for developers. The target is not maximal abstraction. It is a platform whose next user, deploy, incident, and security change can be made with a clear owner, a small review surface, and evidence.&#xA;&#xA;A practical sweep across the field looks like this:&#xA;&#xA;Platform and developer experience: turn tribal deployment knowledge into paved paths, understandable templates, and documented interfaces. Remove a platform abstraction when it only hides the system without removing work.&#xA;CI/CD and GitOps: consolidate duplicated release logic, make promotion and rollback paths explicit, and keep generated or rendered configuration reviewable. A pipeline refactor is incomplete if an operator cannot tell what will reach production.&#xA;Cloud and infrastructure as code: separate reusable modules from environment-specific intent, name ownership boundaries, and avoid copy-pasted resources that drift independently. Treat plans and policy checks as evidence, while remembering that they do not prove the live system is unchanged.&#xA;Kubernetes and runtime operations: simplify workload, ingress, Gateway, policy, and configuration overlays while preserving the routing and rollout contract. Rendered manifests and a bounded runtime check are stronger evidence than a tidy directory tree.&#xA;Security and identity: reduce broad permissions, clarify trust boundaries, remove obsolete access paths, and make secret rotation and break-glass procedures testable. A cleaner IAM policy is not safer unless the required workload still functions and the unwanted path is actually closed.&#xA;Networking and reliability: replace undocumented routing exceptions with explicit contracts; preserve DNS, TLS, timeout, retry, and failover behavior as you simplify. For a customer-facing path, verification must extend beyond a syntax check.&#xA;Observability and incident response: prune dashboards, alerts, and runbooks that no longer map to an operational decision. Keep the signals that let an engineer answer what changed, who is affected, and how to roll back.&#xA;Cost and capacity: remove idle or duplicated resources only after measuring dependency and demand. Cost reduction is a production change, not janitorial work.&#xA;&#xA;None of these areas needs a giant “platform cleanup” programme to improve. The valuable unit is still a small, explainable structural change backed by the evidence appropriate to that layer: an IaC plan, a rendered manifest diff, a policy evaluation, a synthetic request, a rollout observation, or a recovery exercise. The appropriate checks vary by layer, but each should support a specific preservation claim.&#xA;&#xA;Prune on the path of work&#xA;&#xA;A bonsai owner does not reshape the entire tree every time they notice one awkward branch. The same restraint matters in production code.&#xA;&#xA;I prefer refactoring that is adjacent to a real change: improve a boundary before extending it, remove duplication when the second copy appears, name a concept when the team needs to discuss it, and leave unrelated cleanups alone. This keeps the diff reviewable and lets the pull request tell a coherent story.&#xA;&#xA;It also makes verification more honest. A narrowly scoped refactor can state what it preserved, which tests exercised that claim, and what it deliberately did not touch. A broad &#34;cleanup&#34; commit usually cannot.&#xA;&#xA;There are times to fund a larger structural effort. Repeated incidents, a blocked security upgrade, an untestable critical path, or a migration that keeps failing for the same architectural reason are all stronger signals than aesthetic discomfort. Even then, split the work into behavior-preserving slices where possible. The system does not care that the end state looked tidy in a diagram if the path there breaks customers.&#xA;&#xA;A small operating loop&#xA;&#xA;Before calling a change refactoring, I would write down four things:&#xA;&#xA;The behavior to preserve. Describe it from the caller or user perspective, not from the internal class name.&#xA;The structural pressure. Name the repeated cost: duplicated policy, hidden dependency, impossible test setup, ambiguous ownership, or something equally concrete.&#xA;The evidence. Identify the test or probe that should remain green before and after the change.&#xA;The boundary. State what is not being redesigned in this pull request.&#xA;&#xA;Then make one structural move, run the evidence, and review the diff as if you were the next person debugging it at an inconvenient hour.&#xA;&#xA;That is less romantic than trimming a miniature tree. It is also more useful. Refactoring is routine maintenance of the structure through which future changes must travel.&#xA;&#xA;Limits of the metaphor&#xA;&#xA;Bonsai suggests a single careful owner and a stable aesthetic goal. Real systems have many owners, competing requirements, and behavior that must change. Refactoring will not replace migration planning, incident response, performance work, or product decisions. A green test suite also cannot prove every production property, especially across external dependencies and real traffic.&#xA;&#xA;So use the image lightly. Keep the discipline.&#xA;&#xA;Preserve the behavior you can state. Improve the structure that is making real work harder. Keep the change small enough to understand. And do not put the shears down until you have evidence that the tree still lives.&#xA;&#xA;Sources and further reading&#xA;&#xA;Martin Fowler, Refactoring. Fowler defines refactoring as changing internal structure to make software easier to understand and cheaper to modify without changing observable behavior.&#xA;Clare Sudbery, Refactoring: This class is too large (2020). A practical case study of incremental refactoring, covering tests, compiling at every step, and small commits.&#xA;Ulam Labs, Why is refactoring your code important? (2020). A useful example of the bonsai analogy, though this article intentionally avoids treating cleanliness or security as automatic outcomes of refactoring.&#xA;The Shift from Writing to Pruning Software: A Bonsai-Inspired IDE for Reshaping AI Generated Code, arXiv:2503.02833 (2025). A research proposal about navigating and reshaping generated code, not a production-effectiveness study.&#xA;&#xA;!-- taxonomy: #topicplatformengineering #topicgitopsdelivery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>A mature codebase rarely announces that it needs refactoring. It makes a small change feel oddly expensive. A new route requires touching three modules whose names no longer describe their responsibilities. A dependency update turns into archaeology. An incident fix needs a second person on the call because nobody is certain which branch of the conditional protects the customer path.</p>

<p>That is the moment I find the bonsai metaphor useful, with one important correction: the job is not to make code look pretty. The job is to preserve a living system while shaping it so the next change has somewhere safe to go.</p>



<h2 id="the-metaphor-is-useful-because-growth-is-the-problem">The metaphor is useful because growth is the problem</h2>

<p>Bonsai is not a miniature tree that happens by accident. Its shape comes from repeated, deliberate interventions: pruning, wiring, repotting, and deciding which growth to keep. The tree stays alive, but it does not stay untouched.</p>

<p>Software has the same tension. A healthy service keeps changing because product requirements, dependencies, traffic, and operators change. Left alone, the code can still compile and serve requests while its internal shape becomes costly to understand. The first symptom is not necessarily a defect. Often it is hesitation: the team starts treating a local edit as if it might wake something elsewhere.</p>

<p>Martin Fowler&#39;s definition adds the part that is easy to lose: refactoring changes internal structure to make software easier to understand and cheaper to modify, without changing observable behavior. That excludes feature work, a migration that changes a contract, and a rewrite dressed up as cleanup. Those can all be worthwhile. They simply need different planning and different evidence.</p>

<p>Calling everything “refactoring” is how a small maintenance task acquires an unlimited blast radius.</p>

<h2 id="a-bonsai-is-not-a-weekly-haircut">A bonsai is not a weekly haircut</h2>

<p>The weak version of the metaphor produces cosmetic work: rename a few variables, reorder imports, split a long file, declare victory. Those changes can help, but they do not automatically make future changes safer.</p>

<p>A better question is: <em>what recurring change does this shape make expensive?</em></p>

<p>If every new HTTP endpoint requires copying authorization checks, the useful work might be extracting an explicit policy boundary. If a feature flag has spread through handlers, workers, and templates, the issue is probably lifecycle ownership rather than the length of any one function. If an on-call engineer cannot identify the request path during an outage, improving observability or module boundaries may matter more than satisfying a linter.</p>

<p>Refactoring earns its keep when a real next change has exposed a structural constraint.</p>

<h2 id="the-shears-are-tests-not-confidence">The shears are tests, not confidence</h2>

<p>There is a dangerous part of the gardening analogy. Pruning a branch is visible. A behavior regression may be invisible until production traffic finds it.</p>

<p>That is why tests are not an optional polish step around refactoring. They are the evidence that the system still behaves as intended after an internal change. The refactoring literature repeatedly emphasizes small, safe mechanics. In a 2020 case study on an oversized class, Clare Sudbery describes keeping the code compiling and the tests running at every step, with coverage in place before the refactor starts.</p>

<p>For a service, I would want the narrowest test that protects the behavior being moved, plus the checks that make the deployment credible:</p>
<ul><li>a unit or component test for the rule being extracted;</li>
<li>a contract or integration test where the code crosses a boundary; and</li>
<li>where proportionate, a post-deployment synthetic probe or smoke check for the important path.</li></ul>

<p>The exact stack differs. If the available evidence cannot support the claim that the intended behavior stayed stable, describe the change more modestly or add the missing check.</p>

<p>This also gives a useful stop condition. When a proposed cleanup cannot be protected by a reasonably scoped test, the first task may be characterization: document and test what the system does today, including the awkward behavior nobody wants to rediscover during an incident. Only then is the pruning safe.</p>

<h2 id="ai-makes-overgrowth-faster">AI makes overgrowth faster</h2>

<p>AI-assisted coding changes the economics here. It can produce a working branch, a duplicate helper, or another configuration path in seconds. That is useful when the team is deliberate. It also means structural debt can arrive at a rate that manual coding rarely achieved.</p>

<p>A 2025 paper, <em>The Shift from Writing to Pruning Software</em>, proposes a bonsai-inspired interface for AI-generated code. Its core observation is sensible: when generation becomes cheap, the human task shifts toward navigating, evaluating, and reshaping a larger body of code. The paper proposes an interface; it is not evidence that the approach improves production reliability. Still, the direction matches a practical concern: generation can reduce the cost of adding code without reducing the cost of owning it.</p>

<p>The response should not be “never use AI.” It should be to give generated code the same ownership questions as handwritten code:</p>
<ul><li>What responsibility does this new code own?</li>
<li>Which existing path does it duplicate or bypass?</li>
<li>What behavior proves it belongs here?</li>
<li>If it needs to change later, where would an engineer look first?</li></ul>

<p>If those questions have no good answer, the code is not ready just because it passes the current build.</p>

<h2 id="an-infrastructure-engineer-prunes-more-than-application-code">An infrastructure engineer prunes more than application code</h2>

<p>For an infrastructure or platform engineer, the tree extends beyond a service repository: delivery pipelines; infrastructure definitions and environment overlays; identity, network, and secret boundaries; runtime configuration; observability; and the runbooks and recovery paths that connect them.</p>

<p>The same failure mode appears at every layer: a workaround becomes a pattern, a copied pipeline step diverges, an exception disappears into an overlay, or an alert keeps firing because no one owns the decision it is meant to support. Declarative infrastructure can accumulate structural debt just as application code can.</p>

<p>That makes refactoring part of the platform engineer&#39;s job, not a side activity reserved for developers. The target is not maximal abstraction. It is a platform whose next user, deploy, incident, and security change can be made with a clear owner, a small review surface, and evidence.</p>

<p>A practical sweep across the field looks like this:</p>
<ul><li><strong>Platform and developer experience:</strong> turn tribal deployment knowledge into paved paths, understandable templates, and documented interfaces. Remove a platform abstraction when it only hides the system without removing work.</li>
<li><strong>CI/CD and GitOps:</strong> consolidate duplicated release logic, make promotion and rollback paths explicit, and keep generated or rendered configuration reviewable. A pipeline refactor is incomplete if an operator cannot tell what will reach production.</li>
<li><strong>Cloud and infrastructure as code:</strong> separate reusable modules from environment-specific intent, name ownership boundaries, and avoid copy-pasted resources that drift independently. Treat plans and policy checks as evidence, while remembering that they do not prove the live system is unchanged.</li>
<li><strong>Kubernetes and runtime operations:</strong> simplify workload, ingress, Gateway, policy, and configuration overlays while preserving the routing and rollout contract. Rendered manifests and a bounded runtime check are stronger evidence than a tidy directory tree.</li>
<li><strong>Security and identity:</strong> reduce broad permissions, clarify trust boundaries, remove obsolete access paths, and make secret rotation and break-glass procedures testable. A cleaner IAM policy is not safer unless the required workload still functions and the unwanted path is actually closed.</li>
<li><strong>Networking and reliability:</strong> replace undocumented routing exceptions with explicit contracts; preserve DNS, TLS, timeout, retry, and failover behavior as you simplify. For a customer-facing path, verification must extend beyond a syntax check.</li>
<li><strong>Observability and incident response:</strong> prune dashboards, alerts, and runbooks that no longer map to an operational decision. Keep the signals that let an engineer answer what changed, who is affected, and how to roll back.</li>
<li><strong>Cost and capacity:</strong> remove idle or duplicated resources only after measuring dependency and demand. Cost reduction is a production change, not janitorial work.</li></ul>

<p>None of these areas needs a giant “platform cleanup” programme to improve. The valuable unit is still a small, explainable structural change backed by the evidence appropriate to that layer: an IaC plan, a rendered manifest diff, a policy evaluation, a synthetic request, a rollout observation, or a recovery exercise. The appropriate checks vary by layer, but each should support a specific preservation claim.</p>

<h2 id="prune-on-the-path-of-work">Prune on the path of work</h2>

<p>A bonsai owner does not reshape the entire tree every time they notice one awkward branch. The same restraint matters in production code.</p>

<p>I prefer refactoring that is adjacent to a real change: improve a boundary before extending it, remove duplication when the second copy appears, name a concept when the team needs to discuss it, and leave unrelated cleanups alone. This keeps the diff reviewable and lets the pull request tell a coherent story.</p>

<p>It also makes verification more honest. A narrowly scoped refactor can state what it preserved, which tests exercised that claim, and what it deliberately did not touch. A broad “cleanup” commit usually cannot.</p>

<p>There are times to fund a larger structural effort. Repeated incidents, a blocked security upgrade, an untestable critical path, or a migration that keeps failing for the same architectural reason are all stronger signals than aesthetic discomfort. Even then, split the work into behavior-preserving slices where possible. The system does not care that the end state looked tidy in a diagram if the path there breaks customers.</p>

<h2 id="a-small-operating-loop">A small operating loop</h2>

<p>Before calling a change refactoring, I would write down four things:</p>
<ol><li><strong>The behavior to preserve.</strong> Describe it from the caller or user perspective, not from the internal class name.</li>
<li><strong>The structural pressure.</strong> Name the repeated cost: duplicated policy, hidden dependency, impossible test setup, ambiguous ownership, or something equally concrete.</li>
<li><strong>The evidence.</strong> Identify the test or probe that should remain green before and after the change.</li>
<li><strong>The boundary.</strong> State what is not being redesigned in this pull request.</li></ol>

<p>Then make one structural move, run the evidence, and review the diff as if you were the next person debugging it at an inconvenient hour.</p>

<p>That is less romantic than trimming a miniature tree. It is also more useful. Refactoring is routine maintenance of the structure through which future changes must travel.</p>

<h2 id="limits-of-the-metaphor">Limits of the metaphor</h2>

<p>Bonsai suggests a single careful owner and a stable aesthetic goal. Real systems have many owners, competing requirements, and behavior that must change. Refactoring will not replace migration planning, incident response, performance work, or product decisions. A green test suite also cannot prove every production property, especially across external dependencies and real traffic.</p>

<p>So use the image lightly. Keep the discipline.</p>

<p>Preserve the behavior you can state. Improve the structure that is making real work harder. Keep the change small enough to understand. And do not put the shears down until you have evidence that the tree still lives.</p>

<h2 id="sources-and-further-reading">Sources and further reading</h2>
<ul><li>Martin Fowler, <a href="https://refactoring.com/">Refactoring</a>. Fowler defines refactoring as changing internal structure to make software easier to understand and cheaper to modify without changing observable behavior.</li>
<li>Clare Sudbery, <a href="https://martinfowler.com/articles/class-too-large.html">Refactoring: This class is too large</a> (2020). A practical case study of incremental refactoring, covering tests, compiling at every step, and small commits.</li>
<li>Ulam Labs, <a href="https://ulam.io/blog/why-refactoring-code-important/">Why is refactoring your code important?</a> (2020). A useful example of the bonsai analogy, though this article intentionally avoids treating cleanliness or security as automatic outcomes of refactoring.</li>
<li><em>The Shift from Writing to Pruning Software: A Bonsai-Inspired IDE for Reshaping AI Generated Code</em>, <a href="https://arxiv.org/abs/2503.02833">arXiv:2503.02833</a> (2025). A research proposal about navigating and reshaping generated code, not a production-effectiveness study.</li></ul>


]]></content:encoded>
      <guid>https://heyimusa.blog/refactoring-is-bonsai-work</guid>
      <pubDate>Tue, 11 Aug 2026 16:02:41 +0000</pubDate>
    </item>
    <item>
      <title>A GitHub Actions workflow is a deployment boundary, not a YAML file</title>
      <link>https://heyimusa.blog/a-github-actions-workflow-is-a-deployment-boundary</link>
      <description>&lt;![CDATA[I compared two synthetic GitHub Actions workflows in a locked-down Docker container. Both could build and publish a container image. Only one made the trust boundary obvious in review.&#xA;&#xA;GitHub&#39;s 2026 Actions security roadmap is pushing in a useful direction: reproducible action dependencies, policy controls over who can trigger a workflow, and an evaluate mode before enforcement. The common theme is that workflow YAML is security-sensitive infrastructure.&#xA;&#xA;That sounds obvious until a pull request changes three lines and turns a test workflow into a release path.&#xA;&#xA;!--more--&#xA;&#xA;I wanted a small way to describe that change without pretending to run a full GitHub Actions security scanner. The question was narrower:&#xA;&#xA;  Which event can reach which credentialed capability?&#xA;&#xA;That is an execution contract. It is more useful in review than a list of uses: lines.&#xA;&#xA;The two workflows&#xA;&#xA;I wrote two synthetic workflows. They both use docker/build-push-action and both can publish an image to GHCR.&#xA;&#xA;The first one is deliberately unsafe. It runs on pullrequesttarget, checks out the pull request head, has write-all permissions, logs in to GHCR with GITHUBTOKEN, pushes an image, and sends a deployment webhook.&#xA;&#xA;on:&#xA;  pullrequesttarget:&#xA;permissions: write-all&#xA;&#xA;jobs:&#xA;  release:&#xA;    steps:&#xA;      uses: actions/checkout@v4&#xA;        with:&#xA;          ref: ${{ github.event.pullrequest.head.sha }}&#xA;      uses: docker/login-action@v3&#xA;      uses: docker/build-push-action@v6&#xA;        with:&#xA;          push: true&#xA;      run: curl -fsS -X POST &#34;$DEPLOYWEBHOOK&#34;&#xA;        env:&#xA;          DEPLOYWEBHOOK: ${{ secrets.DEPLOYWEBHOOK }}&#xA;&#xA;The second workflow separates verification from publication. Pull requests can run npm ci &amp;&amp; npm test. The publish job runs only on a push to main and declares the one extra permission it needs: packages: write.&#xA;&#xA;on:&#xA;  pullrequest:&#xA;  push:&#xA;    branches: [main]&#xA;permissions:&#xA;  contents: read&#xA;&#xA;jobs:&#xA;  verify:&#xA;    if: github.eventname == &#39;pullrequest&#39;&#xA;    steps:&#xA;      uses: actions/checkout@v4&#xA;      run: npm ci &amp;&amp; npm test&#xA;&#xA;  publish:&#xA;    if: github.eventname == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/main&#39;&#xA;    permissions:&#xA;      contents: read&#xA;      packages: write&#xA;    steps:&#xA;      uses: actions/checkout@v4&#xA;      uses: docker/login-action@v3&#xA;      uses: docker/build-push-action@v6&#xA;        with:&#xA;          push: true&#xA;&#xA;Neither example proves a real repository is safe. They are small enough to make the difference visible.&#xA;&#xA;A contract, not a verdict&#xA;&#xA;I wrote a short parser that reads workflow events, job conditions, permissions, secret references, image publishing, and a simple deployment-webhook signal. Its output is a table-shaped JSON document. It does not execute workflow code and it does not contact GitHub.&#xA;&#xA;The hardened workflow produced two separate contracts:&#xA;&#xA;{&#xA;  &#34;workflow&#34;: &#34;hardened.yml&#34;,&#xA;  &#34;job&#34;: &#34;verify&#34;,&#xA;  &#34;events&#34;: [&#34;pullrequest&#34;, &#34;push&#34;],&#xA;  &#34;condition&#34;: &#34;github.eventname == &#39;pullrequest&#39;&#34;,&#xA;  &#34;capabilities&#34;: {&#xA;    &#34;packageswrite&#34;: false,&#xA;    &#34;publishesimage&#34;: false,&#xA;    &#34;usessecrets&#34;: false&#xA;  },&#xA;  &#34;findings&#34;: []&#xA;}&#xA;&#xA;The publish job had image-publish capability, but only behind an explicit trusted-main condition and packages: write permission. The unsafe workflow showed the opposite shape:&#xA;&#xA;{&#xA;  &#34;workflow&#34;: &#34;unsafe-pr-release.yml&#34;,&#xA;  &#34;job&#34;: &#34;release&#34;,&#xA;  &#34;events&#34;: [&#34;pullrequesttarget&#34;],&#xA;  &#34;condition&#34;: null,&#xA;  &#34;capabilities&#34;: {&#xA;    &#34;deploywebhook&#34;: true,&#xA;    &#34;publishesimage&#34;: true,&#xA;    &#34;usessecrets&#34;: true&#xA;  },&#xA;  &#34;findings&#34;: [&#xA;    &#34;untrusted PR event reaches credentialed publish/deploy capability&#34;,&#xA;    &#34;image publish lacks an explicit trusted-main push guard&#34;&#xA;  ]&#xA;}&#xA;&#xA;The parser did not need to know whether the webhook URL was real or whether the registry existed. It only needed to show a reviewer that a pull request event, credentialed steps, and a publish/deploy capability were in the same execution path.&#xA;&#xA;That is the part I do not want buried in YAML.&#xA;&#xA;SHA pinning answers a different question&#xA;&#xA;Pinning actions/checkout or docker/build-push-action to immutable commits is worthwhile. GitHub&#39;s roadmap makes the case for locking direct and transitive action dependencies so a workflow executes the code that reviewers actually approved.&#xA;&#xA;But a pinned action can still run in the wrong trust context.&#xA;&#xA;A review has at least two questions:&#xA;&#xA;| Review question | Evidence to inspect |&#xA;|---|---|&#xA;| What action code will run? | Immutable action reference or dependency lock |&#xA;| Who can cause that code to run with credentials? | Event, condition, permissions, secrets, and publish/deploy steps |&#xA;&#xA;The first is dependency integrity. The second is an execution boundary. Both matter, and neither substitutes for the other.&#xA;&#xA;Why an evaluate mode matters&#xA;&#xA;GitHub describes an evaluate mode for workflow execution rules: show runs that a policy would block before enforcing the policy. That is the right rollout shape.&#xA;&#xA;CI policy changes can stop releases. A rule that bans a trigger or requires a particular actor may catch a real problem, but it can also catch a legitimate release workflow nobody remembered existed. An evidence-first review gives teams somewhere to start:&#xA;&#xA;workflow event&#xA;  -  job condition&#xA;  -  permissions and secrets&#xA;  -  image publish or deployment capability&#xA;  -  review-required decision&#xA;&#xA;The goal is not to auto-declare every pullrequesttarget workflow malicious, or every image publish wrong. There are repositories with carefully designed reasons to use sensitive events. The goal is to make that design legible before someone merges it.&#xA;&#xA;Where this experiment stops&#xA;&#xA;This was a smoke test, not a GitHub Actions emulator or a security assessment. The parser does not resolve reusable workflows, composite-action internals, organization rulesets, environment protection, branch protection, OIDC claims, runner network policy, or runtime expression values. It does not prove that a job can actually obtain a secret.&#xA;&#xA;The test ran in a disposable Docker image with no network, no mounted host directory, a read-only filesystem, a 16 MiB temporary filesystem, all Linux capabilities dropped, no-new-privileges, one CPU, 128 MiB memory, and a PID limit of 64. The image ran as UID 65532 against two synthetic workflow files. No GitHub credential, repository, workflow run, registry, deployment endpoint, or customer source code was used.&#xA;&#xA;The output JSON SHA-256 was:&#xA;&#xA;665061732b6ec86dca49c8fbfef2dbeff6d809a9e2c53195ae252ca4b5d169b7&#xA;&#xA;The useful next step is not another all-purpose CI scanner. It is a small review companion that can turn a workflow diff into an execution-contract diff: which event, actor, permission, secret, registry, or deployment surface changed, and which part still needs a human decision.&#xA;&#xA;That would extend the same review habit I want from GitOps manifests and Gateway migrations. Treat the artifact as evidence. Make the operational consequence visible. Leave the decision with the person who owns the risk.&#xA;&#xA;Sources: GitHub Actions 2026 security roadmap · GitHub Actions security hardening guidance · I built a security-impact diff for GitOps · A Gateway migration is a routing change, not a YAML conversion&#xA;&#xA;!-- taxonomy: #topicgitopsdelivery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><em>I compared two synthetic GitHub Actions workflows in a locked-down Docker container. Both could build and publish a container image. Only one made the trust boundary obvious in review.</em></p>

<p>GitHub&#39;s 2026 Actions security roadmap is pushing in a useful direction: reproducible action dependencies, policy controls over who can trigger a workflow, and an evaluate mode before enforcement. The common theme is that workflow YAML is security-sensitive infrastructure.</p>

<p>That sounds obvious until a pull request changes three lines and turns a test workflow into a release path.</p>



<p>I wanted a small way to describe that change without pretending to run a full GitHub Actions security scanner. The question was narrower:</p>

<blockquote><p>Which event can reach which credentialed capability?</p></blockquote>

<p>That is an execution contract. It is more useful in review than a list of <code>uses:</code> lines.</p>

<h2 id="the-two-workflows">The two workflows</h2>

<p>I wrote two synthetic workflows. They both use <code>docker/build-push-action</code> and both can publish an image to GHCR.</p>

<p>The first one is deliberately unsafe. It runs on <code>pull_request_target</code>, checks out the pull request head, has <code>write-all</code> permissions, logs in to GHCR with <code>GITHUB_TOKEN</code>, pushes an image, and sends a deployment webhook.</p>

<pre><code class="language-yaml">on:
  pull_request_target:
permissions: write-all

jobs:
  release:
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - uses: docker/login-action@v3
      - uses: docker/build-push-action@v6
        with:
          push: true
      - run: curl -fsS -X POST &#34;$DEPLOY_WEBHOOK&#34;
        env:
          DEPLOY_WEBHOOK: ${{ secrets.DEPLOY_WEBHOOK }}
</code></pre>

<p>The second workflow separates verification from publication. Pull requests can run <code>npm ci &amp;&amp; npm test</code>. The publish job runs only on a push to <code>main</code> and declares the one extra permission it needs: <code>packages: write</code>.</p>

<pre><code class="language-yaml">on:
  pull_request:
  push:
    branches: [main]
permissions:
  contents: read

jobs:
  verify:
    if: github.event_name == &#39;pull_request&#39;
    steps:
      - uses: actions/checkout@v4
      - run: npm ci &amp;&amp; npm test

  publish:
    if: github.event_name == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/main&#39;
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
      - uses: docker/build-push-action@v6
        with:
          push: true
</code></pre>

<p>Neither example proves a real repository is safe. They are small enough to make the difference visible.</p>

<h2 id="a-contract-not-a-verdict">A contract, not a verdict</h2>

<p>I wrote a short parser that reads workflow events, job conditions, permissions, secret references, image publishing, and a simple deployment-webhook signal. Its output is a table-shaped JSON document. It does not execute workflow code and it does not contact GitHub.</p>

<p>The hardened workflow produced two separate contracts:</p>

<pre><code class="language-json">{
  &#34;workflow&#34;: &#34;hardened.yml&#34;,
  &#34;job&#34;: &#34;verify&#34;,
  &#34;events&#34;: [&#34;pull_request&#34;, &#34;push&#34;],
  &#34;condition&#34;: &#34;github.event_name == &#39;pull_request&#39;&#34;,
  &#34;capabilities&#34;: {
    &#34;packages_write&#34;: false,
    &#34;publishes_image&#34;: false,
    &#34;uses_secrets&#34;: false
  },
  &#34;findings&#34;: []
}
</code></pre>

<p>The publish job had image-publish capability, but only behind an explicit trusted-main condition and <code>packages: write</code> permission. The unsafe workflow showed the opposite shape:</p>

<pre><code class="language-json">{
  &#34;workflow&#34;: &#34;unsafe-pr-release.yml&#34;,
  &#34;job&#34;: &#34;release&#34;,
  &#34;events&#34;: [&#34;pull_request_target&#34;],
  &#34;condition&#34;: null,
  &#34;capabilities&#34;: {
    &#34;deploy_webhook&#34;: true,
    &#34;publishes_image&#34;: true,
    &#34;uses_secrets&#34;: true
  },
  &#34;findings&#34;: [
    &#34;untrusted PR event reaches credentialed publish/deploy capability&#34;,
    &#34;image publish lacks an explicit trusted-main push guard&#34;
  ]
}
</code></pre>

<p>The parser did not need to know whether the webhook URL was real or whether the registry existed. It only needed to show a reviewer that a pull request event, credentialed steps, and a publish/deploy capability were in the same execution path.</p>

<p>That is the part I do not want buried in YAML.</p>

<h2 id="sha-pinning-answers-a-different-question">SHA pinning answers a different question</h2>

<p>Pinning <code>actions/checkout</code> or <code>docker/build-push-action</code> to immutable commits is worthwhile. GitHub&#39;s roadmap makes the case for locking direct and transitive action dependencies so a workflow executes the code that reviewers actually approved.</p>

<p>But a pinned action can still run in the wrong trust context.</p>

<p>A review has at least two questions:</p>

<table>
<thead>
<tr>
<th>Review question</th>
<th>Evidence to inspect</th>
</tr>
</thead>

<tbody>
<tr>
<td>What action code will run?</td>
<td>Immutable action reference or dependency lock</td>
</tr>

<tr>
<td>Who can cause that code to run with credentials?</td>
<td>Event, condition, permissions, secrets, and publish/deploy steps</td>
</tr>
</tbody>
</table>

<p>The first is dependency integrity. The second is an execution boundary. Both matter, and neither substitutes for the other.</p>

<h2 id="why-an-evaluate-mode-matters">Why an evaluate mode matters</h2>

<p>GitHub describes an evaluate mode for workflow execution rules: show runs that a policy would block before enforcing the policy. That is the right rollout shape.</p>

<p>CI policy changes can stop releases. A rule that bans a trigger or requires a particular actor may catch a real problem, but it can also catch a legitimate release workflow nobody remembered existed. An evidence-first review gives teams somewhere to start:</p>

<pre><code class="language-text">workflow event
  -&gt; job condition
  -&gt; permissions and secrets
  -&gt; image publish or deployment capability
  -&gt; review-required decision
</code></pre>

<p>The goal is not to auto-declare every <code>pull_request_target</code> workflow malicious, or every image publish wrong. There are repositories with carefully designed reasons to use sensitive events. The goal is to make that design legible before someone merges it.</p>

<h2 id="where-this-experiment-stops">Where this experiment stops</h2>

<p>This was a smoke test, not a GitHub Actions emulator or a security assessment. The parser does not resolve reusable workflows, composite-action internals, organization rulesets, environment protection, branch protection, OIDC claims, runner network policy, or runtime expression values. It does not prove that a job can actually obtain a secret.</p>

<p>The test ran in a disposable Docker image with no network, no mounted host directory, a read-only filesystem, a 16 MiB temporary filesystem, all Linux capabilities dropped, <code>no-new-privileges</code>, one CPU, 128 MiB memory, and a PID limit of 64. The image ran as UID <code>65532</code> against two synthetic workflow files. No GitHub credential, repository, workflow run, registry, deployment endpoint, or customer source code was used.</p>

<p>The output JSON SHA-256 was:</p>

<pre><code class="language-text">665061732b6ec86dca49c8fbfef2dbeff6d809a9e2c53195ae252ca4b5d169b7
</code></pre>

<p>The useful next step is not another all-purpose CI scanner. It is a small review companion that can turn a workflow diff into an execution-contract diff: which event, actor, permission, secret, registry, or deployment surface changed, and which part still needs a human decision.</p>

<p>That would extend the same review habit I want from GitOps manifests and Gateway migrations. Treat the artifact as evidence. Make the operational consequence visible. Leave the decision with the person who owns the risk.</p>

<p><strong>Sources:</strong> <a href="https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/">GitHub Actions 2026 security roadmap</a> · <a href="https://docs.github.com/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions">GitHub Actions security hardening guidance</a> · <a href="https://heyimusa.blog/i-built-a-security-impact-diff-for-gitops">I built a security-impact diff for GitOps</a> · <a href="https://heyimusa.blog/a-gateway-migration-is-a-routing-change">A Gateway migration is a routing change, not a YAML conversion</a></p>


]]></content:encoded>
      <guid>https://heyimusa.blog/a-github-actions-workflow-is-a-deployment-boundary</guid>
      <pubDate>Mon, 27 Jul 2026 15:27:50 +0000</pubDate>
    </item>
    <item>
      <title>I built a security-impact diff for GitOps</title>
      <link>https://heyimusa.blog/i-built-a-security-impact-diff-for-gitops</link>
      <description>&lt;![CDATA[Before and after rendered manifests flow into a focused security-impact review&#xA;&#xA;Original diagram for this build note. It shows the tool’s review model, not a production cluster topology.&#xA;&#xA;A Kubernetes pull request can look harmless when the visible change is short: a new RoleBinding, one securityContext field, a route rule, or a service type. The difficult part is that these changes rarely read like the security event they create.&#xA;&#xA;A Role can gain access to Secrets. A workload can begin sharing a host network namespace. A route can make a new hostname reachable. None of that has to look dramatic in a line-by-line diff.&#xA;&#xA;I wanted a small tool that starts from the artifact GitOps teams already review: rendered manifests before and after a change. That became kube-blast-radius, an offline CLI that explains the security-relevant capability or surface added by that change.&#xA;&#xA;!--more--&#xA;&#xA;The input is the delivery artifact, not another policy file&#xA;&#xA;The command compares two already-rendered states. It can also render Kustomize directories or Helm charts first.&#xA;&#xA;kube-blast-radius diff \&#xA;  --before rendered/main.yaml \&#xA;  --after rendered/pr.yaml&#xA;&#xA;For a simple fixture, a Role gained Secret access while a Deployment enabled hostNetwork and added a privileged container. The CLI reported:&#xA;&#xA;HIGH  Role/payments/checkout: RBAC rule newly grants access to secrets&#xA;      (RBACSECRETACCESSADDED)&#xA;&#xA;HIGH  Deployment/payments/checkout: workload newly enables hostNetwork&#xA;      (WORKLOADHOSTNETWORKADDED)&#xA;&#xA;HIGH  Deployment/payments/checkout: workload newly adds a privileged container&#xA;      (WORKLOADPRIVILEGEDADDED)&#xA;&#xA;The intent is not to decide whether every finding is wrong. A privileged DaemonSet or a public route can be intentional. The point is to turn a subtle manifest change into a question a reviewer can answer deliberately.&#xA;&#xA;I treated false assurance as the main bug&#xA;&#xA;The first version found the obvious cases quickly. The more important work was finding ways it could say too little.&#xA;&#xA;An independent review caught a few examples. A ClusterRole referenced by a namespace-scoped RoleBinding was initially easy to miss because ClusterRoles do not have a namespace. A kind: List wrapper could hide objects from a naive document parser. And an omitted ServiceAccount and an explicit default ServiceAccount should mean the same thing, not produce a scary but meaningless finding.&#xA;&#xA;Those became regression tests, not release notes:&#xA;&#xA;ClusterRole + RoleBinding -  effective Secret access&#xA;kind: List              -  expanded and analyzed&#xA;missing identity        -  analysis error, not silent skip&#xA;implicit default        -  normalized before comparison&#xA;&#xA;The tool also reports unsupported resource kinds as information instead of quietly declaring the change clean. Its text output always carries the same caveat: enabled checks are not a complete security assessment.&#xA;&#xA;Render first, then compare&#xA;&#xA;Raw YAML is useful, but many GitOps changes live in overlays and values files. The tool supports three modes:&#xA;&#xA;raw        YAML manifest stream&#xA;kustomize  kubectl kustomize directory&#xA;helm       helm template chart with before/after values&#xA;&#xA;The render commands use argument vectors rather than a shell, have a fixed timeout, and do not load kubeconfig or talk to a Kubernetes cluster. That is a deliberate boundary. Rendering still means trusting the chart or overlay source, so untrusted inputs belong in an isolated runner.&#xA;&#xA;I tested the renderers inside a Docker container with a read-only filesystem, a non-root user, all Linux capabilities dropped, no-new-privileges, and read-only mounted fixtures. Kustomize caught a newly enabled hostNetwork; Helm caught a values change that made a container privileged.&#xA;&#xA;A real GitOps artifact changed the scope&#xA;&#xA;The useful test was a read-only render of a detached Traefik route-review artifact from a GitOps repository. I exported only that tracked review directory at the commit that introduced it, rendered it with Kustomize, and compared it with an empty prior state. No cluster, DNS provider, Secret values, or live route was accessed.&#xA;&#xA;The first run did not understand Traefik IngressRoute resources. That was the right result to take seriously: an “unsupported kind” message is better than pretending a route addition has no security meaning.&#xA;&#xA;I added focused support for Traefik route matches. The next run marked the added desktop and mobile route rules as high-severity exposure changes. It did not call them malicious. It made their external-surface effect visible in the review output.&#xA;&#xA;What the first release covers&#xA;&#xA;v0.1.0 looks for changes including:&#xA;&#xA;bound RBAC access to Secrets, wildcards, escalation verbs, and broader resource-name scope;&#xA;new bindings to roles that already grant Secret access;&#xA;privileged containers, host namespaces, hostPath volumes, added Linux capabilities, UID 0, and weakened container hardening;&#xA;Service external exposure, Kubernetes Ingress hosts and paths, removed NetworkPolicies, and Traefik IngressRoute rules.&#xA;&#xA;It returns text for a reviewer and JSON for CI. High findings exit with code 1; malformed manifests and renderer failures exit with 2.&#xA;&#xA;What it does not claim&#xA;&#xA;This is not a cluster security platform. It does not calculate full NetworkPolicy reachability, inspect cloud IAM, query a live cluster, read Secret values, or certify compliance.&#xA;&#xA;That limitation is part of the product. A GitOps diff tool should be trusted for the specific questions it can answer, not for imaginary coverage.&#xA;&#xA;The source, release binary, and checksum are public:&#xA;&#xA;kube-blast-radius source&#xA;v0.1.0 release&#xA;&#xA;---&#xA;&#xA;Test notes: I built and tested the CLI locally and in disposable Docker containers. The hardened renderer test used a read-only filesystem, non-root UID 65532, dropped Linux capabilities, no-new-privileges, and read-only mounted synthetic fixtures. The GitOps validation was a read-only render of a detached review artifact; no cluster, Secret value, DNS provider, or live route was accessed. The examples are sanitized.&#xA;&#xA;Related work: Security and regulated operations case study · A rollout needs a return address&#xA;&#xA;!-- taxonomy: #topiccloudsecurity #topicgitopsdelivery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="/img/articles/kube-blast-radius-security-impact-diff.svg" alt="Before and after rendered manifests flow into a focused security-impact review"></p>

<p><em>Original diagram for this build note. It shows the tool’s review model, not a production cluster topology.</em></p>

<p>A Kubernetes pull request can look harmless when the visible change is short: a new RoleBinding, one <code>securityContext</code> field, a route rule, or a service type. The difficult part is that these changes rarely read like the security event they create.</p>

<p>A Role can gain access to Secrets. A workload can begin sharing a host network namespace. A route can make a new hostname reachable. None of that has to look dramatic in a line-by-line diff.</p>

<p>I wanted a small tool that starts from the artifact GitOps teams already review: rendered manifests before and after a change. That became <a href="https://github.com/heyimusa/kube-blast-radius">kube-blast-radius</a>, an offline CLI that explains the security-relevant capability or surface added by that change.</p>



<h2 id="the-input-is-the-delivery-artifact-not-another-policy-file">The input is the delivery artifact, not another policy file</h2>

<p>The command compares two already-rendered states. It can also render Kustomize directories or Helm charts first.</p>

<pre><code class="language-bash">kube-blast-radius diff \
  --before rendered/main.yaml \
  --after rendered/pr.yaml
</code></pre>

<p>For a simple fixture, a Role gained Secret access while a Deployment enabled <code>hostNetwork</code> and added a privileged container. The CLI reported:</p>

<pre><code class="language-text">HIGH  Role/payments/checkout: RBAC rule newly grants access to secrets
      (RBAC_SECRET_ACCESS_ADDED)

HIGH  Deployment/payments/checkout: workload newly enables hostNetwork
      (WORKLOAD_HOST_NETWORK_ADDED)

HIGH  Deployment/payments/checkout: workload newly adds a privileged container
      (WORKLOAD_PRIVILEGED_ADDED)
</code></pre>

<p>The intent is not to decide whether every finding is wrong. A privileged DaemonSet or a public route can be intentional. The point is to turn a subtle manifest change into a question a reviewer can answer deliberately.</p>

<h2 id="i-treated-false-assurance-as-the-main-bug">I treated false assurance as the main bug</h2>

<p>The first version found the obvious cases quickly. The more important work was finding ways it could say too little.</p>

<p>An independent review caught a few examples. A ClusterRole referenced by a namespace-scoped RoleBinding was initially easy to miss because ClusterRoles do not have a namespace. A <code>kind: List</code> wrapper could hide objects from a naive document parser. And an omitted ServiceAccount and an explicit <code>default</code> ServiceAccount should mean the same thing, not produce a scary but meaningless finding.</p>

<p>Those became regression tests, not release notes:</p>

<pre><code class="language-text">ClusterRole + RoleBinding -&gt; effective Secret access
kind: List              -&gt; expanded and analyzed
missing identity        -&gt; analysis error, not silent skip
implicit default        -&gt; normalized before comparison
</code></pre>

<p>The tool also reports unsupported resource kinds as information instead of quietly declaring the change clean. Its text output always carries the same caveat: enabled checks are not a complete security assessment.</p>

<h2 id="render-first-then-compare">Render first, then compare</h2>

<p>Raw YAML is useful, but many GitOps changes live in overlays and values files. The tool supports three modes:</p>

<pre><code class="language-text">raw        YAML manifest stream
kustomize  kubectl kustomize &lt;directory&gt;
helm       helm template &lt;chart&gt; with before/after values
</code></pre>

<p>The render commands use argument vectors rather than a shell, have a fixed timeout, and do not load kubeconfig or talk to a Kubernetes cluster. That is a deliberate boundary. Rendering still means trusting the chart or overlay source, so untrusted inputs belong in an isolated runner.</p>

<p>I tested the renderers inside a Docker container with a read-only filesystem, a non-root user, all Linux capabilities dropped, <code>no-new-privileges</code>, and read-only mounted fixtures. Kustomize caught a newly enabled <code>hostNetwork</code>; Helm caught a values change that made a container privileged.</p>

<h2 id="a-real-gitops-artifact-changed-the-scope">A real GitOps artifact changed the scope</h2>

<p>The useful test was a read-only render of a detached Traefik route-review artifact from a GitOps repository. I exported only that tracked review directory at the commit that introduced it, rendered it with Kustomize, and compared it with an empty prior state. No cluster, DNS provider, Secret values, or live route was accessed.</p>

<p>The first run did not understand Traefik <code>IngressRoute</code> resources. That was the right result to take seriously: an “unsupported kind” message is better than pretending a route addition has no security meaning.</p>

<p>I added focused support for Traefik route matches. The next run marked the added desktop and mobile route rules as high-severity exposure changes. It did not call them malicious. It made their external-surface effect visible in the review output.</p>

<h2 id="what-the-first-release-covers">What the first release covers</h2>

<p><code>v0.1.0</code> looks for changes including:</p>
<ul><li>bound RBAC access to Secrets, wildcards, escalation verbs, and broader resource-name scope;</li>
<li>new bindings to roles that already grant Secret access;</li>
<li>privileged containers, host namespaces, hostPath volumes, added Linux capabilities, UID 0, and weakened container hardening;</li>
<li>Service external exposure, Kubernetes Ingress hosts and paths, removed NetworkPolicies, and Traefik <code>IngressRoute</code> rules.</li></ul>

<p>It returns text for a reviewer and JSON for CI. High findings exit with code <code>1</code>; malformed manifests and renderer failures exit with <code>2</code>.</p>

<h2 id="what-it-does-not-claim">What it does not claim</h2>

<p>This is not a cluster security platform. It does not calculate full NetworkPolicy reachability, inspect cloud IAM, query a live cluster, read Secret values, or certify compliance.</p>

<p>That limitation is part of the product. A GitOps diff tool should be trusted for the specific questions it can answer, not for imaginary coverage.</p>

<p>The source, release binary, and checksum are public:</p>
<ul><li><a href="https://github.com/heyimusa/kube-blast-radius">kube-blast-radius source</a></li>
<li><a href="https://github.com/heyimusa/kube-blast-radius/releases/tag/v0.1.0">v0.1.0 release</a></li></ul>

<hr>

<p><strong>Test notes:</strong> I built and tested the CLI locally and in disposable Docker containers. The hardened renderer test used a read-only filesystem, non-root UID <code>65532</code>, dropped Linux capabilities, <code>no-new-privileges</code>, and read-only mounted synthetic fixtures. The GitOps validation was a read-only render of a detached review artifact; no cluster, Secret value, DNS provider, or live route was accessed. The examples are sanitized.</p>

<p><strong>Related work:</strong> <a href="/portfolio/work/#work">Security and regulated operations case study</a> · <a href="/a-rollout-needs-a-return-address">A rollout needs a return address</a></p>


]]></content:encoded>
      <guid>https://heyimusa.blog/i-built-a-security-impact-diff-for-gitops</guid>
      <pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>I built a small linter for health checks that disagree with rollouts</title>
      <link>https://heyimusa.blog/i-built-a-small-linter-for-health-checks</link>
      <description>&lt;![CDATA[A declared readiness rollout gate disagrees with a Docker Compose healthcheck that calls only liveness&#xA;&#xA;Original diagram for this Docker-only build note. It shows a configuration disagreement, not a production incident.&#xA;&#xA;I kept tripping over the same sentence while writing about health checks: “we gate the rollout on readiness.”&#xA;&#xA;It sounds reassuring. Then you open the deployment file and find a healthcheck that calls /healthz, because that endpoint was easy to add and it returns 200 as long as the process is alive.&#xA;&#xA;Neither file is necessarily wrong on its own. Together, they can tell two different stories.&#xA;&#xA;!--more--&#xA;&#xA;So I made a small CLI called probe-contract. It compares a declared health contract with a Docker Compose file and reports the disagreements that are easy to miss in review.&#xA;&#xA;The first release is deliberately small. It does not call a live endpoint. It does not attempt to infer whether a checkout flow, queue, or database is truly healthy. It reads configuration and asks a narrower question: does the probe in the deployment file support the health assumption you wrote down?&#xA;&#xA;The smallest useful contract&#xA;&#xA;The tool takes a tiny YAML file alongside Compose:&#xA;&#xA;services:&#xA;  api:&#xA;    liveness: /healthz&#xA;    readiness: /readyz&#xA;    userpath: /checkout&#xA;    rolloutgate: readiness&#xA;&#xA;Then it compares that declaration with the Compose healthcheck.&#xA;&#xA;For the mismatch case, the contract said readiness should gate the rollout, while the Compose probe called only /healthz:&#xA;&#xA;WARNING  api: rolloutgate is readiness, but Compose healthcheck does not reference /readyz (READINESSNOTPROBED)&#xA;&#xA;That warning is the whole point of the first version. It does not prove /readyz is a good readiness endpoint. It makes the disagreement visible before somebody treats a green container as proof that a rollout is safe.&#xA;&#xA;I tested the tool in a container too&#xA;&#xA;I ran the released CLI against its fixtures inside a Docker container with a read-only filesystem, a non-root user, dropped capabilities, and no-new-privileges.&#xA;&#xA;The valid fixture produced empty JSON:&#xA;&#xA;{&#xA;  &#34;diagnostics&#34;: []&#xA;}&#xA;&#xA;A Compose file with its healthcheck explicitly disabled produced an error and exit code 1:&#xA;&#xA;ERROR  api: service has no Compose healthcheck (HEALTHCHECKMISSING)&#xA;&#xA;That second case mattered. An early review of the tool found that healthcheck: { disable: true } could look like an active healthcheck to a naive YAML parser. The release now treats both disable: true and Docker’s test: [NONE] form as missing checks.&#xA;&#xA;The review also caught a less obvious false pass: /ready should not match /readyz just because one string contains the other. That is fixed too. A health contract is already an approximation; the checker should not add accidental ambiguity on top.&#xA;&#xA;What it checks today&#xA;&#xA;probe-contract v0.1.0 checks a few things and stops there:&#xA;&#xA;a service declared in the contract is present in Compose;&#xA;that service has an active healthcheck;&#xA;a readiness rollout gate actually probes the declared readiness path;&#xA;liveness and readiness are not declared as the same endpoint by accident;&#xA;interval, timeout, and retries are present on the healthcheck.&#xA;&#xA;It emits human-readable text by default and JSON with --format json, which is enough to start using it in CI without making every warning a release blocker.&#xA;&#xA;What it intentionally does not do&#xA;&#xA;I do not want this to become another linter that promises too much.&#xA;&#xA;It does not inspect Kubernetes manifests yet. It does not send traffic to a live service. It does not infer a business transaction from a URL. And it cannot tell whether a dependency should be part of readiness for a particular application.&#xA;&#xA;Those choices belong to the team running the service. The tool only asks them to make the choice explicit, then checks whether the Compose file agrees.&#xA;&#xA;That scope is small enough to be useful. It is also small enough that a reviewer can understand what a warning means without trusting a black box.&#xA;&#xA;Release and next steps&#xA;&#xA;The project is public under MIT and includes a Linux amd64 binary with a checksum:&#xA;&#xA;Source and README&#xA;v0.1.0 release&#xA;&#xA;The next likely steps are Kubernetes manifest support, SARIF output, and a GitHub Action. I am deliberately not calling those features until the Compose contract is useful enough to earn them.&#xA;&#xA;---&#xA;&#xA;Test notes: I built and ran probe-contract v0.1.0 only in Docker for this note. The demonstration container used a read-only filesystem, non-root UID 65532, dropped Linux capabilities, no-new-privileges, and read-only mounted fixture files. The results are configuration checks against local fixtures, not a benchmark or a production deployment.&#xA;&#xA;Related work: Delivery systems case study · I made a healthy service page on purpose&#xA;&#xA;!-- taxonomy: #topicplatformengineering #topicgitopsdelivery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="/img/articles/probe-contract-makes-health-assumptions-visible.svg" alt="A declared readiness rollout gate disagrees with a Docker Compose healthcheck that calls only liveness"></p>

<p><em>Original diagram for this Docker-only build note. It shows a configuration disagreement, not a production incident.</em></p>

<p>I kept tripping over the same sentence while writing about health checks: “we gate the rollout on readiness.”</p>

<p>It sounds reassuring. Then you open the deployment file and find a healthcheck that calls <code>/healthz</code>, because that endpoint was easy to add and it returns <code>200</code> as long as the process is alive.</p>

<p>Neither file is necessarily wrong on its own. Together, they can tell two different stories.</p>



<p>So I made a small CLI called <a href="https://github.com/heyimusa/probe-contract"><code>probe-contract</code></a>. It compares a declared health contract with a Docker Compose file and reports the disagreements that are easy to miss in review.</p>

<p>The first release is deliberately small. It does not call a live endpoint. It does not attempt to infer whether a checkout flow, queue, or database is truly healthy. It reads configuration and asks a narrower question: does the probe in the deployment file support the health assumption you wrote down?</p>

<h2 id="the-smallest-useful-contract">The smallest useful contract</h2>

<p>The tool takes a tiny YAML file alongside Compose:</p>

<pre><code class="language-yaml">services:
  api:
    liveness: /healthz
    readiness: /readyz
    user_path: /checkout
    rollout_gate: readiness
</code></pre>

<p>Then it compares that declaration with the Compose healthcheck.</p>

<p>For the mismatch case, the contract said readiness should gate the rollout, while the Compose probe called only <code>/healthz</code>:</p>

<pre><code class="language-text">WARNING  api: rollout_gate is readiness, but Compose healthcheck does not reference /readyz (READINESS_NOT_PROBED)
</code></pre>

<p>That warning is the whole point of the first version. It does not prove <code>/readyz</code> is a good readiness endpoint. It makes the disagreement visible before somebody treats a green container as proof that a rollout is safe.</p>

<h2 id="i-tested-the-tool-in-a-container-too">I tested the tool in a container too</h2>

<p>I ran the released CLI against its fixtures inside a Docker container with a read-only filesystem, a non-root user, dropped capabilities, and <code>no-new-privileges</code>.</p>

<p>The valid fixture produced empty JSON:</p>

<pre><code class="language-json">{
  &#34;diagnostics&#34;: []
}
</code></pre>

<p>A Compose file with its healthcheck explicitly disabled produced an error and exit code <code>1</code>:</p>

<pre><code class="language-text">ERROR  api: service has no Compose healthcheck (HEALTHCHECK_MISSING)
</code></pre>

<p>That second case mattered. An early review of the tool found that <code>healthcheck: { disable: true }</code> could look like an active healthcheck to a naive YAML parser. The release now treats both <code>disable: true</code> and Docker’s <code>test: [NONE]</code> form as missing checks.</p>

<p>The review also caught a less obvious false pass: <code>/ready</code> should not match <code>/readyz</code> just because one string contains the other. That is fixed too. A health contract is already an approximation; the checker should not add accidental ambiguity on top.</p>

<h2 id="what-it-checks-today">What it checks today</h2>

<p><code>probe-contract v0.1.0</code> checks a few things and stops there:</p>
<ul><li>a service declared in the contract is present in Compose;</li>
<li>that service has an active healthcheck;</li>
<li>a readiness rollout gate actually probes the declared readiness path;</li>
<li>liveness and readiness are not declared as the same endpoint by accident;</li>
<li>interval, timeout, and retries are present on the healthcheck.</li></ul>

<p>It emits human-readable text by default and JSON with <code>--format json</code>, which is enough to start using it in CI without making every warning a release blocker.</p>

<h2 id="what-it-intentionally-does-not-do">What it intentionally does not do</h2>

<p>I do not want this to become another linter that promises too much.</p>

<p>It does not inspect Kubernetes manifests yet. It does not send traffic to a live service. It does not infer a business transaction from a URL. And it cannot tell whether a dependency should be part of readiness for a particular application.</p>

<p>Those choices belong to the team running the service. The tool only asks them to make the choice explicit, then checks whether the Compose file agrees.</p>

<p>That scope is small enough to be useful. It is also small enough that a reviewer can understand what a warning means without trusting a black box.</p>

<h2 id="release-and-next-steps">Release and next steps</h2>

<p>The project is public under MIT and includes a Linux amd64 binary with a checksum:</p>
<ul><li><a href="https://github.com/heyimusa/probe-contract">Source and README</a></li>
<li><a href="https://github.com/heyimusa/probe-contract/releases/tag/v0.1.0">v0.1.0 release</a></li></ul>

<p>The next likely steps are Kubernetes manifest support, SARIF output, and a GitHub Action. I am deliberately not calling those features until the Compose contract is useful enough to earn them.</p>

<hr>

<p><strong>Test notes:</strong> I built and ran <code>probe-contract v0.1.0</code> only in Docker for this note. The demonstration container used a read-only filesystem, non-root UID <code>65532</code>, dropped Linux capabilities, <code>no-new-privileges</code>, and read-only mounted fixture files. The results are configuration checks against local fixtures, not a benchmark or a production deployment.</p>

<p><strong>Related work:</strong> <a href="/portfolio/work/#delivery-systems">Delivery systems case study</a> · <a href="/i-made-a-healthy-service-page-on-purpose">I made a healthy service page on purpose</a></p>


]]></content:encoded>
      <guid>https://heyimusa.blog/i-built-a-small-linter-for-health-checks</guid>
      <pubDate>Mon, 20 Jul 2026 13:26:02 +0000</pubDate>
    </item>
    <item>
      <title>A rollout needs a return address</title>
      <link>https://heyimusa.blog/a-rollout-needs-a-return-address</link>
      <description>&lt;![CDATA[A small declarative rollout and rollback experiment&#xA;&#xA;Original diagram for this note. It describes a Docker-only simulated release flow, not a production deployment or benchmark.&#xA;&#xA;I do not trust a deployment plan until I can explain what happens when the new version is the problem.&#xA;&#xA;!--more--&#xA;&#xA;That sounds obvious. It is still easy to build a delivery process around the happy path: create an image, update a manifest, watch the rollout, call it done. The awkward part starts when a release looks healthy enough to leave the pipeline but is not healthy enough to keep.&#xA;&#xA;To keep this small, I ran a toy release flow in a disposable Docker container. There was no host change, cluster, registry, or real service. The container held a desired-state file for a fictional checkout service, plus a saved copy of the known-good release.&#xA;&#xA;apply: checkout:1.4.3&#xA;healthcheck: /readyz -  FAIL (simulated)&#xA;rollback: checkout:1.4.2&#xA;healthcheck: /readyz -  OK (simulated)&#xA;finaldesiredstate: image=checkout:1.4.2 replicas=3&#xA;&#xA;The experiment is intentionally boring. That is the point. A rollback should not require somebody to reconstruct the previous state from memory while a production graph turns red.&#xA;&#xA;The release needs a return address&#xA;&#xA;A deployment is a state transition. The candidate release is only one half of that transition; the other half is the state you can return to when the candidate fails.&#xA;&#xA;In the tiny experiment, that state was just a saved file:&#xA;&#xA;image=checkout:1.4.2&#xA;replicas=3&#xA;&#xA;Real systems are less neat. There may be configuration changes, schema compatibility, feature flags, asynchronous workers, or traffic shifts. But the basic question does not change: what exact state are we restoring, and can the deployment system express it?&#xA;&#xA;If the answer is &#34;we will figure it out,&#34; the rollback plan is not really a plan.&#xA;&#xA;Rollback is not an apology button&#xA;&#xA;People often talk about rollback as if it is the opposite of deployment. It is not. It is another deployment, with the same need for identity, evidence, and verification.&#xA;&#xA;A useful rollback path has at least three properties:&#xA;&#xA;The previous artifact or desired state is identifiable.&#xA;The path to apply it is known before the incident.&#xA;The system has a signal that says the restored version is actually healthy.&#xA;&#xA;The third item gets skipped surprisingly often. Reverting an image tag is not proof that the service recovered. It only proves that the deployment controller accepted another instruction.&#xA;&#xA;Why declarative state helps&#xA;&#xA;This is where GitOps and other declarative delivery patterns earn their keep. They make the intended state visible. They also make a reversal more concrete: restore a reviewed revision, reconcile it, and watch the same health signals that justified the rollout.&#xA;&#xA;That does not make every rollback safe. Database migrations can make a simple reversal impossible. A downstream dependency may have changed underneath you. A feature flag may be the safer first lever. Declarative state is not magic; it just removes one common source of panic: having to guess which version and configuration were running before the change.&#xA;&#xA;The part I would test next&#xA;&#xA;The toy flow did not cover the hard cases. It did not include a database, traffic management, or an actual Kubernetes controller. It only checked the shape of the idea: a failed health check should lead to a named previous state, then to a second health check.&#xA;&#xA;The next useful step is to run the same exercise against a non-critical service in an isolated environment:&#xA;&#xA;deploy a known candidate;&#xA;deliberately fail a readiness condition;&#xA;reconcile the previous revision;&#xA;verify the restored service through the same route and alert signal users depend on.&#xA;&#xA;If that feels cumbersome in a test environment, it will feel worse during an incident.&#xA;&#xA;---&#xA;&#xA;Test notes: This note is based on a disposable Docker container limited to 0.25 CPU and 128 MiB memory. It simulated a declarative checkout release changing from 1.4.2 to 1.4.3, a failed readiness check, and restoration to 1.4.2. No production infrastructure, repository, cluster, registry, or database was accessed.&#xA;&#xA;Related work: Turning deployments into a repeatable platform capability&#xA;&#xA;!-- taxonomy: #topicgitopsdelivery #topicincidentrecovery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="/img/articles/rollbacks-need-a-place-to-land.svg" alt="A small declarative rollout and rollback experiment"></p>

<p><em>Original diagram for this note. It describes a Docker-only simulated release flow, not a production deployment or benchmark.</em></p>

<p>I do not trust a deployment plan until I can explain what happens when the new version is the problem.</p>



<p>That sounds obvious. It is still easy to build a delivery process around the happy path: create an image, update a manifest, watch the rollout, call it done. The awkward part starts when a release looks healthy enough to leave the pipeline but is not healthy enough to keep.</p>

<p>To keep this small, I ran a toy release flow in a disposable Docker container. There was no host change, cluster, registry, or real service. The container held a desired-state file for a fictional <code>checkout</code> service, plus a saved copy of the known-good release.</p>

<pre><code class="language-text">apply: checkout:1.4.3
healthcheck: /readyz -&gt; FAIL (simulated)
rollback: checkout:1.4.2
healthcheck: /readyz -&gt; OK (simulated)
final_desired_state: image=checkout:1.4.2 replicas=3
</code></pre>

<p>The experiment is intentionally boring. That is the point. A rollback should not require somebody to reconstruct the previous state from memory while a production graph turns red.</p>

<h2 id="the-release-needs-a-return-address">The release needs a return address</h2>

<p>A deployment is a state transition. The candidate release is only one half of that transition; the other half is the state you can return to when the candidate fails.</p>

<p>In the tiny experiment, that state was just a saved file:</p>

<pre><code class="language-text">image=checkout:1.4.2
replicas=3
</code></pre>

<p>Real systems are less neat. There may be configuration changes, schema compatibility, feature flags, asynchronous workers, or traffic shifts. But the basic question does not change: <strong>what exact state are we restoring, and can the deployment system express it?</strong></p>

<p>If the answer is “we will figure it out,” the rollback plan is not really a plan.</p>

<h2 id="rollback-is-not-an-apology-button">Rollback is not an apology button</h2>

<p>People often talk about rollback as if it is the opposite of deployment. It is not. It is another deployment, with the same need for identity, evidence, and verification.</p>

<p>A useful rollback path has at least three properties:</p>
<ul><li>The previous artifact or desired state is identifiable.</li>
<li>The path to apply it is known before the incident.</li>
<li>The system has a signal that says the restored version is actually healthy.</li></ul>

<p>The third item gets skipped surprisingly often. Reverting an image tag is not proof that the service recovered. It only proves that the deployment controller accepted another instruction.</p>

<h2 id="why-declarative-state-helps">Why declarative state helps</h2>

<p>This is where GitOps and other declarative delivery patterns earn their keep. They make the intended state visible. They also make a reversal more concrete: restore a reviewed revision, reconcile it, and watch the same health signals that justified the rollout.</p>

<p>That does not make every rollback safe. Database migrations can make a simple reversal impossible. A downstream dependency may have changed underneath you. A feature flag may be the safer first lever. Declarative state is not magic; it just removes one common source of panic: having to guess which version and configuration were running before the change.</p>

<h2 id="the-part-i-would-test-next">The part I would test next</h2>

<p>The toy flow did not cover the hard cases. It did not include a database, traffic management, or an actual Kubernetes controller. It only checked the shape of the idea: a failed health check should lead to a named previous state, then to a second health check.</p>

<p>The next useful step is to run the same exercise against a non-critical service in an isolated environment:</p>
<ol><li>deploy a known candidate;</li>
<li>deliberately fail a readiness condition;</li>
<li>reconcile the previous revision;</li>
<li>verify the restored service through the same route and alert signal users depend on.</li></ol>

<p>If that feels cumbersome in a test environment, it will feel worse during an incident.</p>

<hr>

<p><strong>Test notes:</strong> This note is based on a disposable Docker container limited to 0.25 CPU and 128 MiB memory. It simulated a declarative <code>checkout</code> release changing from <code>1.4.2</code> to <code>1.4.3</code>, a failed readiness check, and restoration to <code>1.4.2</code>. No production infrastructure, repository, cluster, registry, or database was accessed.</p>

<p><strong>Related work:</strong> <a href="/portfolio/work/#work">Turning deployments into a repeatable platform capability</a></p>


]]></content:encoded>
      <guid>https://heyimusa.blog/a-rollout-needs-a-return-address</guid>
      <pubDate>Sun, 19 Jul 2026 17:31:09 +0000</pubDate>
    </item>
    <item>
      <title>I tried code-review-graph in Docker. Useful, but don’t swallow the 528x number whole.</title>
      <link>https://heyimusa.blog/github-trending-context-engineering-ai-coding</link>
      <description>&lt;![CDATA[Diagram comparing whole-corpus reading with graph queries in the code-review-graph benchmark&#xA;&#xA;Diagram from code-review-graph, commit 6a1ee1c · MIT licensed · kept locally so this post does not depend on a hotlink.&#xA;&#xA;I saw code-review-graph climbing GitHub Trending and nearly wrote the usual post about why AI coding needs better context. Then I stopped. That sentence is true, but it is also a pretty easy way to avoid trying the tool.&#xA;&#xA;So I did a small smoke test first.&#xA;&#xA;!--more--&#xA;&#xA;I ran it inside a Docker container, not on the host. The container had one CPU and 1.5 GiB of memory. Inside it, I made a tiny Python repository: authentication, a login endpoint, and one test. Then I installed code-review-graph 2.3.7 and built the graph.&#xA;&#xA;Full build: 3 files, 6 nodes, 9 edges&#xA;Nodes: 6&#xA;Edges: 9&#xA;Files: 3&#xA;Languages: python&#xA;&#xA;Nothing dramatic happened, which was reassuring. The tool connected api.py, auth.py, and the test in the way I expected. That only proves it can build a graph for a tiny repository. It does not prove that it will make pull request reviews better on a real codebase. Still, the smallest claim held up without giving the tool access to the machine running the blog.&#xA;&#xA;What the tool is trying to do&#xA;&#xA;code-review-graph uses Tree-sitter to map a repository. The intended payoff is simple: when an agent reviews a change, it should not need to read an entire repository just to find the few files that matter.&#xA;&#xA;That sounds obvious until a change touches an API, a worker, a deployment chart, and an alert rule. At that point the problem is not a lack of context window. It is finding the context that is actually connected to the change.&#xA;&#xA;For a three-file repository, I would still open the files myself. There is no prize for adding a graph database to a problem that rg can solve in ten seconds. The case gets more interesting once the repository is large enough that a change has consequences outside the diff.&#xA;&#xA;About the 528x number&#xA;&#xA;The project README leads with an eye-catching result: up to 528x fewer tokens. That number is real, but it is the best case from the project&#39;s FastAPI benchmark.&#xA;&#xA;The more useful number is their reported median of roughly 82x per question across six repositories. Even that needs context. The comparison is whole-corpus reading versus a graph query. A good engineer does not normally paste an entire repository into an LLM and hope for the best, so this is an upper-bound baseline, not a normal day at work.&#xA;&#xA;I like that the README says as much. Too many AI tool pages put the caveat in a footnote, if they include one at all.&#xA;&#xA;What I liked&#xA;&#xA;The project does not try to make the model magically smarter. It tries to give the model less irrelevant material. That is a healthier problem to work on.&#xA;&#xA;It also has a plausible route into real workflows: a CLI, MCP support, incremental updates, and a GitHub Action. A graph that looks good in a demo is easy. Keeping one useful after a repository changes every day is the hard part.&#xA;&#xA;What I still do not know&#xA;&#xA;This smoke test was deliberately small. It did not answer the questions I would ask before adopting it on a serious repository:&#xA;&#xA;How long does the first build take on a monorepo?&#xA;How large does the graph database become?&#xA;Does retrieval surface the files an experienced reviewer would open anyway?&#xA;Does it save time, or does it add another moving part to maintain?&#xA;&#xA;I would not give a trending tool write access to a repository on the strength of a screenshot and a benchmark chart. A read-only container, a non-critical repository, and outputs that a human can inspect are enough for a first pass.&#xA;&#xA;If you want to try it&#xA;&#xA;Start with one repository you know well. Measure the first build and an incremental update. Then compare the suggested files with the files you would have opened during a normal review.&#xA;&#xA;If the answers line up, keep going. If they do not, you have still learned something useful and avoided adding another MCP server just because it was trending.&#xA;&#xA;---&#xA;&#xA;Test notes: code-review-graph 2.3.7 ran in an isolated Docker container with Python 3.12, one CPU, and 1.5 GiB of memory. This was a graph-build smoke test on a small Python repository. I did not independently reproduce the project&#39;s token benchmark.&#xA;&#xA;Sources: code-review-graph · benchmark methodology · GitHub Trending&#xA;&#xA;!-- taxonomy: #topicgitopsdelivery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="/img/articles/code-review-graph-token-problem.png" alt="Diagram comparing whole-corpus reading with graph queries in the code-review-graph benchmark"></p>

<p><em>Diagram from <a href="https://github.com/tirth8205/code-review-graph">code-review-graph</a>, commit <code>6a1ee1c</code> · MIT licensed · kept locally so this post does not depend on a hotlink.</em></p>

<p>I saw <code>code-review-graph</code> climbing GitHub Trending and nearly wrote the usual post about why AI coding needs better context. Then I stopped. That sentence is true, but it is also a pretty easy way to avoid trying the tool.</p>

<p>So I did a small smoke test first.</p>



<p>I ran it inside a Docker container, not on the host. The container had one CPU and 1.5 GiB of memory. Inside it, I made a tiny Python repository: authentication, a login endpoint, and one test. Then I installed <code>code-review-graph 2.3.7</code> and built the graph.</p>

<pre><code class="language-text">Full build: 3 files, 6 nodes, 9 edges
Nodes: 6
Edges: 9
Files: 3
Languages: python
</code></pre>

<p>Nothing dramatic happened, which was reassuring. The tool connected <code>api.py</code>, <code>auth.py</code>, and the test in the way I expected. That only proves it can build a graph for a tiny repository. It does not prove that it will make pull request reviews better on a real codebase. Still, the smallest claim held up without giving the tool access to the machine running the blog.</p>

<h2 id="what-the-tool-is-trying-to-do">What the tool is trying to do</h2>

<p><code>code-review-graph</code> uses Tree-sitter to map a repository. The intended payoff is simple: when an agent reviews a change, it should not need to read an entire repository just to find the few files that matter.</p>

<p>That sounds obvious until a change touches an API, a worker, a deployment chart, and an alert rule. At that point the problem is not a lack of context window. It is finding the context that is actually connected to the change.</p>

<p>For a three-file repository, I would still open the files myself. There is no prize for adding a graph database to a problem that <code>rg</code> can solve in ten seconds. The case gets more interesting once the repository is large enough that a change has consequences outside the diff.</p>

<h2 id="about-the-528x-number">About the 528x number</h2>

<p>The project README leads with an eye-catching result: up to <strong>528x</strong> fewer tokens. That number is real, but it is the best case from the project&#39;s FastAPI benchmark.</p>

<p>The more useful number is their reported median of roughly <strong>82x</strong> per question across six repositories. Even that needs context. The comparison is whole-corpus reading versus a graph query. A good engineer does not normally paste an entire repository into an LLM and hope for the best, so this is an upper-bound baseline, not a normal day at work.</p>

<p>I like that the README says as much. Too many AI tool pages put the caveat in a footnote, if they include one at all.</p>

<h2 id="what-i-liked">What I liked</h2>

<p>The project does not try to make the model magically smarter. It tries to give the model less irrelevant material. That is a healthier problem to work on.</p>

<p>It also has a plausible route into real workflows: a CLI, MCP support, incremental updates, and a GitHub Action. A graph that looks good in a demo is easy. Keeping one useful after a repository changes every day is the hard part.</p>

<h2 id="what-i-still-do-not-know">What I still do not know</h2>

<p>This smoke test was deliberately small. It did not answer the questions I would ask before adopting it on a serious repository:</p>
<ul><li>How long does the first build take on a monorepo?</li>
<li>How large does the graph database become?</li>
<li>Does retrieval surface the files an experienced reviewer would open anyway?</li>
<li>Does it save time, or does it add another moving part to maintain?</li></ul>

<p>I would not give a trending tool write access to a repository on the strength of a screenshot and a benchmark chart. A read-only container, a non-critical repository, and outputs that a human can inspect are enough for a first pass.</p>

<h2 id="if-you-want-to-try-it">If you want to try it</h2>

<p>Start with one repository you know well. Measure the first build and an incremental update. Then compare the suggested files with the files you would have opened during a normal review.</p>

<p>If the answers line up, keep going. If they do not, you have still learned something useful and avoided adding another MCP server just because it was trending.</p>

<hr>

<p><strong>Test notes:</strong> <code>code-review-graph 2.3.7</code> ran in an isolated Docker container with Python 3.12, one CPU, and 1.5 GiB of memory. This was a graph-build smoke test on a small Python repository. I did not independently reproduce the project&#39;s token benchmark.</p>

<p><strong>Sources:</strong> <a href="https://github.com/tirth8205/code-review-graph">code-review-graph</a> · <a href="https://github.com/tirth8205/code-review-graph/blob/main/docs/REPRODUCING.md">benchmark methodology</a> · <a href="https://github.com/trending">GitHub Trending</a></p>


]]></content:encoded>
      <guid>https://heyimusa.blog/github-trending-context-engineering-ai-coding</guid>
      <pubDate>Sun, 19 Jul 2026 14:56:35 +0000</pubDate>
    </item>
  </channel>
</rss>