DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

DevOps and CI/CD

The cultural movement that is DevOps — which, in short, encourages close collaboration among developers, IT operations, and system admins — also encompasses a set of tools, techniques, and practices. As part of DevOps, the CI/CD process incorporates automation into the SDLC, allowing teams to integrate and deliver incremental changes iteratively and at a quicker pace. Together, these human- and technology-oriented elements enable smooth, fast, and quality software releases. This Zone is your go-to source on all things DevOps and CI/CD (end to end!).

icon
Latest Premium Content
Trend Report
Developer Experience
Developer Experience
Refcard #291
Code Review Core Practices
Code Review Core Practices
Refcard #387
Getting Started With CI/CD Pipeline Security
Getting Started With CI/CD Pipeline Security

DZone's Featured DevOps and CI/CD Resources

Building a Real Pipeline With senro: Monorepos, Caching, and Triggers

Building a Real Pipeline With senro: Monorepos, Caching, and Triggers

By Xavier Portilla Edo DZone Core CORE
In the introduction to senro, I covered the model: a pipeline is a Go program; it builds an immutable plan, and the engine executes that plan while anything can attach to the event stream. This article is the practical follow-up. It covers the five things you will actually write on your first real pipeline: getting your code into a step, caching a step so it never runs the same work twice, fanning one step out over every module in a monorepo, narrowing that fan-out to only what a change touches, and deciding whether a webhook should start a run at all. Everything below was run against senro v1.4.0. A Pipeline That Builds a Real Repository Here is the shape of a Go CI pipeline in senro. Check the code out into a workspace, vet and test it in parallel, then build: Go // Command ci checks the repo out into a workspace, then vets, tests and // builds it. package main import ( "context" "log" "os" "github.com/xavidop/senro" "github.com/xavidop/senro/artifact" "github.com/xavidop/senro/exec" "github.com/xavidop/senro/retry" ) func main() { ctx := context.Background() repo := os.Getenv("REPO_URL") home := os.Getenv("HOME") p := senro.New("ci") // One directory for this run, mounted by every step that needs the code. src := senro.Workspace("src", senro.Scope(senro.ScopeRun)) verify := p.Workflow("verify") verify.Step("checkout", exec.Command("git", "clone", "--depth", "1", repo, ".")). WorkDir("/src"). Mount(src.At("/src", senro.RW)). Retry(3, retry.OnInfra()) verify.Step("vet", exec.Command("go", "vet", "./...")). Needs("checkout"). WorkDir("/src"). Mount(src.At("/src", senro.RO)). Env("HOME", home) verify.Step("test", exec.Command("go", "test", "./...")). Needs("checkout"). WorkDir("/src"). Mount(src.At("/src", senro.RO)). Env("HOME", home) verify.Step("build", exec.Command("go", "build", "-o", "bin/app", "./cmd/app")). Needs("vet", "test"). WorkDir("/src"). Mount(src.At("/src", senro.RW)). Env("HOME", home). Pure(). Inputs(artifact.Glob("**/*.go"), artifact.File("go.sum")). Outputs(artifact.File("bin/app")). Retry(3, retry.OnInfra()) if err := senro.Run(ctx, p); err != nil { log.Print(err) os.Exit(1) } } Plain Text test stdout | ? senrolab/ci [no test files] test stdout | ? senrolab/cmd/app [no test files] test stdout | ok senrolab/greet 0.360s The graph those Needs calls describe, rendered from the run's own plan.json: Plain Text pipeline: ci │ ├─ wave 1 checkout retry 3/infra │ ├─ wave 2 (2 parallel) test │ vet │ └─ wave 3 build needs vet, test · retry 3/infra Two things in there are not obvious coming from a CI system that hands you a checkout and a shell. Workspaces: How Code Reaches a Step Every step runs in an isolated, empty working directory. Nothing hands you a checkout, because "hand the step a checkout" means four different things across a local process, a container, a pod, and an SSH host. A Workspace is a named, versioned directory that steps mount. senro snapshots it into a content-addressed store when a step that mounts it finishes, and restores it for the next step that needs it. So the code gets into the run the way it gets into any CI runner: the checkout step clones it, and the src workspace carries the result to the other three. Mount modes matter: ModeUse it whensenro.RWThe step writes into the workspace, including its declared Outputssenro.ROThe step only reads In the pipeline above, checkout is RW because it writes the tree, vet and test are RO because they only read it, and build is RW because its output lands there. ws.At(path, mode) always needs a mode, Mount accumulates so one step can mount several things, and two mounts at one path are refused at build time. Workspaces come in three scopes: ScopeWhat you getsenro.ScopeRunThe default. One directory for this run, shared by every step that mounts itsenro.ScopePersistentOne directory on this machine that outlives the run. Requires explicit boundssenro.ScopeStepOne fresh directory per step, shared with nobody Declaring the Environment Build() adds nothing to a step's environment. Not a HOME, not a GOPATH, nothing. The local executor supplies its own PATH and that is all. This is deliberate: two developers on the same commit produce the same plan because nothing about their shells leaks into it, and the environment is one of the eleven components a cache key is computed from. In practice, it means tools that need something get told about it: Go .Env("HOME", home) Env takes one pair per call, and it is not variadic on purpose: Env("A", "1", "B") would compile and mean nothing. Caching a Step With Pure() Pure() is a promise: given these inputs, this command produces these outputs, and nothing else matters. senro hashes the inputs, the command, the environment, the workspaces, the mount shape, and six other things into a key. On a second run with the same key, the step is not executed at all, and its recorded outputs are restored. Running the pipeline above twice: Plain Text RUN ID PIPELINE STATUS STARTED DURATION 20260911T105259-8fc9b32e31 ci succeeded 2026-09-11 19:52:59 1s 20260911T105243-8b696eb1d0 ci succeeded 2026-09-11 19:52:43 2s Plain Text $ senro cache explain --run 20260911T105259-8fc9b32e31 build HIT build key a52a9b6c The event stream records it as a state, not a label: JSON {"type":"cache.hit","step":"build", "payload":{"key":"sha256:a52a9b6c...","from_run":"20260911T105243-8b696eb1d0"} {"type":"step.finished","step":"build","payload":{"state":"cached","cached":true} Touch a source file and the same command tells you exactly why it has to run again: Plain Text MISS build key 0e769472 (previous a52a9b6c) ✗ input_digests: ci/main.go ee7563f3 → 4f7d4315 ✗ input_digests: greet/greet.go 59d2873d → 57930022 ✗ input_digests: probe/main.go added ✗ workspace_digests: src f377ce04 → de2069cc ✓ command, env, secrets, executor_class, platform, mount_shape, step_shape, func_identity, tool_versions, version unchanged One rule about the key is worth memorizing, because it decides how you structure a fan-out: Mount puts the whole mounted workspace into the cache key. Inputs narrows what you declare as sources, but it does not narrow the workspace component. A step that mounts a big shared tree has a key that depends on that entire tree. The practical consequence: when you cache expensive per-unit work, give each unit its own workspace instead of sharing one. Read-Only Mounts Are Enforced A step that writes through an RO mount fails, and says so: Plain Text engine: step "build[unit=services/api]" wrote through its read-only mount of workspace "tree" (c9271303 became 832bb638); a read-only mount that changes makes every cache key computed from it wrong That one is go build dropping a compiled binary next to the source. The fix is go build -o /dev/null ./..., which is what a build check wanted anyway. How the rule is enforced depends on the executor, and it is worth knowing which you are relying on: ExecutorWhat senro.RO doesWhen a write is caughtContainerA real read-only bind mountAt the write. It failsKubernetesreadOnly on the volume mountAt the write. It failsLocalNothing at mount timeRight afterward, when the workspace is found changedSSHNothing on the far sideOn read-back. The write is reported, not carried home On local and SSH, read-only is a request senro checks after the fact rather than a rule the kernel enforces. Keep sensitive input out of any workspace a step could overwrite by mistake. Monorepos: Fan Out With Expand Expand(id, graph) adds one step per unit that a graph discovers. Add a module, get a step, and nobody has to write it. Here is a Go workspace with four modules and a dependency chain three deep: Plain Text libs/log <- libs/config <- services/api libs/log <- services/worker Go verify.Expand("build", gowork.Modules()). MaxParallel(4). Needs("checkout"). Affected(src). Template(func(u senro.Unit) *senro.StepBuilder { // WorkDir is the mount point itself, and go -C moves into the unit. return senro.NewStep(exec.Command("go", "-C", u.Dir, "build", "-o", "/dev/null", "./...")). WorkDir("/tree"). Mount(tree.At("/tree", senro.RO)). Env("HOME", home) }) Plain Text checkout -> succeeded build[unit=libs/log] -> succeeded build[unit=libs/config] -> succeeded build[unit=services/worker] -> succeeded build[unit=services/api] -> succeeded One Expand call, four nodes in the plan: Plain Text pipeline: mono (no change filter) │ ├─ wave 1 checkout │ └─ wave 2 (4 parallel) build[unit=libs/config] build[unit=libs/log] build[unit=services/api] build[unit=services/worker] Template is called once per unit in sorted order and must return a fresh *senro.StepBuilder, built with senro.NewStep. Each unit gives you: On UnitWhat it isu.ID, u.NameThe unit's stable identityu.DirThe unit's directory, relative to the rootu.Base()The last path segment: "web" for "apps/web"u.Sources()Every file under the unit, ready for .Inputs(...) on a Pure() template Child IDs come from the expansion ID and the unit (build[unit=libs/log]), never a name the template picks, so the same repository always builds the same graph and a re-run reconstitutes the same children. Two bounds are worth setting. MaxParallel(n) limits how many children of this expansion run at once. MaxNodes(n) rejects an expansion wider than n at build time, default 500, so a scheduler never discovers mid-run that it has forty thousand sandboxes to hold open. Expansion happens once, at Build(). Every unit is discovered and written into the plan before the first step starts. The Eight Unit Graphs GraphDiscoversglob.Dirs / glob.FilesOne unit per matching directory, or per directory containing a matching filegoworkGo workspace modulescargoRust workspace cratesjsworknpm, pnpm or yarn workspacesmaven, gradleJVM modulespyprojectPython projectsbazelBazel packages glob just matches paths. The others read the ecosystem's own manifests, which is what makes the next section work. Running Only What a Change Affects Affected(src) narrows the fan-out to the units a change reaches: the ones owning a changed file, and everything that depends on them, at any depth. The same pipeline, told that one file changed: Plain Text === changed: services/api/main.go === build[unit=services/api] === changed: libs/config/config.go === build[unit=libs/config] build[unit=services/api] === changed: libs/log/log.go === build[unit=libs/config] build[unit=libs/log] build[unit=services/api] build[unit=services/worker] The third case is the point. services/api does not import libs/log. It imports libs/config, which does. That transitive hop is why reading manifests beats globbing paths, and it is why Affected over a glob graph is refused at build time: a path does not know what imports it, and senro would rather say so than quietly run everything. Unaffected units are not skipped; they are not in the plan at all. That is visible in the plan itself. Same pipeline, same Expand, told that libs/config/config.go changed: Plain Text pipeline: mono (libs/config changed) │ ├─ wave 1 checkout │ └─ wave 2 (2 parallel) build[unit=libs/config] build[unit=services/api] Two nodes instead of four, decided at build time. libs/log and services/worker are not present to be skipped, so a re-run of this recorded plan reconstitutes exactly these two. The change source is pluggable: SourceWhat it ischange.FromTrigger(ev)What the webhook that started the run recordedchange.Paths("a/x.go", ...)A literal list, for your own logic and for testschange.Everything()Every unit runschange.Ignoring(src, "docs/**")The above, minus paths you do not care about change.Paths() with no arguments means nothing changed, not everything. Use change.Everything() for that. They are different answers. Keep the Run Directory Out of the Tree senro writes each run to runs/<id>/ under the working directory. That directory holds the run's workspaces, and a workspace usually holds a checkout of the repository. Unit graphs walk the filesystem for manifests, pruning only .git and node_modules, so a second run will discover the first run's checkout as a second copy of every module: Plain Text build[unit=runs/20260911T105558-08e7daff14/ws/tree/services/api] Pin the run directory somewhere outside the tree you fan out over: Go senro.Run(ctx, p, senro.WithDir("/var/lib/senro/runs/"+runID)) Adding runs/ to .gitignore does not help, because the unit graph is walking the filesystem, not asking git. Triggers: The Pipeline Decides Whether to Run Instead of a CI service deciding whether to invoke your pipeline, the pipeline binary is its own matcher. Hand it a webhook delivery, and it decides whether the event is its business: Go err = senro.Run(ctx, pipeline(ev), senro.WithTrigger(ev, // A push to main reports mode "all", which change.FromTrigger passes // straight through: everything builds there. trigger.OnPush(trigger.Branches("main")), // Everything else is narrowed to what it touched. trigger.OnPush(), trigger.OnPullRequest(trigger.Actions("opened", "synchronize")), )) A push to a topic branch that touched services/api/main.go runs one step. A tag push, which this pipeline does not want, runs nothing and says why: Plain Text $ go run ./examples/monorepo --trigger-event /tmp/ev-tag.json senro: trigger: no trigger matched the event: tag refs/tags/v1.2.3 push(branches=[main]): only answers push events, this was tag push: only answers push events, this was tag pull_request(actions=[opened synchronize]): only answers pull_request events, this was tag exit status 78 Exit code 78 is EX_CONFIG, the conventional "this was not my business". One line per matcher explaining why it declined, which is a better answer than a GitHub Actions on: block that silently did not fire. GitHub, GitLab, Bitbucket and Gitea ship, plus your own provider. And the event file is optional: trigger.FromRequest verifies and parses a delivery straight off the wire, per source, so the pipeline binary can be the HTTP endpoint itself. Because the matcher is a binary, you can test a trigger decision locally against a saved payload before it ever reaches a runner. Handlers: Cleanup and Evidence OnFailure runs its handlers once retries are exhausted and the step still failed. Always runs whatever the outcome. A handler can be a Go function, and it knows what it is cleaning up after: Go func Collect(ctx senro.Ctx, _ ReportParams) error { f, ok := ctx.Failure() if !ok { return errors.New("ship/collect only makes sense as a handler") } fmt.Fprintf(ctx.Stdout(), "%s ended %s (exit %d) on attempt %d\n", f.Step, f.State, f.ExitCode, f.Attempt) fmt.Fprintf(ctx.Stdout(), "log tail: %q\n", f.LogTail) return nil } // in the pipeline: build.Step("package", exec.Command("sh", "-c", "echo packaging; exit 1")). OnFailure(senro.Handler("collect", senro.Func("ship/collect", ReportParams{}))). ContinueOnError() Plain Text package ended failed (exit 1) on attempt 1 log tail: "packaging\n" The log tail arrives with the failure, so a handler that classifies errors never has to open a file. Handlers run in the order listed, and a handler that fails never masks the step's own failure: the recorded cause stays the original step. Use senro.Handler(id, action) rather than a workflow's Step. Passing a real step to OnFailure is rejected at build time, because that step would otherwise run twice. A handler also gets its parent's workspaces read-only at the same paths automatically, so Mount on one is refused. Conclusion Put the pieces together, and the argument is not about syntax. The fan-out is a function called once per unit, so what your build does is testable with go test. The Affected graph reads your ecosystem's manifests, so a change to a shared library rebuilds the things that transitively depend on it and nothing else. The cache tells you which of eleven key components changed when it misses. The trigger decision happens in a binary you can run locally against a saved payload. And the whole thing is a Go module with a go.sum, reviewed like the rest of your code. None of that is impossible in YAML. It is that in YAML each one is a plugin, a marketplace action, a service feature, or four hundred lines of bash, and here they are library calls in a language with a compiler. Next in this series: Genkit flows as senro steps: AI pipelines where every model call is a graph node with its own retry, log, and cache entry.Debugging senro pipelines: replaying a finished run, shelling into a live step, and auditing a cache. senro lives at github.com/xavidop/senro. More
Adding Graph Retrieval to a RAG Pipeline With Python

Adding Graph Retrieval to a RAG Pipeline With Python

By Sriharsha Makineni
The majority of retrieval-augmented generation (RAG) examples start as follows: document segmentation; create embedding representations for segmented document sections ("chunks"); store those representations in a vector database and use them to "retrieve" the "best matching" document section(s) for any user-inputted search query. That is a good starting point. However, there is one significant limitation to the process described above: not all questions relate solely to similarity. Some questions relate to relationships. For example: Which team owns the API that Checkout Service uses? While a vector search system could potentially identify documents related to "Checkout Service," "Payment API," and "Platform Team," the critical piece of information is the relationship between these concepts: Markdown Checkout Service --DEPENDS ON--> Payment API Platform Team --OWNS--> Payment API This is when graph retrieval becomes a value-added element. Unlike retrieving only documents based on similarity to a query, a graph retrieval layer can retrieve connected Facts from a graph, providing relationship-aware context to a large language model (LLM) while allowing the LLM to generate the final answer. In other words, instead of simply providing additional context to an LLM in terms of a specific set of retrieved documents, a graph retrieval layer would be able to provide structured context. In this tutorial, we will develop a basic graph retrieval layer to enhance a RAG pipeline utilizing Python and NetworkX. We will load relationship triples from a JSON File, build a directed graph, retrieve the local neighborhood of any entity matched by the retrieval layer, and convert that graph-based context into a format suitable for an LLM Prompt. Why Graph Retrieval Helps Most traditional RAG methods are effective for questions that have answers composed within a couple of data "chunks." Examples include: What database does Payment API utilize? As an example, if you have documentation that states, "The Payment API utilizes PostgreSQL," then a vector search should normally identify it. However, as soon as you are looking for information about related facts (i.e., multiple connected pieces of fact), plain similarity search may not provide sufficient results: Who owns the API that the Checkout Service relies upon? In order to determine who owns the API that the Checkout Service relies upon, you need to perform at least two link traversals: Checkout Service -> Payment APIPlatform Team -> Payment API Since this creates a relationship map (a graph), systems, teams, APIs, and databases all represent nodes in the graph, while their relationships all represent the edges in the graph. As long as this structure is in place, retrieval can follow edges (relationships) rather than simply rely on semantic similarities. This does not mean that graph retrieval replaces vector search completely. A hybrid approach has been found to be more successful in most production environments; specifically, the vector search identifies relevant text, while the graph retrieval identifies relevant relationships among those texts, and ultimately the final answer will be determined using both. Therefore, our main emphasis in this article will be solely on the graph retrieval layer. What We Are Building We are creating a short Python application: Loading data (relationship triples) from file "triples.json".Building a directional graph using NetworkX.Identifying all entities mentioned within the user's input query.Retrieving related graph information.Converting that related information to form context for the LLM prompt.Writing the processed contextual information to "graph_context.json." Output will appear as follows: JSON { "question": "which team is responsible for owning the api Checkout Service depends on?", "context": [ "Checkout Service depends_on payment api.", "Platform Team OWNS Payment API." ] } Then you could pass that created contextual information into an LLM prompt portion of a RAG prompt. Project Setup Create a new project folder: Shell mkdir graph-retrieval-rag cd graph-retrieval-rag Create a requirements.txt file: Plain Text networkx==3.3 Install the dependency: Shell pip install -r requirements.txt Create a file named 'triples.json': JSON [ { "subject": "Checkout Service", "relation": "DEPENDS_ON", "object": "Payment API", "source_text": "Checkout Service depends on Payment API during payment authorization." }, { "subject": "Payment API", "relation": "USES", "object": "PostgreSQL", "source_text": "Payment API uses PostgreSQL for transaction metadata." }, { "subject": "Platform Team", "relation": "OWNS", "object": "Payment API", "source_text": "Platform Team owns Payment API." }, { "subject": "Recommendation Service", "relation": "CALLS", "object": "Catalog API", "source_text": "Recommendation Service calls Catalog API to retrieve product details." }, { "subject": "Catalog API", "relation": "USES", "object": "Elasticsearch", "source_text": "Catalog API uses Elasticsearch for product search." }, { "subject": "Search Team", "relation": "OWNS", "object": "Catalog API", "source_text": "Search Team owns Catalog API." } ] This file is intentionally small. In a real system, these triples could come from entity extraction, service catalogs, architecture documents, source code metadata, or cloud inventory. Step 1: Load Relationship Triples Create a file named graph_retriever.py. Python import json def load_triples(path): with open(path, "r", encoding="utf-8") as file: return json.load(file) Each triple has four fields: Plain Text subject relation object source_text The source_text field matters because graph context should remain traceable. When the model uses a retrieved relationship, the system should know where that relationship came from. Step 2: Build a Directed Graph Next, we will create a graph from these triples by importing the NetworkX library for Python, which can create different graph types (for example, directed or undirected). Python import networkx as nx def build_graph(triples): graph = nx.DiGraph() for triple in triples: subject = triple["subject"] object_ = triple["object"] relation = triple["relation"] graph.add_node(subject) graph.add_node(object_) graph.add_edge( subject, object_, relation=relation, source_text=triple["source_text"], ) return graph A directed graph is important because relationship direction changes meaning. This: Plain Text Checkout Service --DEPENDS_ON--> Payment API does not mean the same thing as: Plain Text Payment API --DEPENDS_ON--> Checkout Service Direction matters for dependency analysis, ownership lookup, impact analysis, and many GraphRAG workflows. Step 3: Find Query Entities We're going to find entities mentioned in a question using simple string searching as the means of finding them. Python def find_query_entities(graph, question): question_lower = question.lower() matches = [] for node in graph.nodes: if node.lower() in question_lower: matches.append(node) return matches The code here does not represent a full NLP solution. This is simply a straightforward baseline. In production, you would probably want to do things like aliasing, entity linking, fuzzy matching, etc., or train your own model. In order to normalize the way that a question is phrased versus how something is named in the graph. For instance, "checkout" might be used to ask about the "Checkout Service. Step 4: Retrieve a Graph Neighborhood After identifying the graph element that will answer a question, identify all other elements connected to it. One method to do this would be to gather together both outgoing and incoming edges at each identified node. This could be done with the following code snippet: Python def retrieve_neighborhood(graph, seed_entities, max_hops=2): visited_nodes = set(seed_entities) frontier = set(seed_entities) retrieved_edges = [] for _ in range(max_hops): next_frontier = set() for node in frontier: for neighbor in graph.successors(node): edge_data = graph.get_edge_data(node, neighbor) retrieved_edges.append((node, edge_data["relation"], neighbor, edge_data)) next_frontier.add(neighbor) for neighbor in graph.predecessors(node): edge_data = graph.get_edge_data(neighbor, node) retrieved_edges.append((neighbor, edge_data["relation"], node, edge_data)) next_frontier.add(neighbor) frontier = next_frontier - visited_nodes visited_nodes.update(next_frontier) return deduplicate_edges(retrieved_edges) # Adding a helper function to remove duplicate edges: def deduplicate_edges(edges): seen = set() unique_edges = [] for source, relation, target, edge_data in edges: key = (source, relation, target) if key not in seen: seen.add(key) unique_edges.append((source, relation, target, edge_data)) return unique_edges The max_hops parameter controls how far retrieval travels from the original query entity. For example: Plain Text max_hops=1 retrieves immediate neighbors. max_hops=2 retrieves neighbors of neighbors. While higher values give you a larger amount of additional knowledge about your subject matter, this may also introduce a lot of "noise" into your results. Step 5: Convert Graph Results Into LLM Context The retrieved graph edges are structured data. Before sending them to an LLM, convert them into short, readable facts. Python def format_edges_as_context(edges): context = [] for source, relation, target, edge_data in edges: context.append( { "fact": f"{source} {relation} {target}.", "source_text": edge_data["source_text"], } ) return context This gives the prompt two useful forms of evidence: A normalized graph fact.The original source sentence. That makes the final answer easier to debug and easier to cite. Step 6: Build a RAG Prompt Now create a simple prompt builder. Python def build_prompt(question, graph_context): context_lines = [] for item in graph_context: context_lines.append(f"- {item['fact']} Source: {item['source_text']}") context_text = "\n".join(context_lines) return f""" You are answering a question using graph-retrieved context. Use only the facts provided in the context. If the context does not contain enough information, say so. Question: {question} Context: {context_text} Answer: """.strip() This prompt is intentionally restrictive. The model should use the source instead of guessing. A good production application would also include a format for citations, rules for how to write responses, and fallback behavior. Complete Example Here is the full code: Python import json import networkx as nx def load_triples(path): with open(path, "r", encoding="utf-8") as file: return json.load(file) def build_graph(triples): graph = nx.DiGraph() for triple in triples: subject = triple["subject"] object_ = triple["object"] relation = triple["relation"] graph.add_node(subject) graph.add_node(object_) graph.add_edge( subject, object_, relation=relation, source_text=triple["source_text"], ) return graph def find_query_entities(graph, question): question_lower = question.lower() matches = [] for node in graph.nodes: if node.lower() in question_lower: matches.append(node) return matches def retrieve_neighborhood(graph, seed_entities, max_hops=2): visited_nodes = set(seed_entities) frontier = set(seed_entities) retrieved_edges = [] for _ in range(max_hops): next_frontier = set() for node in frontier: for neighbor in graph.successors(node): edge_data = graph.get_edge_data(node, neighbor) retrieved_edges.append((node, edge_data["relation"], neighbor, edge_data)) next_frontier.add(neighbor) for neighbor in graph.predecessors(node): edge_data = graph.get_edge_data(neighbor, node) retrieved_edges.append((neighbor, edge_data["relation"], node, edge_data)) next_frontier.add(neighbor) frontier = next_frontier - visited_nodes visited_nodes.update(next_frontier) return deduplicate_edges(retrieved_edges) def deduplicate_edges(edges): seen = set() unique_edges = [] for source, relation, target, edge_data in edges: key = (source, relation, target) if key not in seen: seen.add(key) unique_edges.append((source, relation, target, edge_data)) return unique_edges def format_edges_as_context(edges): context = [] for source, relation, target, edge_data in edges: context.append( { "fact": f"{source} {relation} {target}.", "source_text": edge_data["source_text"], } ) return context def build_prompt(question, graph_context): context_lines = [] for item in graph_context: context_lines.append(f"- {item['fact']} Source: {item['source_text']}") context_text = "\n".join(context_lines) return f""" You are answering a question using graph-retrieved context. Use only the facts provided in the context. If the context does not contain enough information, say so. Question: {question} Context: {context_text} Answer: """.strip() def write_json(path, data): with open(path, "w", encoding="utf-8") as file: json.dump(data, file, indent=2) if __name__ == "__main__": question = "Which team owns the API that Checkout Service depends on?" triples = load_triples("triples.json") graph = build_graph(triples) seed_entities = find_query_entities(graph, question) if not seed_entities: print("No matching graph entities found in the question.") raise SystemExit(0) retrieved_edges = retrieve_neighborhood(graph, seed_entities, max_hops=2) graph_context = format_edges_as_context(retrieved_edges) prompt = build_prompt(question, graph_context) result = { "question": question, "seed_entities": seed_entities, "graph_context": graph_context, "prompt": prompt, } write_json("graph_context.json", result) print("Seed entities:") print(seed_entities) print("\nGraph context:") for item in graph_context: print("-", item["fact"]) print("\nPrompt:") print(prompt) Run it: Python python graph_retriever.py Expected output: Python Seed entities: ['Checkout Service'] Graph context: - Checkout Service DEPENDS_ON Payment API. - Payment API USES PostgreSQL. - Platform Team OWNS Payment API. Prompt: You are answering a question using graph-retrieved context. Use only the facts provided in the context. If the context does not contain enough information, say so. Question: Which team owns the API that Checkout Service depends on? Context: - Checkout Service DEPENDS_ON Payment API. Source: Checkout Service depends on Payment API during payment authorization. - Payment API USES PostgreSQL. Source: Payment API uses PostgreSQL for transaction metadata. - Platform Team OWNS Payment API. Source: Platform Team owns Payment API. Answer: The answer is now straightforward: Plain Text Platform Team owns Payment API, which Checkout Service depends on. The important part is not that an LLM can phrase the answer. The important part is that retrieval found the connected facts needed to support it. What This Adds to a RAG Pipeline A standard RAG pipeline often looks like this: Markdown Documents | Chunking | Embeddings | Vector Search | LLM A graph-enhanced pipeline adds another retrieval path: Markdown Question | Query entity detection | Graph neighborhood retrieval | Graph context formatting | LLM In a hybrid system, both paths can work together: Markdown Question | |---- Vector search -> relevant text chunks | |---- Graph search -> connected facts | Combined context | LLM This is useful because vector search and graph retrieval solve different problems. Vector search is good at finding semantically similar text. Graph retrieval is good at following explicit relationships. Production Considerations The graph retrieval layer used in this example is very rudimentary. A production-quality implementation will have many additional "guardrails." Normalized entities are necessary for performing entity matching. Exact string matches will miss most common aliases (i.e., checkout, checkout-service, etc.); however, production-quality implementations will use canonical IDs/aliases/entity linking. Graph retrieval has limits. While there is no limit to how large a graph can grow in terms of returned context, returning too much context can be detrimental. To limit the amount of context returned, implement boundaries, including but not limited to hop count, edge type, node type, confidence value, and source data freshness. All relationships in the graph are not equal in reliability. For example, a dependency established by a service catalog entry may be a more reliable relationship than a dependency inferred from an older document. Therefore, store your confidence level, timestamp, and other source metadata values along with every edge. Not all relationships are equally reliable. Contextual information, in addition to just what was retrieved from the graph, should also contain evidence that supports or refutes the answer. As a result of providing evidence with contextual information, when someone reviews your answer, they will be able to easily verify the answer, and if something does go wrong during development, it will make it easier to debug. Hybrid approaches are best suited for using graph retrieval. While graph retrieval can help the model understand why certain pieces of information relate to each other, document chunks can provide much more detail about those related pieces. When retrieving both types of information, the model will likely receive more complete contextual information. Evaluation is mandatory. You will need to create realistic test questions and determine if the sub-graph retrieved from the database includes enough factual information for answering those test questions. Never assume that graph retrieval will improve performance unless you measure it. Key Takeaways Graph retrieval adds relational contextual knowledge to a RAG framework. A simple directed graph can answer many complex questions involving multi-step logic that would be difficult to accomplish with plain similarity-based search. One key design decision in developing an effective graph-retrieval system is what determines the success of the retrieval layer: does it identify the correct entities and their associated relationships? Graph retrieval works well when the question involves dependency, ownership, impact assessment, lineage, and/or multi-step reasoning. In addition to graph retrieval, build a production-quality retrieval system that includes source tracking, entity normalization, confidence scoring, and evaluation. Try It Yourself Add these triples to triples.json JSON { "subject": "Billing Worker", "relation": "PUBLISHES_TO", "object": "Kafka", "source_text": "Billing Worker publishes payment events to Kafka." }, { "subject": "Data Platform Team", "relation": "OWNS", "object": "Kafka", "source_text": "Data Platform Team owns Kafka." } Then change the question to: Plain Text Which team owns the system that Billing Worker publishes events to? Your goal is to retrieve: Plain Text Billing Worker PUBLISHES_TO Kafka. Data Platform Team OWNS Kafka. Once that works, try lowering max_hops from 2 to 1 and compare the retrieved context. This is a simple way to see why hop count matters in graph retrieval. More
Introducing senro: A Pipeline Engine You Program in Go
Introducing senro: A Pipeline Engine You Program in Go
By Xavier Portilla Edo DZone Core CORE
When RabbitMQ Accepts a Message but Does Not Route It to a Queue
When RabbitMQ Accepts a Message but Does Not Route It to a Queue
By Vamsi Krishna Gude
How to Keep AI Agents From Going Rogue Using Docker Sandbox
How to Keep AI Agents From Going Rogue Using Docker Sandbox
By Siri Varma Vegiraju DZone Core CORE
Data Integration Landscape 2026: Event Streaming, API, and Batch in the Era of Agentic AI
Data Integration Landscape 2026: Event Streaming, API, and Batch in the Era of Agentic AI

Data integration used to be treated as back-office plumbing. Difficult, yes, and always operationally critical, but rarely seen as strategic. That has changed. The Data Integration Landscape 2026 maps this shift across three communication paradigms and every major vendor. The largest software companies in the world have paid a fortune to own the integration layer: IBM acquired Confluent for $11 billion, Salesforce acquired Informatica for $8 billion, Qlik absorbed Talend, TIBCO folded into Cloud Software Group, and Fivetran merged with dbt Labs. When integration vendors command prices like these, the integration layer has become a strategic asset, not a piece of infrastructure to take for granted. The pressure behind this has been building for a long time. Data volumes keep exploding, digitalization keeps pushing more of the business onto real-time rails, and the demand for fresh data has been rising for years. AI is the latest and most acute driver. Models and autonomous agents need data that is current, governed, and continuously flowing. A nightly batch job and an API that returns yesterday's state cannot support a system making decisions in the moment. An agent working from outdated data does not fail loudly. It confidently does the wrong thing. That raised the stakes on a problem enterprises have been wrestling with for a decade. What Are the Three Data Integration Paradigms? The three data integration paradigms are request-response, batch, and event streaming. The landscape is best understood not by vendor but by how data actually moves. Choosing which paradigm sits at the center is among the most consequential architectural decisions an enterprise makes. Request-response is the most familiar. A system asks another for data and waits. REST APIs, GraphQL, and API management and gateway products work this way. It suits transactional interactions like fetching a record or submitting an order, but it couples the caller to the callee: you must know where the other system is and wait for it to answer. Batch moves data in bulk on a schedule. It is reliable and well understood for historical loads, analytics, regulatory reporting, and AI model training. Its weakness is freshness. The data is always minutes or hours old, which breaks down the moment a system needs to act on what is happening right now. Event streaming moves data continuously as events occur, and Apache Kafka has become its de facto standard. When a transaction completes or a record changes, that event is published to a stream, and any system that needs it consumes independently, without asking the source for anything. It is decoupled, persistent, and real-time, which is why it has become the foundation for both modern integration and agentic AI. Apache Flink has emerged as the de facto standard for stateful stream processing on top of Kafka, covering real-time transformation, aggregation, and AI-enriched data pipelines. The Data Integration Landscape below maps every major vendor across these three paradigms. Filled bubbles are platform leaders; outlined bubbles are specialists and emerging players. Grouped by paradigm, not by ranking. The full analysis is available as a free PDF without registration. Why Should Event-Driven Architecture Be the Center of Data Integration? The central argument of this landscape is simple. Event-driven architecture belongs at the center, with request-response and batch as consumer interfaces on top of it. The reason is decoupling. In an event-driven model, the system that produces data does not need to know who consumes it, when, or how. Producers and consumers are independent, which means a slow or unavailable system does not block everything connected to it. This is most commonly implemented with event streaming platforms like Apache Kafka, but message brokers such as IBM MQ and Solace serve event-driven patterns too, particularly where guaranteed delivery and fine-grained routing matter more than high-throughput streaming. Most enterprises use all three paradigms at once, and that is correct. The mistake is treating the choice as either/or. A business unit running iPaaS workflows, an analytics team pulling batch exports, and an AI agent querying live state can all be served from the same event-driven backbone without coupling to each other or to the underlying systems. Each connects in the way that suits it: some consume the stream directly, others reach it through an API gateway that exposes the stream as an endpoint or pushes events to a webhook or function. No large enterprise runs a single integration platform, so the real question is never which one product to buy. It is which paradigm sits at the center, and how the rest connect to it without recreating the tangle of point-to-point links it was meant to replace. The full landscape works through the market vendor by vendor: Data Integration Landscape. Is Batch Integration Dead? No. Batch is still the right tool for historical loads, analytical refresh cycles, regulatory reporting, and AI model training. What has changed is where the new thinking happens. The innovation has moved to the streaming layer, where data is processed continuously and acted on at the right time and context, often in real time. The batch column of the landscape has no emerging row because new entrants are building for streaming and change data capture, not for scheduled pipelines. Batch endures. It is just no longer where the architecture is evolving. Which Architecture Decisions Matter More Than Vendor Selection? The consolidation wave points to three decisions that outweigh which vendor you pick. The three-paradigm suite is the new baseline. The major platform vendors now each cover all three paradigms in one ecosystem. Choosing one of them increasingly means committing to that ecosystem across API management, data integration, and event streaming at once. That lock-in is far more expensive to unwind than any single product swap. If you are making a five-year platform decision, map which paradigms each candidate actually covers before you compare feature lists. The market is moving toward event-driven architecture. Databricks added Kafka-compatible APIs to its Zerobus Ingest service, allowing existing Kafka producers to stream data directly into Databricks with a configuration change rather than code changes. Snowflake announced Datastream, its own Kafka-compatible service, now heading into private preview. Batch ETL vendors are adding change data capture. iPaaS vendors are adding event-driven capabilities. The direction is consistent because agents and operational systems cannot wait for batch cycles. Legacy integration shapes the architecture more than vendor selection does. Same-vendor shortcuts like Zero-ETL work inside one ecosystem, but they do nothing for the SAP iDoc pipeline, the mainframe, the EDIFACT exchange, or the hospital's HL7 feed. Real enterprise architecture absorbs legacy formats at the edge for years. A platform that connects cleanly to the future but needs custom work to reach the past is not a complete platform for most enterprises. How Does Data Integration Connect to Process Intelligence and Agentic AI? Data integration does not stand alone. It is the foundation beneath process intelligence and trusted agentic AI, the three layers I think of as the Trinity of modern data architecture. The data layer moves and governs the data. Process intelligence understands how the organization actually runs. Trusted agentic AI acts on both. Each layer depends on the one beneath it. The full vendor-by-vendor analysis covers the platform leaders, the specialists, and the emerging players across all three paradigms, plus the legacy integration challenge, industrial IoT, and the trends shaping the next two years. If you are making an integration decision this year, the Data Integration Landscape 2026 is the starting point. Begin with the paradigms, not the vendor list. Map which ones you actually need at the center and which belong at the edges. The vendor choice gets much easier once the architecture is clear. About this landscape: This is an independent analyst perspective. No vendor paid for inclusion or placement. No vendor reviewed its section before publication. I run an advisory practice through Kai Waehner GmbH and have held roles at Talend, TIBCO, and Confluent. I also serve as Global Field CTO at Kestra, a workflow orchestration vendor outside this landscape's scope. The complete methodology note is in the full landscape.

By Kai Wähner DZone Core CORE
Engineering Audit-Ready Data Pipelines for Regulatory Reporting
Engineering Audit-Ready Data Pipelines for Regulatory Reporting

There's a particular kind of silence that happens on a call when a regulator asks, "Can you walk me through exactly how this number was calculated?" and the person on the other end has to say, "Let me get back to you." I've been on that call. It is not a fun place to be, and it usually isn't because anyone did anything wrong. It's because the pipeline that produced the number was never built to answer that question in the first place. Regulatory reporting, whether it's a liquidity return, a capital calculation, or something more niche, has a strange property compared to most data engineering work. Nobody outside the team really cares how the dashboard behind it looks. What they care about is whether you can prove, months or years later, exactly where every figure came from, who touched it, and why it changed between two reporting periods. That's a fundamentally different design goal than "make the report run fast," and honestly, a lot of pipelines get built for speed and convenience first, with auditability bolted on afterward as an afterthought. It shows. What "Audit-Ready" Actually Means in Practice I think people sometimes hear "audit-ready" and picture some heavyweight compliance layer sitting on top of a normal pipeline. In my experience, it's less about adding a layer and more about changing a handful of default behaviors early, before the pipeline exists in production. Four things tend to matter most. First, nothing gets overwritten. Ever. If a source record changes, you keep the old version and the new one, both timestamped. Second, every transformation step has to be reproducible from code, not from a person's memory of what they clicked in a tool three years ago. Third, there's a reconciliation point, a moment where the pipeline checks its own output against the source and either passes or stops. And fourth, a human actually looks at the number before it leaves the building. Not a dashboard glance, an actual documented sign-off with a name and a timestamp attached to it. Here's roughly how that fits together end to end. A couple of things in that picture are doing more work than they look like they are. The raw zone sitting right after ingestion, untouched and append-only, is what lets you go back eighteen months later and reconstruct exactly what the source system handed you on a given day, independent of whatever the curated tables look like now. I've genuinely had to do this, pulling raw records from a specific date to settle a dispute about whether a discrepancy came from us or from upstream. Without that raw layer, that conversation becomes a lot more uncomfortable. The Reconciliation Step Is Where Trust Actually Gets Built If I had to pick one part of this architecture that matters more than the rest, it's the reconciliation checkpoint. This is the moment where the pipeline stops behaving like a normal ETL job and starts behaving like a control. The logic itself isn't exotic. You compare row counts, sums, and control totals against the source, and you either match within tolerance or you don't. What matters is what happens on the "no" branch. A lot of pipelines I've inherited over the years treated a reconciliation break as a warning buried in a log file that nobody reads until someone complains. That's backward. A break should halt the pipeline and page someone, because a regulatory submission built on unreconciled data is worse than a late submission. I'd rather explain a delay than explain a restated figure. I'll admit there's a tension here worth naming honestly. Hard stops on every mismatch can grind a team down if your tolerance thresholds are too tight, and you end up with alert fatigue where people start ignoring pages because most of them turn out to be rounding noise from a currency conversion. Getting the thresholds right, informed by prior period baselines rather than guessed at, takes real tuning. It's not a solved problem you configure once and forget. Lineage and the Four-Eyes Check The governance band running underneath the pipeline, capturing lineage and version history, is what turns "I think this is right" into "here's the exact model, the exact commit, and the exact person who approved it." On Unity Catalog or an equivalent lineage tool, this is largely automatic once you've built your transformations in something like dbt rather than scattered SQL scripts. That's honestly one of the more underrated benefits of moving regulatory pipelines onto a tool with built-in lineage tracking. You stop having to build that tracking yourself. The human sign-off matters just as much, maybe more, even though it's the least technically interesting part of the whole design. A second person reviewing the output before submission, with their review logged, catches the kind of error that automated tests won't, like a number that's technically correct by every rule you wrote but obviously wrong to a person who understands the business context. A Small Piece of Advice If you're building one of these pipelines from scratch, resist the urge to treat the audit trail as something you'll add once the pipeline is "working." Retrofitting immutability and lineage onto tables that have already been overwritten a hundred times is painful, and in some cases you simply can't recover the history you needed. Build the raw, append-only layer first, even if it feels like overhead in week one. Six months later, when someone asks you to explain a number from last quarter, you'll be glad it's there.

By SUNILKUMAR REDDY Eraganeni
Node and Capacity Spillover: What Happens When Kubernetes Runs Out of Room
Node and Capacity Spillover: What Happens When Kubernetes Runs Out of Room

A pod stuck in Pending is one of the most misleading states in Kubernetes. Nothing looks wrong. There's no error banner, no red text screaming at you. The scheduler just quietly gave up finding a home for it and is waiting for the world to change. I've stared at that status more times than I'd like to admit, usually at the worst possible hour, usually while someone in a chat window is asking why traffic isn't going through. Eleven minutes is about how long it took once, on a cluster that had close to 40% CPU sitting free the entire time. That gap between "there's capacity somewhere" and "this pod actually got scheduled" is what this article is really about. This is capacity spillover: demand outgrows what the current set of nodes can absorb, and the cluster needs somewhere for the overflow to land. Handle that well and it's a blip nobody notices. Handle it badly, and it's an outage that shows up in a customer complaint before it shows up in your dashboard. I'll be honest, a few years ago I would have called this an edge case worth a paragraph in the docs and moved on. Not anymore. Ask around, and you'll find most people running workloads in the cloud right now have a story about a specific instance type just not being there when they needed it, GPU-backed machines most visibly, but I've seen it happen with fairly ordinary compute SKUs too, usually right in the middle of a demand spike nobody scheduled around. That's really the whole case for spillover, once you strip away the architecture diagrams. Without it, what you get during a capacity crunch is close to random: whichever pod happens to hit the scheduler first grabs whatever's left, and everyone else waits and hopes. With it, the randomness mostly goes away. Workloads land where you decided they should, in the order you decided matters, instead of wherever the scheduler happened to find room first. New pod needs scheduling -> primary node pool checked -> if room is available, schedule normally; if the pool is full, spill to the secondary pool (overflow capacity). Why Scheduling Failures Happen in the First Place The scheduler's job sounds almost too simple: find a node with enough free CPU and memory for the pod's requests. In practice, a pod can fail to schedule for reasons that have nothing to do with the cluster actually running out of room. Here's the part that trips people up the first time they see it. A cluster can have plenty of free CPU in aggregate and still fail to place a memory-heavy pod, because no single node has enough memory available on its own. Resource requests get evaluated per node, not pooled across the whole cluster. I've had a coworker insist this was a bug the first time they saw it happen. It isn't. It's just how bin-packing works when nobody told you that's what you were looking at. Taints, tolerations, and node affinity narrow the field further. A pod that needs a GPU node, or needs to steer clear of a specific zone, only ever sees the slice of the cluster that matches. Spare capacity can be sitting right there, and the pod simply isn't allowed near it. And then there's the timing problem, which is the one that actually pages people. Even when the cluster genuinely needs more nodes and the autoscaler agrees, provisioning a node isn't instant. There's a real window between "we need capacity" and "capacity exists," and whatever's waiting in that window experiences it as a failure, not as a system doing exactly what it was designed to do. What Spillover Actually Looks Like "Spillover" gets used loosely, but it covers a few genuinely different situations, and I think that's where a lot of confusion comes from. People nod along in a meeting assuming everyone means the same thing, and they don't. The first one, and honestly the one most teams reach for first, is spilling to a different node pool entirely. You run a primary pool sized for steady-state load, and a secondary pool, usually spot or preemptible, that only spins up once the primary's full. Interruption-tolerant stuff gets pushed there when things get tight, which keeps the expensive, stable pool free for whatever actually can't afford to get evicted. It's an easy sell to whoever owns the cloud bill, which is probably why it's the version everyone sets up first. Then there's spilling across zones, or in the worst cases, regions. A single zone running out of a specific instance type during a demand spike isn't some rare event you plan a tabletop exercise around; it's just a Tuesday at this point. So you spread the workload wider. Fine in theory. I've watched a "let's just spread it across zones" fix get shipped on a Friday and turn into a cross-region latency ticket by the following Wednesday, because nobody had checked whether the workload actually tolerated that. And there's a third flavor that's less about running out of room and more about not wanting to spend the money. Technically, there's room on the expensive reserved instances. The better default is letting things spill onto cheap infrastructure first, and only reaching for the premium stuff when nothing cheaper is left over. Build it the other way, and you won't find out until the next billing cycle, which is a genuinely bad time to find out. Designing for It Instead of Reacting to It The cluster autoscaler, or something like Karpenter, handles the mechanics of adding nodes once pods can't be placed anywhere. That's useful, but it's also reactive by definition, closer to a fire alarm than a smoke detector. By the time it fires, everyone already knows something's wrong. A few things actually close the gap between "capacity ran out" and "someone got paged," in my experience. Pod priority and preemption are the ones people underestimate. Not every workload deserves an equal claim on scarce capacity, and treating them as if they do just means everything fails to schedule at once, in whatever order the scheduler happened to try them, which helps nobody. Give the stuff that actually matters a higher PriorityClass and let it evict lower-priority pods when things get tight. Over-provisioning on purpose sounds wasteful until you sit with it for a second. A handful of low-priority placeholder pods, doing nothing except holding a spot, get evicted the instant real demand shows up, and the real workload schedules immediately instead of waiting on a node to boot. Small constant cost, much faster response when it actually counts. Provisioning speed is the one that surprised me. Tools that can add nodes directly, without needing to fit some pre-defined pool shape first, shrink that window between "pod can't schedule" and "a node exists to put it on" a lot more than I expected going in. That window is basically where the pager lives. And testing failure on purpose, which is the step almost everyone skips. It's easy to assume spillover works because it's configured correctly on paper. Deliberately draining a node pool in a test environment and watching whether pods actually land where they're supposed to, in the order they're supposed to, finds a category of misconfiguration you will never catch by reading YAML, however carefully you read it. The Part That's Easy to Skip Past Spillover design tends to get treated as a purely technical exercise. Pick the autoscaler, define the pools, set the priorities, done. The harder question usually isn't technical at all: who decides which workloads are allowed to spill onto cheaper or less reliable capacity, and who's on the hook when a decision made for cost reasons turns into someone else's latency problem three sprints later. Getting the mechanics right is necessary. It's not enough on its own, because nobody's actually agreed on the policy those mechanics are supposed to enforce, and that conversation almost always happens after the first bad incident instead of before it. It's happened to every team I've worked with in some form. I'd rather have that conversation early, even if it's a little awkward and nobody wants to own the answer yet. It beats the alternative meeting, the one scheduled at 9 am the morning after something broke.

By Kishor Patil
Hot-Reloading Kubernetes Secrets and ConfigMaps in Go With mamori (No Pod Restarts)
Hot-Reloading Kubernetes Secrets and ConfigMaps in Go With mamori (No Pod Restarts)

If you run Go services on Kubernetes, you know the ritual. You update a Secret or a ConfigMap, and then you do one of these: kubectl rollout restart deployment/my-app and eat the disruption.Install Reloader so it restarts your pods for you when a Secret changes.Mount the Secret as a volume and cross your fingers that your app re-reads the file (spoiler: most apps read it once at boot). All three share the same flaw: the way you "pick up" a config change is to throw away the process and start a new one. For a rotating database password, that means dropping every in-flight request, cold connection pools, and a latency spike — all to change one string. I built mamori partly because of this exact frustration. mamori watches your Kubernetes Secrets and ConfigMaps with native informers, and when a value changes it reconciles the new value into your running Go process — typed, validated, and atomically swapped — without restarting anything. New to mamori? Start with the launch article, Introducing mamori: Typed, Watchable Config & Secrets for Go, for the full picture. This post is the Kubernetes-focused deep cut. The Two Schemes You'll Use mamori's Kubernetes provider gives you two ref schemes: RefReads fromNotesk8s-secret://<namespace>/<name>#<key>a Secretvalue is base64-decoded for youk8s-cm://<namespace>/<name>#<key>a ConfigMapplain string values Drop the #key and you get the whole Secret/ConfigMap as a JSON map, which you can decode into a nested struct with flatten. A few concrete examples: Plain Text k8s-secret://prod/db-creds#password # one key from a Secret, base64-decoded k8s-secret://prod/tls#ca.crt # a certificate as []byte k8s-cm://prod/app-config#log_level # one key from a ConfigMap k8s-cm://prod/app-config # the whole ConfigMap as a JSON map Setup The provider lives in its own module. Note the import path is providers/k8s (not providers/kubernetes): Shell go get github.com/xavidop/mamori go get github.com/xavidop/mamori/providers/k8s Register it with a blank import — same convention as database/sql drivers. Its init() wires the k8s-secret and k8s-cm schemes into mamori: Go import _ "github.com/xavidop/mamori/providers/k8s" In-cluster, the provider uses the pod's mounted ServiceAccount automatically. The only thing you owe it is RBAC: it needs to get, list, and watch the resources you reference in the namespaces you reference. YAML apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: mamori-reader namespace: prod rules: - apiGroups: [""] resources: ["secrets", "configmaps"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: mamori-reader namespace: prod subjects: - kind: ServiceAccount name: my-app namespace: prod roleRef: kind: Role name: mamori-reader apiGroup: rbac.authorization.k8s.io The watch verb is the important one — it's what lets mamori use an informer instead of hammering the API server with polls. Load Once, Then Watch Forever Describe what you need as a struct. mamori resolves every ref, applies defaults, and validates — all before your main gets a config: Go package main import ( "context" "log" "github.com/xavidop/mamori" "github.com/xavidop/mamori/secret" _ "github.com/xavidop/mamori/providers/k8s" ) type Config struct { DBPassword secret.String `source:"k8s-secret://prod/db-creds#password"` LogLevel string `source:"k8s-cm://prod/app-config#log_level" default:"info" validate:"oneof=debug info warn error"` MaxConns int `source:"k8s-cm://prod/app-config#max_conns" default:"10" validate:"gte=1,lte=500"` CACert []byte `source:"k8s-secret://prod/tls#ca.crt"` } func main() { cfg, err := mamori.Load[Config](context.Background()) if err != nil { log.Fatal(err) } _ = cfg // fully resolved, defaulted, validated } Notice DBPassword is a secret.String, not a plain string. That type redacts itself in logs, fmt, and JSON — so a stray log.Printf("%+v", cfg) prints [REDACTED] instead of leaking the password. You only ever see the plaintext when you explicitly call .Reveal(). Now the part that matters. Swap Load for Watch, and the moment someone runs kubectl edit secret db-creds, your process reacts: Go w, err := mamori.Watch[Config](ctx, mamori.OnChange(func(ev mamori.Change[Config]) { if ev.Changed("DBPassword") { // Rebuild the pool with the rotated password — live, no restart. pool.Rotate(ev.New.DBPassword.Reveal()) } for _, f := range ev.Fields { log.Printf("reconciled %s: %s -> %s", f.Path, f.OldVersion, f.NewVersion) } }), mamori.OnError(func(err error) { metrics.Inc("config_error") }), ) if err != nil { log.Fatal(err) } defer w.Close() cfg := w.Get() // lock-free snapshot; always the last VALID config w.Get() is lock-free and always returns the last valid configuration. If someone pushes a ConfigMap with log_level: banana, the validate:"oneof=..." tag rejects it: the update is dropped atomically, Get() keeps returning the good value, and OnError receives a *ValidationError. A bad edit never takes your service into a broken state. Informers, Not Polling This is the difference between mamori and the mount-a-file-and-poll approach. The Kubernetes provider implements native watching backed by the client-go informer machinery, so change detection is push-based — the API server tells your process the instant a Secret changes, rather than your process asking every N seconds. Practically, that means: Near-instant reconciliation. No polling interval to tune, no min(sync, interval) latency floor like the kubelet's ~60s volume refresh.No API-server hammering. One watch connection per resource, shared, instead of a GET loop per pod.Graceful fallback. If a native watch fails to start for some reason, mamori automatically falls back to polling with jittered intervals, so you never lose reconciliation entirely. Compare that to the alternatives: ApproachHow a change is picked upRestart?kubectl rollout restartyou, manuallyyesReloader / Stakateroperator restarts the podyesSecret mounted as volumekubelet re-syncs file (~60s), if your app re-reads itno, but you must re-readmamoriinformer pushes change → validated atomic swap → callbackno Whole ConfigMaps and JSON Blobs Sometimes you keep a structured blob in one key. Decode it straight into a nested struct with flatten: Go type Config struct { Redis RedisConfig `source:"k8s-secret://prod/redis-conn" flatten:"json"` } type RedisConfig struct { Addr string `mapstructure:"addr"` Password secret.String `mapstructure:"password"` DB int `mapstructure:"db"` } That reads the whole redis-conn Secret (a JSON document), decodes it, and — because Password is a secret.String — keeps the password redacted throughout. A Note on TLS Certificates Certificates rotate too, and a cert reload is the perfect no-restart use case. Because file and Secret updates for certs usually want to apply immediately (no coalescing delay), you can set ?debounce=0 on the ref: Go type TLS struct { Cert []byte `source:"k8s-secret://prod/tls#tls.crt?debounce=0"` Key secret.Bytes `source:"k8s-secret://prod/tls#tls.key?debounce=0"` } Wire the OnChange callback to swap the *tls.Certificate your server hands out in its GetCertificate hook, and you've got zero-downtime cert rotation from a Kubernetes Secret. When You Don't Need This Be honest with yourself: if your config is a log level from a ConfigMap that only ever changes on deploy, you don't need runtime watching — a plain Load at boot is fine, and mamori is happy to do just that. And if you already mount a Secret as a file and genuinely re-read it, mamori's built-in file:// provider (with fsnotify) covers that too, without any Kubernetes RBAC at all. mamori earns its keep the moment a value changes while your process is running and you'd rather rotate a pool than recycle a pod. Wrapping Up Kubernetes already tells you the instant a Secret changes — via informers. mamori just connects that signal to a typed, validated, redacted struct inside your Go process, so a rotation becomes a callback instead of a rollout. Shell go get github.com/xavidop/mamori/providers/k8s Full introduction: Introducing mamoriDocs: mamorigo.dev/docsSource: github.com/xavidop/mamori mamori is MIT-licensed and currently at v0.1.0, so issues and feedback are very welcome. Happy shipping!

By Xavier Portilla Edo DZone Core CORE
"Works on My Machine" Is an Architecture Problem

The familiar claim that software behaves correctly on a developer workstation but fails elsewhere is often treated as a debugging cliché. In practice, it describes an architectural failure where part of the system’s behavior exists outside version control. Source code may be identical while the effective execution environment differs in runtime version, CPU architecture, filesystem semantics, locale, time zone, native libraries, certificates, environment variables, dependency resolution, or build tooling. When those inputs remain implicit, the developer workstation becomes an undocumented dependency. The resulting failure is not merely environmental inconvenience, but it is evidence that the system lacks a complete, reproducible definition of the conditions under which it is expected to build and run. Reproducible-build practices are based on the same principle: a declared source and build inputs should create an independently verifiable path to the resulting artifact rather than depending on hidden machine state. The Machine Is an Input, Even When the Repository Pretends Otherwise A repository normally captures application source, configuration templates, and dependency manifests, but operating assumptions often remain outside it. A service compiled with one JDK can behave differently under another. Code that relies on the host time zone inherits machine configuration, and Java’s default time zone is derived from the environment in which the JVM runs; APIs that obtain the system default can therefore return different values across hosts. Locale has a similar characteristic because a JVM maintains a default locale that can influence locale-sensitive operations. The safer approach is to make important defaults explicit at application boundaries. A timestamp formatted for interchange, for example, should not silently inherit workstation configuration: Java String timestamp = eventTime .atZone(ZoneOffset.UTC) .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); Using an explicit UTC offset removes the host time zone from this operation. The same principle applies to number parsing, character encoding, paths, sorting, and regional formatting. Defaults are convenient only while machines happen to share them. Architecture becomes more predictable when environmental choices are represented as declared inputs or explicit invariants. Java’s time API distinguishes instants, offsets, and full time-zone rules, including daylight-saving gaps and overlaps, making this distinction important whenever local time participates in application logic. Filesystem behavior creates a subtler form of skew. Git documents core.ignoreCasebecause filesystems including APFS, HFS+, FAT, and NTFS can require case-insensitive handling. Git probes this behavior when a repository is created or cloned and configures its operation accordingly. Consequently, a reference to Config.json may appear valid in one environment while deployment fails because the repository actually contains config.json. Reproducibility Starts Before Deployment Locking application dependencies is necessary, but it is not sufficient. The package manager, compiler, operating-system libraries, runtime, and build-time tools also participate in artifact production. npm’s npm ci, for example, requires an existing lockfile and exits when package.json and the lockfile disagree rather than modifying the lockfile to accommodate the current installation. That behavior makes dependency resolution more suitable for automated, repeatable builds. Shell node --version npm --version npm ci npm test These commands become significantly more meaningful when the Node.js and npm versions are controlled by CI rather than inherited from whatever happens to be installed. A successful workstation build should not define correctness as a clean build executed from declared inputs should. Hermetic build systems take this further. Bazel defines a hermetic action as one that consumes declared inputs rather than arbitrary files from the host filesystem and produces declared outputs. Sandboxing helps detect accidental dependencies on local tools or files. The architectural benefit extends beyond caching and build speed, as an undeclared dependency can become a build failure instead of remaining hidden until another machine lacks it. That changes an important relationship: Plain Text source + declared dependencies + declared toolchain ↓ artifact The workstation should not be an additional invisible operand in that equation. Containers Reduce Machine Drift, but They Do Not Erase It Containers are useful because they package a controlled userspace, but containerization does not make host differences disappear. CPU architecture remains significant. Docker treats linux/amd64 and linux/arm64 as separate platform targets and provides multi-platform builds for producing images across operating-system and architecture combinations. Native libraries, JNI dependencies, compiled Node modules, and architecture-specific executables can expose such differences immediately. A CI pipeline can turn architecture support into an executable requirement: Shell docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag registry.example.com/payments:${GIT_SHA} \ --push . This is stronger than discovering an architecture mismatch after deployment because the supported platforms become part of the build contract. Base images introduce another source of environmental movement. Container tags are references that can point to newer image content over time. Docker recommends digest pinning when exact image identity matters, as referencing a digest ensures that the same image version is selected even if the associated tag later changes. Dockerfile ARG RUNTIME_IMAGE FROM ${RUNTIME_IMAGE} ENV TZ=UTC COPY service.jar /app/service.jar ENTRYPOINT ["java", "-Duser.timezone=UTC", "-jar", "/app/service.jar"] The pipeline can provide RUNTIME_IMAGE as an approved repository plus immutable digest. Base-image updates then become explicit changes that can pass through build, test, review, and deployment rather than appearing because a floating tag moved. Containers therefore narrow the environment boundary, and they do not eliminate the requirement to define it. Development Environments Belong in Source Control Development machines do not need to reproduce every operational detail of production, but the toolchain that determines software behavior should be describable. The Development Container Specification exists to capture development-specific tools, settings, runtime configuration, and metadata in a machine-readable definition such as devcontainer.json. JSON { "name": "payments-service", "image": "ghcr.io/example/java-dev@sha256:<approved-digest>", "containerEnv": { "TZ": "UTC", "LANG": "en_US.UTF-8" } } The important property is not this particular format. The architectural goal is that a clean machine can reconstruct the required toolchain without depending on manually installed compilers, remembered shell configuration, stale global packages, or undocumented IDE state. This distinction separates documentation from reproducibility. A setup guide describes actions that are expected to produce an environment. An executable environment definition participates directly in producing that environment. The latter is easier to validate continuously and considerably harder to misinterpret. Environmental Assumptions Should Be Observable Even controlled environments can drift. Production diagnosis becomes easier when runtime identity is recorded at startup alongside application version and build commit. Java exposes runtime, architecture, time zone, and locale information directly, allowing relevant execution characteristics to be captured without exposing arbitrary environment variables. Java log.info( "runtime java={} arch={} timezone={} locale={} commit={}", Runtime.version(), System.getProperty("os.arch"), ZoneId.systemDefault().getId(), Locale.getDefault().toLanguageTag(), buildCommit ); This fingerprint turns environment comparison into evidence. A defect occurring only on aarch64, under a particular time zone, or under a different runtime version can be correlated with actual execution metadata instead of machine descriptions passed through incident discussions. Critical assumptions can go further and become startup invariants: Java if (!ZoneId.systemDefault().equals(ZoneOffset.UTC)) { throw new IllegalStateException( "Unsupported runtime timezone: " + ZoneId.systemDefault() ); } Failing immediately is often safer than allowing an unsupported environment to produce subtly incorrect state. Testing should follow the same principle. Dimensions intentionally supported by the software should vary in CI, while dimensions that must remain constant should be frozen. Cross-platform builds validate CPU targets. Clean package installation validates dependency locks. Case-sensitive environments expose path errors. Explicit time-zone tests reveal accidental dependence on host defaults. The distinction is architectural, as supported variability is tested and unsupported variability is removed. Architecture Must Define the Machine Boundary The lasting solution to environment-specific failures is not a larger setup document or a longer troubleshooting checklist. It is a tighter definition of system inputs where locked dependencies, controlled toolchains, pinned base images, explicit locale and time-zone behavior, validated CPU architectures, hermetic builds where practical, reproducible development environments, and observable runtime identity. Reproducible Builds describes reproducibility as an independently verifiable path from source to binary, and the same engineering discipline can be extended to the environment in which that binary executes. A reliable software architecture therefore includes more than source code, services, databases, and deployment topology. It also includes the assumptions connecting code to a machine. When those assumptions are declared, versioned, tested, and observable, environment-specific failures become ordinary engineering defects with traceable causes. When they remain implicit, every workstation quietly becomes a different architecture.

By Uthej Mopathi DZone Core CORE
When One Docker Swarm Replica Became Unreachable Over an Overlay Network: A Production Case With Traefik
When One Docker Swarm Replica Became Unreachable Over an Overlay Network: A Production Case With Traefik

Context The incident occurred in the production backend infrastructure of a mobile application. External HTTP traffic was handled by Traefik, deployed on Docker Swarm manager nodes. Traefik ran in swarmMode, used providers.docker, was configured to use the mgp_default network, and had lbswarm disabled so that routing used the required overlay network rather than the Swarm VIP. One of the critical services was mgp_router-app. It was an entry-point service with two replicas deployed to preserve availability if one of them failed. The service used placement constraints, max replicas per node = 1, and a Traefik health check configured for /actuator/health on port 8080. This meant we relied on external health checking and strictly controlled replica placement across nodes. Swarm-level load balancing was intentionally disabled. We needed routing control at the Traefik layer, along with the ability to quickly remove unhealthy backends based on their health check status. For this production setup, that provided more flexibility than the built-in Swarm load-balancing logic. What Happened The issue was neither a widespread outage nor a failure of the entire service. One replica became unreachable to Traefik over the overlay network: a Traefik instance on one of the manager nodes could not establish a connection to that replica’s backend endpoint. The second replica continued to work normally, so the service as a whole remained available. For regular traffic, the incident did not affect service availability: Traefik continued routing requests to the healthy replica and removed the unavailable backend from routing. During targeted manual testing of the unhealthy replica’s backend endpoint through Traefik, requests ended in a timeout or HTTP 503. The container appeared healthy, but only locally. The service responded inside the container, and /actuator/health returned UP. However, other containers and Traefik instances on remote nodes could not establish a connection to that replica through its IP address in mgp_default. Initial Checks The container had the same DNS name in both networks. This ruled out name mismatch as an independent cause of the unavailability: the issue occurred specifically at the network-connectivity level to the replica IP in mgp_default. First, we identified the problematic container and inspected its state: Shell docker ps --filter "name=mgp_router-app" --q docker inspect 6bd15b62f891 Next, we checked the networks connected to the container: Shell docker inspect 6bd15b62f891 --format '{{json .NetworkSettings.Networks}' | jq The container was attached to two networks: ingress with IP 10.22.3.137/22;mgp_default with IP 10.29.72.232/13. We then entered the container and checked whether the application was listening on the expected port: Shell docker exec -it 6bd15b62f891 bash ss -tuln The health endpoint also responded correctly: Shell curl "http://10.29.72.232:8080/actuator/health" The expected response was: Shell {"status":"UP"} At this point, it was clear that the issue was not the application itself, its startup, or its bind address inside the container. The service was alive, but that alone said nothing about its reachability from outside the container. Checking Connectivity Between Nodes The next step was critical: We checked whether another node in the cluster could reach the container using its overlay IP. Shell docker exec -it 373354916bfc bash ping 10.22.3.137 This was the IP from the ingress network, and the test confirmed ICMP reachability of the address in that network. That indicated the issue did not look like a complete loss of inter-node connectivity. The situation was different for the mgp_default IP: pinging it from other nodes failed, even though this overlay network was supposed to provide inter-container communication in Docker Swarm. This distinction was important. ICMP reachability of the address in ingress remained available, while connectivity to the replica through mgp_default was broken. Therefore, the problem was not the container itself or a complete network failure; it was a specific break in the network path used to reach that replica. What Docker Logs Showed The dockerd logs on the affected node provided another useful signal. They contained repeated messages related to Swarm network synchronization: Shell Jun 02 11:08:42 node-router2 dockerd: time="2025-06-02T11:08:42.462965233Z" level=warning msg="bulk sync to node 091d01df89ae failed: failed to send a TCP message during bulk sync: dial tcp 10.170.0.145:7946: i/o timeout" Jun 02 11:17:02 node-router2 dockerd: time="2025-06-02T11:17:02.581936915Z" level=error msg="Bulk sync to node 091d01df89ae timed out" The logs also contained NetworkDB stats entries: Shell Jun 02 11:07:08 node-router2 dockerd: time="2025-06-02T11:07:08.012509772Z" level=info msg="NetworkDB stats node-router2(2642aeb5172b) - netID:q9athb3moj967oxmb3y8xw4lj leaving:false netPeers:58 entries:13563 Queue qLen:2 netMsg/s:7" These messages do not prove a specific bug or root cause, but they indicate problems with exchanging network state between nodes. Combined with the endpoint being unreachable through mgp_default, they strengthened the working hypothesis of an overlay-networking issue. This was an important diagnostic signal: the Docker daemon was running, and the container was started, but the cluster’s network state was not ideal and was likely becoming inconsistent during node-to-node state exchange. Investigating the Hypotheses Hypothesis 1: The container itself was the problem. This hypothesis was not confirmed. A curl request inside the container to http://10.29.72.232:8080/actuator/health returned {"status":"UP"}, and ss -tuln showed that the service was listening on the required port. Hypothesis 2: The problem was port publishing or binding. This hypothesis was not confirmed. Even though port 10005 was reachable externally, that did not explain why Traefik could not reach port 8080 over the overlay network. The container worked locally, while the issue occurred on the network path between nodes. Hypothesis 3: Traefik itself was the problem. Probably not. Traefik was not the source of the failure; it was the first component that detected that the backend was unavailable from its node. Hypothesis 4: The Docker Swarm overlay network was the problem. This became the primary working hypothesis. The replica was reachable from inside its container but was not properly reachable from other nodes through its overlay IP. Shell curl http://10.29.72.232:8080 nc -vz 10.29.72.232 8080 The curl request worked locally, while nc -vz 10.29.72.232 8080 failed from other nodes. This indicated that traffic was not being routed correctly through the overlay network. An MTU/VXLAN issue was also considered. This is a plausible class of problems for Docker overlay networks, especially when there is an MTU mismatch or a Path MTU Discovery issue. In this case, however, it remained only a hypothesis rather than a confirmed root cause. Recovery We restored the problematic replica by forcing Docker Swarm to recreate the task through a rolling update: Shell docker service update --force \ --update-parallelism 1 \ --update-order start-first \ --update-delay 30s \ --detach=true \ mgp_router-app --force triggered an update even though neither the image nor the service configuration had changed. Swarm recreated the task for the affected replica and reattached it to mgp_default; after that, Traefik could reach it again over the overlay network. The rollout parameters were selected to avoid affecting service availability: --update-parallelism 1 – update one replica at a time;--update-order start-first – start the new task before stopping the old one;--update-delay 30s – give the new task time to start and pass its checks. Restarting Docker on the node and placing the node into drain mode were considered fallback measures, but they were not required to resolve this incident. Why It Happened The exact upstream root cause cannot be stated without additional evidence. However, the most well-supported explanation is that a local network-state inconsistency occurred in the Docker Swarm overlay network. As a result, one replica remained operational inside its container, but became unreachable through its overlay IP. The most plausible explanations include: the overlay network on the node was not fully ready when the container started;an issue in Docker libnetwork;an outdated VXLAN routing table;an iptables issue or a race condition while updating network rules;a node restart without restarting the Docker daemon;an issue with a Swarm endpoint that was not properly advertised or connected. These situations are particularly unpleasant because Docker continues to consider the container running and its internal health check green, while the network path to the replica is already partially broken. Key Takeaways The main conclusion is simple: a healthy container does not necessarily mean a reachable replica. This is critical in production because: a local curl inside the container does not prove external reachability;an internal health check does not guarantee that Traefik can reach the backend;Swarm may remain formally healthy even when one replica has dropped out of normal network connectivity. Therefore, in such clusters, it is necessary to check not only container state but also inter-node reachability over the overlay network. What to Monitor To prevent similar incidents, it is useful to monitor not only service health but also the cluster’s network plane. The minimum set of checks includes: reachability of container overlay IPs from other nodes;health checks through Traefik;bulk sync and NetworkDB errors in Docker logs;recurring issues on the same node;consistency between a container’s local health and its external reachability. It is also useful to have a script that specifically checks connectivity between nodes through the overlay network, not just through ingress or localhost. Conclusion This case demonstrated several important points. First, a container can be healthy locally while remaining unreachable externally. This is especially important for services that handle user traffic. Second, Traefik is a good early indicator of network problems. It can reveal that a backend on one node has stopped responding. Third, Docker Swarm overlay networking can degrade locally. In that case, the cluster may appear operational, while one replica has already dropped out of normal routing. Fourth, such incidents must be diagnosed along the network path, not only through application state. First, check the container from the inside, then verify reachability from other nodes, then inspect Docker logs, and only after that draw conclusions about the root cause.

By Denis Tiumentsev
Your Visual Regression Suite Is Mostly Testing Anti-Aliasing
Your Visual Regression Suite Is Mostly Testing Anti-Aliasing

Visual regression testing has a dirty secret: most of the failures it reports are not bugs. They are anti-aliasing differences, sub-pixel layout shifts, font-hinting changes between OS versions, a shadow that moves three pixels, or a GPU driver that rounds a gradient differently. Every one of those trips a pixel-diff, a human triages it, marks it "not a real defect," and re-baselines the golden image. Multiply that by every screen, every platform, every appearance mode, and the maintenance cost of the test suite quietly exceeds the cost of the bugs it catches. I have spent years leading rendering-quality engineering for systems that ship across iOS, macOS, visionOS, and CarPlay, and the conclusion I keep arriving at is that pixel comparison is the wrong abstraction for validating a rendered scene. The question we actually care about is not "is this frame byte-identical to the golden image?" It is "does this frame show the right thing?" Those are different questions, and the gap between them is where semantic visual validation lives. Why Pixel-Diffing Fails at Scale Pixel-level comparison — whether raw MSE or a perceptual metric like SSIM — measures signal similarity, not meaning. SSIM (Wang et al., 2004) was a real improvement over mean-squared error because it models luminance, contrast, and local structure the way the human visual system roughly does. But it is still a low-level statistic. It will happily flag a difference no user could ever perceive, and it has no concept of whether the content of the scene is correct. Two Structural Problems Make This Worse as You Scale The False-Positive Tax A rendering pipeline is nondeterministic at the pixel level across environments. Anti-aliasing, text rasterization, sub-pixel positioning, and GPU-dependent gradient rounding all vary by device and OS build without any defect being present. A strict pixel-diff treats all of this as failure. Loosen the threshold to suppress the noise, and you start missing the defects you actually care about. There is no threshold that separates "shadow rounded differently" from "the bridge failed to render" — because to a pixel-diff, both are just pixels that changed. Combinatorial Baseline Explosion A single view does not need one golden image. It needs one per platform, per resolution, per light/dark appearance, per dynamic-type size, per locale. For a 3D map surface — with a Mercator-to-globe transition, buildings, terrain, labels, and navigation overlays — the baseline matrix is enormous, and every intentional design change invalidates all of it at once. The suite spends more engineering time being re-baselined than finding bugs. The insight is that these are not tuning problems. They are a category error: we are using a similarity metric to answer a semantic question. The Architecture of Semantic Validation Semantic visual validation replaces "compare pixels to a golden image" with "extract meaning from the frame and check it against intent." A multimodal model looks at the rendered frame and answers structured questions about what it contains, and the test asserts on those answers. The pipeline has four stages: stageresponsibilityimplementation notes Capture Grab the rendered frame deterministically Native rendering-API hooks; capture at a fixed camera pose, seed, and time-of-day so the scene is reproducible even if pixels are not Normalize Prepare the frame for the model Downscale to the model’s input resolution; strip volatile overlays (clocks, FPS counters); redact any user data before it leaves the device Reason Extract semantic content and judge it A vision-language model (e.g., Qwen-VL) run with constrained/structured decoding, returning a typed verdict — not free text Decide Convert the verdict into a CI outcome Parse structured output; apply calibrated thresholds; emit pass / fail / uncertain The important architectural decision is at the Capture stage: you must make the scene deterministic (fixed pose, seed, lighting) even though the pixels are not. That is what lets a probabilistic model produce stable decisions. Implementation: Structured Output and CI Determinism The hardest objection to using an LLM in a test pipeline is nondeterminism — a test that returns different answers on the same input is worse than useless. You defeat this with four disciplines: pin the model version, decode at temperature 0, force structured (schema-constrained) output, and gate only on high-confidence verdicts while routing uncertainty to humans rather than failing the build. Python # Reference implementation - illustrative, not drop-in. API names follow the # Qwen-VL / DashScope pattern; adapt to your serving stack. What matters is the # structure (schema-constrained output, deterministic decoding, a calibrated # confidence gate, and a three-way verdict), not the specific client. from pydantic import BaseModel, Field MODEL = "qwen-vl-2.5" # pin the exact version; a model upgrade is a test change CONFIDENCE_GATE = 0.85 # derived from calibration (see below), not guessed class Verdict(BaseModel): passed: bool confidence: float = Field(ge=0.0, le=1.0) defects: list[str] # e.g. ["building geometry missing", "label overlap"] rationale: str def validate_frame(frame, expected_context: str) -> str: """Return 'pass', 'fail', or 'uncertain'. Never raises on ambiguity.""" result = client.infer( model=MODEL, image=frame, prompt=( f"You are validating a rendered map frame. Expected: {expected_context}. " "Report geometry defects, missing elements, and label collisions." ), response_schema=Verdict, # constrained decoding -> always valid, typed JSON temperature=0.0, # deterministic decoding seed=42, ) # Low confidence never hard-fails CI -> avoids a flaky pipeline. if result.confidence < CONFIDENCE_GATE: return "uncertain" # -> human review queue; pixel-diff tripwire still runs return "fail" if not result.passed else "pass" Two points readers should not miss. First, response_schema (constrained decoding) is what makes the output parseable every time — you are not regexing prose, you are getting a validated object. Second, even at temperature=0, token-level output is not guaranteed bit-identical across hardware and batching. That is fine, because you are not asserting on tokens — you are asserting on a decision (passed) that is stable well away from the confidence gate. Treat the model as a classifier, not a text generator. Model Evaluation and Calibration A confidence score the model emits is meaningless until you calibrate it. You build a labeled ground-truth set — frames you have manually classified as correct or defective — and measure the model against it: precision, recall, and a reliability diagram (does "0.9 confidence" actually mean right 90% of the time?). The confidence gate above is chosen from the precision-recall curve to hit a target false-negative rate, because in QA a missed defect costs more than a false alarm. Calibration is also environment-specific. A model tuned for standard daylight scenes will be over- or under-confident on high-contrast night rendering. Maintain per-environment operating thresholds, and re-run calibration whenever you change the model version, the prompt, or the rendering environment — each of those is a change to the test's behavior and deserves the same rigor as changing test code. Handling False Positives and False Negatives Semantic validation trades one failure mode for two. It largely eliminates the pixel-diff false-positive tax (perceptually-invisible differences stop failing the build). But it introduces false negatives (the model misses a subtle defect) and model false positives (the model hallucinates a defect that is not there). You manage this with layered defenses: Tiered validation: Keep a cheap pixel-diff as a tripwire for regions that must be exact — legal text, logos, brand color swatches — where semantic tolerance is wrong. Use the model for everything spatial and contextual.Three-way verdicts: 'uncertain' is a first-class outcome. It routes to a human review queue; it never fails the build. This is what keeps the pipeline non-flaky.Standing ground-truth audits: Periodically re-score the model against the labeled set and track precision/recall over time. A drop is your early warning that a model or prompt change regressed the validator itself. CI/CD Integration In the pipeline, semantic validation runs as a stage that only invokes the model on views whose rendering inputs actually changed — you do not pay for inference on untouched screens. A high-confidence 'fail' blocks the merge with the model's defects list attached to the check. 'uncertain' posts to a review queue and does not block. Every run logs the model version, prompt hash, confidence, and verdict, so a failing check is reproducible and explainable rather than an opaque "the AI said no." Cost, Latency, Privacy, and Observability Cost: Model inference is dollars-per-call, whereas pixel-diff is free. The tiered strategy is the control: cheap checks first, model only on changed or ambiguous views, batched where possible. Latency: Pixel-diff is milliseconds; VL inference is seconds. Run it asynchronously and in parallel across views so wall-clock stays flat, and reserve full semantic passes for pull requests rather than every commit. Privacy: Frame buffers can contain user content (a map centered on someone's home, a real address in a label). Normalize and redact on-device, and run inference inside your own trust boundary — never ship a user-bearing frame to an external API. This is a hard architectural constraint, not a policy checkbox. Observability: Track the `uncertain` rate, the false-positive/negative rate against the audit set, and confidence drift over time. These are the health metrics of the validator; when they move, you investigate before you trust another green check. A Migration Strategy From Pixel-Based Testing Do not flip the switch. Migrate in shadow mode: Run in parallel, gate on nothing. Keep pixel-diff authoritative; run semantic validation alongside it, logging verdicts without blocking.Mine the disagreements. Every case where the two disagree is a labeled example — pixel-diff-failed-but-semantically-fine is your false-positive tax made visible; semantically-failed-but-pixel-passed is a defect your old suite would have missed.Calibrate on that data. Set your confidence gate and per-environment thresholds from the real disagreement distribution.Flip gating for high-confidence defects only. Let the model block merges where it is confident; keep pixel-diff as the exact-match tripwire.Retire redundant baselines. As confidence holds, drop the golden-image matrix for everything except exact-match regions. At each step, you have a fallback and real data, so the migration is reversible and evidence-based rather than a leap of faith. Conclusion The move to semantic validation is a change in what we assert, not just which tool we run. Pixel-diffing asks "did the pixels change?" — a question whose answer is almost always "yes, and it doesn't matter." Semantic validation asks "is the scene correct?" — the question we actually have.

By Ridhdhi Desai
How Long-Running ECS Tasks Survive Rolling Deployments Without Duplicating Output
How Long-Running ECS Tasks Survive Rolling Deployments Without Duplicating Output

The first deploy of the new pipeline shipped cleanly: metrics looked right, the deployment circuit breaker came back green, and the on-call channel stayed quiet. Two days later, a downstream consumer team pinged us. Their dashboard showed the same five-minute snapshot reported twice, by two upstream task runs that did not agree on the values. Both runs had completed normally. The problem was an assumption ECS makes about the tasks it replaces. The pipeline ran as an ECS service on Fargate with a desired count of one. The container looped on a five-minute cadence: at each boundary it pulled inputs, computed a network state snapshot for that window, wrote the result to S3, and slept until the next boundary. One iteration took about ninety seconds. The service used a textbook rolling update: minimum healthy percent of 100, maximum percent of 200, deployment circuit breaker enabled. The configuration was correct for the workload it was designed for. A rolling deploy of a stateless web server and a rolling deploy of a long-running task are different problems. For a web server, each request is a discrete unit and the load balancer routes around tasks being drained. For a task that runs ninety seconds and produces an output, a deploy that lands mid-iteration leaves two instances alive: the old one finishing under the old image and the new one starting under the new image. Both compute the same window, read the same inputs, and target the same output destination. Both exit cleanly, so the circuit breaker sees nothing. Downstream, there are now two versions of the same logical work, and the storage layer cannot tell which one is authoritative. The fix lives in the output layer rather than the deployment configuration, because ECS has no visibility into what a task's output means. Idempotent keying at the write boundary lets the system tolerate duplicate execution by collapsing duplicate outputs into a single canonical result. It adds no coordination between tasks, does not slow deployments, and holds for unplanned restarts as well as planned rollouts. Why ECS Rolling Deployments Break Long-Running Tasks Two parameters govern an ECS service deployment: minimumHealthyPercent and maximumPercent. At 100 and 200, the scheduler starts tasks under the new task definition before stopping any existing ones, and it considers the deployment complete once the new task count reaches the desired count and the old tasks have stopped. For a stateless service behind a load balancer, this is the right model. For a task that produces output, it assumes the running task can be replaced without semantic consequence, and that assumption does not hold. Here is how it played out for us. The service task, running revision N, started an iteration at the top of a five-minute window. About thirty seconds in, a deploy rolled out revision N+1. ECS started the new task and left the old one running until its stop timeout. The new task began an iteration for the current window on startup, because that is what its loop does. The old task wrote its result. The new task wrote its result. Both writes went to S3 under keys that included the task's launch timestamp, so they landed as two objects. The downstream consumer listed the window prefix, found two candidates, and picked one by listing order. Different consumers picked differently, and their metrics disagreed. This failure does not show up in load tests. Health checks pass and the deploy dashboard stays green. The bug surfaces later, somewhere downstream, as an inconsistency in metrics, a flickering chart, or a model that disagrees with itself. By the time someone correlates the symptom to a deployment window, several deploys have passed, and consumers have already mixed the duplicate writes into their reads. We found it only because the consumer team's anomaly detector tripped on the divergence. Code block 1 shows the Terraform that produces this behavior by default. The task definition and the deployment configuration are both correct. The problem is the interaction between the rolling update and the application's write semantics, which Terraform cannot see. Code block 1: ECS service and task definition with a by-the-book rolling update Plain Text # ECS service running a single long-lived task. The container loops on a # 5-minute cadence and writes one network state snapshot per window. # The deployment configuration is by-the-book and still leaves two tasks # alive, computing the same window, during every rolling deployment. resource "aws_ecs_task_definition" "network_state_job" { family = "network-state-snapshot" network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] cpu = "2048" memory = "4096" execution_role_arn = aws_iam_role.task_execution.arn task_role_arn = aws_iam_role.task.arn container_definitions = jsonencode([{ name = "snapshotter" image = "${aws_ecr_repository.snapshotter.repository_url}:${var.image_tag}" essential = true environment = [ { name = "OUTPUT_BUCKET", value = aws_s3_bucket.snapshots.bucket }, { name = "JOB_VERSION", value = var.job_version }, { name = "WINDOW_SECONDS", value = "300" } ] }]) } # Rolling update: start the new task before stopping the old one. With a # 90-second iteration, the old task is usually mid-write when the new task # starts its own iteration for the same window. resource "aws_ecs_service" "network_state" { name = "network-state" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.network_state_job.arn desired_count = 1 launch_type = "FARGATE" deployment_minimum_healthy_percent = 100 deployment_maximum_percent = 200 deployment_circuit_breaker { enable = true rollback = true } network_configuration { subnets = var.private_subnet_ids security_groups = [aws_security_group.task.id] } } Detecting Duplicate Output Without Already Knowing You Have It Detection is often harder than the fix, because by the time the symptom is loud enough to investigate, the data has already been blended downstream. Two approaches cover it. Forensic scanning looks backward. Scan output destinations for multiple writes against the same logical work identifier. For S3, correlate PutObject records from CloudTrail or S3 access logs with the task ARN that wrote them: two distinct task ARNs writing to the same window key is the signature of this failure. The first write is not the problem; the second write from a different task is. Preventive instrumentation gives a live signal. Stamp every write with the writing task's ECS task ARN and task definition revision, then scan for windows with more than one distinct task ARN. Run this hourly during and after any deployment to the service. Code block 2 shows the audit script. It lists objects under a window prefix, groups by logical window, and reports windows with writes from more than one task. Code block 2: Audit script that flags duplicate task writes per window Python """ Audit logical-key duplicates across an S3 prefix where outputs are tagged with the writing ECS task ARN. Surfaces cases where two tasks wrote to the same logical job-key window, typically during rolling deployments. Run this hourly during and after any deployment to the service. Exit code 1 if duplicates are found, 0 otherwise, so it composes with CI. """ import boto3 from collections import defaultdict from datetime import datetime, timedelta, timezone S3 = boto3.client("s3") BUCKET = "network-state-snapshots" def window_of(key: str) -> str: # Expected key shape: snapshots/<yyyy-mm-dd>/<HHMM>/... parts = key.split("/") return "/".join(parts[1:3]) def audit(hours: int = 24) -> dict[str, set[str]]: cutoff = datetime.now(timezone.utc) - timedelta(hours=hours) paginator = S3.get_paginator("list_objects_v2") dupes: dict[str, set[str]] = defaultdict(set) for page in paginator.paginate(Bucket=BUCKET, Prefix="snapshots/"): for obj in page.get("Contents", []): if obj["LastModified"] < cutoff: continue head = S3.head_object(Bucket=BUCKET, Key=obj["Key"]) task_arn = head["Metadata"].get("x-task-arn", "unknown") dupes[window_of(obj["Key"])].add(task_arn) return {w: arns for w, arns in dupes.items() if len(arns) > 1} def main() -> int: dupes = audit() for window, arns in sorted(dupes.items()): print(f"DUPLICATE window={window} writers={len(arns)}") for arn in sorted(arns): print(f" writer={arn}") print(f"scanned windows with duplicates: {len(dupes)}") return 1 if dupes else 0 if __name__ == "__main__": raise SystemExit(main()) Idempotent Keying as the Structural Fix Idempotency at the output layer means a duplicate execution cannot produce a duplicate output. Two tasks doing the same logical work resolve to the same key, so the storage layer keeps exactly one result. The mechanism has two halves: a deterministic key derived from the work identity, and a conditional write that makes the second write a no-op. A workable scheme follows three rules. First, the key derives deterministically from the logical work identity, never from the task's own attributes. Task ARNs, launch timestamps, and container IDs all change between the old and new task, so any key that includes them defeats the purpose. The window boundary, the job version, and a fingerprint over the pinned input versions are stable across both tasks. Second, the key must be content-aware enough that genuinely different inputs never collide. Pin the input versions at the window boundary and hash them into the key. Two tasks that read the same pinned inputs produce the same key. A correction that arrives later pins different input versions and lands on a different key, which is correct: it is new work, not a retry. Third, publish through a conditional write. S3's If-None-Match conditional write fails the PUT when the key already exists, which is as close to a compare-and-swap as object storage offers. The first writer wins; the second sees the precondition failure and treats it as success, because the object it would have written is byte-identical to what is already there. Code block 3 shows the Rust implementation we landed on. The key is derived from the trigger window, a job version, and a fingerprint over the pinned input table versions. Code block 3: Deterministic output key derivation Rust //! Two task executions for the same trigger window and same pinned inputs //! always produce the same key, and therefore collide at the storage layer //! where the conditional write enforces single-writer semantics. use sha2::{Digest, Sha256}; pub struct WorkIdentity { pub window_start_epoch: u64, pub job_version: u32, pub source_table_versions: Vec<(String, u64)>, // resolved as of the window boundary, never read live } impl WorkIdentity { pub fn output_key(&self) -> String { let mut h = Sha256::new(); h.update(self.window_start_epoch.to_be_bytes()); h.update(self.job_version.to_be_bytes()); let mut pinned = self.source_table_versions.clone(); pinned.sort(); for (table, version) in &pinned { h.update(table.as_bytes()); h.update(version.to_be_bytes()); } let digest = h.finalize(); format!("snapshots/{}/{:x}/state.parquet", self.window_start_epoch, digest) } } The other half is the conditional write. The deterministic key collapses duplicate logical work into a single destination, and the conditional write turns the race between the two tasks into a single winner with no coordination between them. Code block 4: Conditional write with conflict retry Rust //! Of two tasks racing toward the same key, one PutObject succeeds and the //! other sees PreconditionFailed (treated as a no-op) or, if both are in //! flight together, ConditionalRequestConflict (retried, then no-op). use std::time::Duration; use aws_sdk_s3::{Client, primitives::ByteStream}; const MAX_ATTEMPTS: u32 = 5; pub async fn publish_once( client: &Client, bucket: &str, key: &str, payload: Vec<u8>, task_arn: &str, ) -> anyhow::Result<()> { for attempt in 1..=MAX_ATTEMPTS { // If-None-Match: "*" tells S3 to fail the PUT if the key already exists. // The check is server-enforced, so it holds even when client-side // coordination is impossible, as during a rolling deployment. let result = client .put_object() .bucket(bucket) .key(key) .body(ByteStream::from(payload.clone())) .if_none_match("*") .metadata("x-task-arn", task_arn) .send() .await; match result { Ok(_) => return Ok(()), Err(e) if is_precondition_failed(&e) => return Ok(()), // already published: no-op Err(e) if is_conditional_conflict(&e) && attempt < MAX_ATTEMPTS => { tokio::time::sleep(Duration::from_millis(200 * attempt as u64)).await; continue; } Err(e) => return Err(anyhow::Error::from(e)), } } Err(anyhow::anyhow!( "conditional write to {key} did not settle after {MAX_ATTEMPTS} attempts" )) } Trade-Offs and Adoption Guidance Idempotent keying has costs. The deterministic hash adds single-digit milliseconds per job when it runs over input metadata, and more if it hashes payloads; keep the fingerprint over pinned input versions, not over data. The conditional write adds one round trip with retry backoff, which is negligible next to a ninety-second iteration. A less obvious cost is operational flexibility. Once outputs are content-keyed, you cannot rerun a job and overwrite its result by accident, which is the point, but it also means deliberate replays need a new job version. Build the job version into the key from the start so replays are a first-class operation rather than a hack around the scheme. Storage growth is the second consideration. Duplicate writes are dropped rather than overwritten, but successive job versions and new logical windows each add objects. A lifecycle policy on the output prefix keeps this bounded; the keying scheme makes objects immutable, so expiring old windows is safe. Roll this out incrementally. Start with the audit script running hourly against production to measure how often duplicate writes actually happen. Then ship the keyed write path behind the old path in shadow mode: compute the key, log it, but keep writing to the old location. When the shadow keys match the expected single-winner behavior across a few deployments, cut over. The audit script becomes your ongoing verification. The general lesson is that long-running compute under rolling deployments is a regime where the orchestrator cannot give you correctness, because the orchestrator does not understand your output. Pushing correctness down to the write boundary, where the semantics live, turns a deployment-time race into a property of the data. If you take one habit away from this, take this one. Write the property-based test for your keying scheme before you build the rest of the pipeline. Two executions with the same work identity must produce the same key; two executions with different pinned inputs must not. That test is the contract everything else hangs on. References AWS, "Amazon S3 now supports conditional writes," AWS What's New, August 2024. https://aws.amazon.com/about-aws/whats-new/2024/08/amazon-s3-conditional-writes/ AWS, "Deploy Amazon ECS services by replacing tasks (rolling update)," AWS Documentation. https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html AWS, "How to prevent object overwrites with conditional writes," Amazon S3 User Guide. https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html AWS SDK for Rust, aws-sdk-s3 crate documentation. https://docs.rs/aws-sdk-s3/ proptest crate, property-based testing for Rust. https://docs.rs/proptest/

By Kiran Kumar Manku
Building a Scope-Constrained AI Agent for Automated CI Failure Repair
Building a Scope-Constrained AI Agent for Automated CI Failure Repair

CI failures often expose a narrow defect through a broad stream of evidence such as compiler output, test reports, stack traces, changed files, dependency metadata, and build logs. Large language models can turn that evidence into candidate repairs, and recent industrial research has shown that LLM-assisted CI repair can resolve a meaningful share of compilation failures when the model receives carefully selected context. The difficult part is not generating a patch. It is ensuring that automated repair changes only code justified by the failure, leaves protected files untouched, and proves that the original failure is actually fixed. Automated program repair research has repeatedly identified patch overfitting as a core risk: a patch can satisfy available tests without being semantically correct. Capture Failure Evidence Before Asking for a Fix A repair agent should start from structured CI evidence rather than a raw prompt containing an entire build log. GitHub Actions artifacts can persist test output, logs, and other files after a job completes, making them a practical handoff format between the failing job and a later repair job. For a Maven project, the failing job can retain both console output and Surefire reports: YAML - name: Run tests run: ./mvnw test 2>&1 | tee ci.log - name: Upload failure evidence if: failure() uses: actions/upload-artifact@v4 with: name: failure-evidence path: | ci.log target/surefire-reports/ The repair process should normalize this data into a compact incident object containing the failing test, exception type, stack trace, failing command, commit SHA, and original pull-request diff. Current surveys of LLM-based software repair describe retrieval and analysis augmentation as important layers for repository-level repair, reinforcing the value of selecting failure-relevant context before generation. Before patch generation, the failure should be reproduced in an isolated checkout. Git worktrees support linked working directories and can create a detached worktree for experimental changes without disturbing the primary checkout. A repair job can create a disposable workspace and rerun only the failing test: Shell git worktree add --detach ../repair "$HEAD_SHA" cd ../repair ./mvnw -Dtest=PaymentServiceTest#shouldRetryTimeout test Maven Surefire supports selecting a specific test class and, for supported providers, individual test methods. If the failure cannot be reproduced, automatic patching should stop. A passing rerun provides no reliable oracle for judging a generated fix. Turn Failure Localization Into an Edit Boundary The central control is an explicit distinction between readable and editable files. A failing test, configuration file, or build script may be necessary for diagnosis without being safe for autonomous modification. A scope document can represent that boundary: YAML failure: PaymentServiceTest.shouldRetryTimeout readable: [src/test/java/com/acme/payment/**, src/main/java/com/acme/payment/**, pom.xml] editable: [src/main/java/com/acme/payment/**] protected: [src/test/**, pom.xml, .github/**, db/migration/**] The editable set can be derived from the original PR diff, stack-trace locations, and a small neighborhood of related production symbols. Git provides machine-readable changed-path output through git diff --name-only, while pathspecs can limit comparisons to selected repository regions. The model can inspect broader context, but the orchestrator should reject any patch that escapes the approved surface. A validator needs a repair baseline captured immediately before the model edits the worktree: Shell REPAIR_BASE=$(git rev-parse HEAD) git diff --name-only "$REPAIR_BASE" > repair-files.txt while read -r path; do case "$path" in src/main/java/com/acme/payment/*) ;; *) echo "Out-of-scope change: $path"; exit 1 ;; esac done < repair-files.txt This check matters because prompt instructions such as "modify only relevant code" are advisory, while a diff gate is enforceable. Git also provides exit-status behavior through git diff --exit-code, which is useful for automated policy checks. Generate a Patch, Not an Open-Ended Coding Session The repair model should receive a bounded request like normalized failure data, relevant source fragments, the original PR diff, editable paths, protected paths, and the exact verification command. The response should be structured around a patch rather than unrestricted shell execution. Java RepairRequest request = new RepairRequest( failure, relevantSource, originalDiff, editablePaths, protectedPaths, verificationCommand ); Patch patch = repairModel.generate(request); patchValidator.validate(patch); patchApplier.apply(patch); The model proposes the smallest plausible correction where the orchestrator remains responsible for permissions and validation. This separation reflects a broader trend in LLM-based program repair toward procedural and agentic workflows that combine generation with retrieval, analysis, execution feedback, and external control. For an initial implementation, tests and CI configuration should remain read-only. A failure that genuinely requires changing a test, dependency declaration, migration, or workflow can be escalated for manual review rather than allowing the agent to broaden its own permissions. Validate More Than a Green Build A candidate patch should first rerun the exact failing test. Only after that test passes should validation expand to the affected module and then the normal CI suite. Shell ./mvnw -Dtest=PaymentServiceTest#shouldRetryTimeout test ./mvnw -pl payment test ./mvnw verify Passing tests alone are not proof of correctness. Automated program repair research documents the overfitting problem, where generated patches satisfy the available test suite without correctly repairing the underlying behavior. Scope validation therefore acts as an additional safeguard. A successful repair should preserve protected files, avoid dependency changes, keep the patch reviewable, and retain the original failing test unchanged. A final guard can verify that tests were not modified during repair: Shell git diff --quiet "$REPAIR_BASE" -- src/test || { echo "Repair modified protected tests" exit 1 } The resulting output should be an evidence package rather than an opaque "fixed" status, but contain the root-cause summary, exact patch, changed-file list, focused-test result, module-test result, and full-CI result. That evidence makes the automated repair reviewable. To ensure a patch does not escape its intended scope, the workflow can run the agent in an isolated Git worktree and enforce explicit path-based allowlists. For example, a CI script might use git worktree add -d /tmp/agent main so the agent’s changes occur in a separate directory. After the agent generates a commit, the pipeline can run git diff --name-only origin/main...HEAD and check that all file paths match the allowlist (for instance, grep -qvE '^(src/payment/|config/)' rejects unauthorized modifications). Protected paths (e.g. .git/) would instantly be blocked by the allowlist. The pipeline then reruns the failing test class in isolation (mvn -Dtest=PaymentServiceTest test) to confirm the issue is fixed, and executes the full test suite. A quick diff or mutation-based check can detect if any test assertions were weakened or disabled. Finally, a canary merge or smoke-test job automatically undoes the change on failure. For instance, a GitHub Actions step might include the following so that any failing health check immediately triggers an automated rollback: SQL - name: Rollback if smoke tests fail if: steps.smoke.outcome == 'failure' run: ./rollback.sh These combined safeguards (worktree isolation, allowlist enforcement, retests, and rollback triggers) help guarantee the AI-generated patch is minimal, correct, and reversible. Keep the Repair Job Less Privileged Than the Code It Examines An autonomous repair workflow processes repository content and may execute code originating from a pull request, so CI permissions need careful separation. GitHub warns that privileged workflow_run and pull_request_target workflows must not blindly check out untrusted pull-request code, and recommends granting GITHUB_TOKEN only the minimum required permissions. A safer pattern is to perform diagnosis and patch generation with read-oriented credentials, store the candidate patch as an artifact, and use a separate trusted step to create a branch or pull request. Workflow artifacts should also be treated cautiously when produced by untrusted jobs. This prevents the repair agent from turning a build failure into a path toward repository write access. Conclusion A useful CI repair agent is not an LLM attached directly to a failing build. It is a constrained repair pipeline that captures structured failure evidence, reproduces the defect, narrows the editable surface, generates a minimal candidate patch, rejects out-of-scope changes, and validates the result from the failing test through the full CI suite. Industrial evidence shows that LLM-assisted repair can be effective in CI settings, but automated program repair research also shows why a green build cannot be treated as sufficient proof. Enforced edit boundaries, isolated execution, layered testing, and least-privilege automation convert model-generated code from an uncontrolled suggestion into a reviewable, evidence-backed repair mechanism.

By Uthej Mopathi DZone Core CORE
The Code Generation Bottleneck Is Gone. The Maintenance Bottleneck Isn't.
The Code Generation Bottleneck Is Gone. The Maintenance Bottleneck Isn't.

As AI-generated code expands across production systems, provenance becomes essential for tracing where each change originated, which model or prompt influenced it, and what evidence supports its review, ownership, and trustworthiness. AI-assisted development changes the economics of software creation. Source code can now be produced faster than many engineering organizations can review, understand, test, secure, and maintain it. The resulting risk is not simply "bad AI code." Recent research is more nuanced, as one 2026 study of real repositories found AI-generated code to be more verbose and to exhibit different duplication patterns, while another maintenance study found lower subsequent maintenance activity for sampled AI-generated files than for comparable human-generated files. A 2025 controlled study likewise found fewer defects in some LLM-generated solutions but also structural problems on harder tasks. The operational conclusion is therefore not that generated code is inherently inferior, but that code volume can grow faster than governance capacity. The language and stack are unspecified, so the implementation patterns below use Git, Java, Python, TypeScript, and GitHub-style CI as representative examples. The central strategy is to treat code generation as an input to an engineering control system so we can record provenance, establish ownership, prevent duplicate abstractions, enforce complexity budgets, map tests to changed behavior, automate small refactorings, constrain dependencies, and make security and repository policy executable. Code Abundance Requires Traceability and Ownership Traditional repositories mostly answer who committed a change and when. AI-assisted repositories also need to answer how a change was produced, which tool or agent contributed, which task authorized the change, and which human or team accepted responsibility. Git trailers are a practical mechanism because Git supports structured key-value metadata at the end of commit messages and can parse that metadata with git interpret-trailers. Shell git commit -m "Add invoice reconciliation" \ --trailer "AI-Origin: assisted" \ --trailer "AI-Tool: coding-agent" \ --trailer "Task: PAY-431" This metadata should describe provenance rather than transfer accountability to a model. Ownership remains organizational. A CODEOWNERS file can map sensitive or high-change paths to responsible teams, and GitHub rules can require approval from those owners before merge. GitHub rulesets can also require status checks, dismiss stale approvals after new pushes, and require approval of the latest reviewable push. Plain Text /payments/ @acme/payments /auth/ @acme/security /platform/ @acme/platform The important repository metric is therefore not generated lines per week. It is generated surface area without durable ownership. A repository can absorb high code generation throughput when every merged path has an accountable maintainer, searchable provenance, and enforceable review boundaries. Quality Budgets Must Stop Net-New Entropy Code abundance makes absolute cleanup programs impractical. Quality gates should instead prevent each change from making the repository worse. Duplication is a useful example. PMD’s Copy/Paste Detector supports multiple languages, can fail CI when duplicates are detected, and can normalize identifiers for structurally similar Java or C++ fragments. Its documentation explicitly recommends removing duplication rather than attempting to keep copied blocks synchronized. Shell pmd cpd --minimum-tokens 100 \ --dir src/main/java \ --ignore-identifiers This check becomes more useful when applied differentially, as existing duplication can be baselined, while newly introduced clones fail the pull request. The same principle applies to complexity. ESLint exposes a cyclomatic-complexity rule with a configurable maximum, allowing TypeScript or JavaScript repositories to turn complexity from a review opinion into a merge condition. JavaScript export default [{ rules: { complexity: ["error", { max: 12 }] } }]; A complexity budget should be paired with automated refactoring rather than only rejection. OpenRewrite operates on semantic trees and provides recipes for framework migrations, security fixes, and consistent source transformations while preserving formatting. That model is particularly suitable for code-abundant repositories because repetitive cleanup can be expressed once and applied consistently. Recent industrial research from Bloomberg also suggests that automated maintenance is easier to review when it arrives as very small pull requests, as its experimental system targeted roughly ten line changes and merged most generated patches in a one-month deployment, although the sample was small and should not be generalized broadly. Tests and CI Need Change-Level Intelligence A large test suite does not automatically create confidence. Maintenance becomes expensive when a changed function cannot be connected to the tests that exercise it. Coverage.py supports dynamic contexts that record which test function executed each covered line, making a code-to-test map available for Python repositories. Plain Text [run] dynamic_context = test_function That mapping enables risk-based CI. Changed files can select directly related tests first, while shared libraries, authentication code, persistence layers, or other high-blast-radius modules can trigger broader suites. Coverage remains a gate rather than a vanity percentage. For Java, JaCoCo can halt a build when configured line, branch, instruction, class, or complexity thresholds are not met. A compact CI stage can combine these controls without treating AI-originated code as a separate species: YAML quality: steps: - run: npm run lint - run: pmd cpd --minimum-tokens 100 --dir src/main/java - run: pytest --cov=src --cov-context=test - run: ./mvnw verify The stronger policy is "no unexplained quality regression," not "all generated code receives maximal testing." Provenance can increase scrutiny for high-risk generated changes, but changed behavior, dependency reach, security sensitivity, and ownership should determine the actual gate. Dependency and Security Hygiene Must Scale With Generation Code-generation workflows capable of editing manifests can expand a repository’s dependency surface as quickly as application code. Dependency review therefore belongs at pull request time. GitHub’s dependency review reports direct and indirect changes from manifests and lockfiles, includes vulnerability information, and can fail a required check when vulnerable packages are introduced. This supports a repository policy in which new dependencies require an explicit purpose, lockfile changes receive review, and unused packages are removed during routine maintenance. Security analysis should operate on every merge path rather than rely on prompt instructions. GitHub code scanning can run CodeQL and other analyzers on pull requests and the default branch, and CodeQL data-flow analysis can identify unsafe flows such as dangerous arguments or sensitive data leakage. Required code-scanning checks can block merging until findings are resolved. Runtime-facing checks provide a complementary boundary. OWASP ZAP’s baseline scan spiders a deployed target, performs passive scanning, supports configurable fail conditions, and is explicitly designed for CI/CD environments. Static analysis can therefore guard source changes while dynamic scanning checks observable application behavior. The important distinction is between generation-time guidance and repository-time enforcement, as model instructions are advisory, while CI gates are reproducible controls. Repository governance ties the system together. Rulesets should require code-owner approval for protected areas, passing quality and security checks, review of dependency changes, and fresh approval after material updates. Such controls keep throughput high without allowing generated volume to bypass the same engineering standards applied to manually written code. Conclusion The maintenance problem created by AI is fundamentally a throughput mismatch. Code production can accelerate immediately, while ownership, review capacity, testing, security analysis, and architectural coherence remain finite. Current research does not justify treating AI-generated code as uniformly worse than human-written code, but it does show meaningful differences in verbosity, structure, duplication, defects, and maintenance behavior that make passive governance increasingly risky. Sustainable code abundance therefore depends on converting maintenance expectations into repository mechanics where provenance is attached to commits, ownership is attached to paths, quality budgets are enforced on new changes, tests are mapped to behavior, refactoring is delivered in small reviewable patches, dependencies are examined before merge, and security checks are made mandatory. The durable advantage will not come from generating the most code. It will come from operating a codebase that can absorb generated code without allowing maintainability to collapse under its volume.

By Uthej Mopathi DZone Core CORE

Monthly Top DevOps and CI/CD Experts

expert thumbnail

Xavier Portilla Edo

Head of Cloud Infrastructure,
Voiceflow

Xavier hails from Valencia. He has earned degrees from the Polytechnic University of Valencia. He is a software developer with more than 5 years of experience; ranging from health to industry sector, learn and research, at everything from startups to the largest companies in the world, and working in-office to remote.
expert thumbnail

Boris Zaikin

Lead Solution Architect,
CloudAstro GmBH

Lead Cloud Architect Expert who is passionate about building solutions and architecture that solve complex problems and bring value to the business. He has solid experience designing and developing complex solutions based on the Azure, Google, AWS clouds. Boris has expertise in building distributed systems and frameworks based on Kubernetes, Azure Service Fabric, etc. His solutions successfully work in the following domains: Green Energy, Fintech, Aerospace, Mixed Reality. His areas of interest Enterprise Cloud Solutions, Edge Computing, High loaded Web API and Application, Multitenant Distributed Systems, Internet-of-Things Solutions.
expert thumbnail

Sai Sandeep Ogety

Director of Cloud & DevOps Engineering,
Fidelity Investments

Sai Sandeep Ogety is a globally recognized expert in Cloud, DevOps, and Infrastructure with over 12 years of IT experience. He holds a Master’s degree in Computer Engineering from Gannon University and specializes in cloud platforms like AWS, Azure, and GCP. Sai has significantly improved operational efficiency across various industries, particularly in financial services and fintech, through scalable cloud architectures and CI/CD automation. An advocate for cloud security, he ensures compliance with industry standards and excels in Kubernetes management and infrastructure automation using tools like Terraform and Ansible. As a dedicated researcher and mentor, Sai actively contributes to professional journals and engages with the tech community, sharing insights on emerging technologies and fostering the next generation of engineers.
expert thumbnail

Shamsher Khan

Sr. Engineer,
GlobalLogic Inc - A Hitachi Company

Cloud & DevOps Engineer with expertise in Kubernetes, security, and AI-driven automation. IEEE Senior Member with a focus on delivering scalable architectures and advancing cloud-native engineering practices through research and hands-on contribution to the community.

The Latest DevOps and CI/CD Topics

article thumbnail
Closing the Drift Loop: An AI Agent That Drafts Terraform Fixes
Drift detection is solved. Remediation still means a human at the console at 2 a.m. Here's a workflow where an agent drafts the fix and a PR gates it.
October 29, 2026
by Sidharth Shah
· 394 Views · 1 Like
article thumbnail
Solving Comprehension Debt in AI-Generated Codebases
Learn how the Trace, Tag, and Time-box (TTT) framework helps teams manage comprehension debt and review AI-generated code safely.
October 29, 2026
by Pushkar Gopalakrishna
· 437 Views
article thumbnail
Treat Salesforce Configuration Like Code: Release Discipline in a Copado Pipeline
Learn how to manage Salesforce metadata changes with Git, Copado deployment pipelines, conflict resolution, and automated regression testing.
October 28, 2026
by Sri Charan Reddy Pennaka
· 499 Views · 1 Like
article thumbnail
When a Coding Agent Fixes the Test Instead of the Bug
Green CI doesn't mean the bug is fixed. When a coding agent edits the test too, a swap check with Git worktrees shows what really made it pass.
October 28, 2026
by Serhii Romanov
· 618 Views · 4 Likes
article thumbnail
Genkit Flows as senro Steps: Extremely Powerful AI Pipelines in Go
Put a Genkit flow inside a senro step, and every model call becomes a cached, retryable graph node: change one document out of three, pay for one call.
October 26, 2026
by Xavier Portilla Edo DZone Core CORE
· 714 Views
article thumbnail
Adding Graph Retrieval to a RAG Pipeline With Python
Learn how to add a graph retrieval layer to a retrieval-augmented generation pipeline using Python, NetworkX, and relationship triples.
October 23, 2026
by Sriharsha Makineni
· 1,168 Views · 1 Like
article thumbnail
Building a Real Pipeline With senro: Monorepos, Caching, and Triggers
Workspaces, step caching, fanning out over every module in a monorepo, narrowing to what changed, and webhook triggers: senro for everyday pipelines.
October 23, 2026
by Xavier Portilla Edo DZone Core CORE
· 919 Views · 1 Like
article thumbnail
When RabbitMQ Accepts a Message but Does Not Route It to a Queue
RabbitMQ accepted messages, but some topic exchange bindings did not route them to queues because the runtime topic routing state was incomplete.
October 22, 2026
by Vamsi Krishna Gude
· 1,132 Views
article thumbnail
Introducing senro: A Pipeline Engine You Program in Go
senro is a new open-source pipeline engine where the pipeline is a Go program, not YAML. Run it locally, in containers, on Kubernetes, or over SSH.
October 21, 2026
by Xavier Portilla Edo DZone Core CORE
· 1,089 Views
article thumbnail
How to Keep AI Agents From Going Rogue Using Docker Sandbox
AI agents are powerful because they can autonomously plan and use tools. Sandboxes provide guardrails by isolating agents and restricting what they can access or execute.
October 21, 2026
by Siri Varma Vegiraju DZone Core CORE
· 1,422 Views
article thumbnail
Data Integration Landscape 2026: Event Streaming, API, and Batch in the Era of Agentic AI
Why event-driven architecture belongs at the center of enterprise integration, and how every major vendor maps across three paradigms.
October 20, 2026
by Kai Wähner DZone Core CORE
· 1,881 Views · 1 Like
article thumbnail
OwlVigil Open-Sources Its Go SDK for Enterprise AI
Discover OwlVigil's open-source Go SDK for integrating AI gateway APIs, model routing, cost management, access control, and observability.
October 20, 2026
by Jake Tao
· 957 Views
article thumbnail
Engineering Audit-Ready Data Pipelines for Regulatory Reporting
Build traceable, audit-ready regulatory data pipelines with immutable data, reconciliation, lineage, and human approval.
October 19, 2026
by SUNILKUMAR REDDY Eraganeni
· 858 Views · 1 Like
article thumbnail
Hot-Reloading Kubernetes Secrets and ConfigMaps in Go With mamori (No Pod Restarts)
Stop running kubectl rollout restart. mamori watches Secrets and ConfigMaps with native informers and reconciles changes into your running Go process.
October 19, 2026
by Xavier Portilla Edo DZone Core CORE
· 960 Views
article thumbnail
Node and Capacity Spillover: What Happens When Kubernetes Runs Out of Room
A pod stuck in Pending isn't broken; it's just out of room. Here's why that happens even with spare capacity, and how to design around it.
October 15, 2026
by Kishor Patil
· 1,277 Views · 1 Like
article thumbnail
"Works on My Machine" Is an Architecture Problem
"Works on my machine" exposes architectural gaps in reproducibility, dependency control, runtime consistency, and environment-aware system design.
October 13, 2026
by Uthej Mopathi DZone Core CORE
· 1,528 Views · 1 Like
article thumbnail
When One Docker Swarm Replica Became Unreachable Over an Overlay Network: A Production Case With Traefik
One Docker Swarm replica became unreachable to Traefik over the overlay network, while the service remained available through its healthy replica.
October 13, 2026
by Denis Tiumentsev
· 1,354 Views · 1 Like
article thumbnail
Your Visual Regression Suite Is Mostly Testing Anti-Aliasing
Visual regression testing flags mostly noise, not bugs. A semantic, CI-safe approach that verifies what a rendered frame means, not whether its pixels changed.
October 13, 2026
by Ridhdhi Desai
· 1,118 Views
article thumbnail
How Long-Running ECS Tasks Survive Rolling Deployments Without Duplicating Output
Rolling ECS deploys can leave two tasks computing the same window, duplicating output. Fix it with a deterministic key and a conditional write.
October 13, 2026
by Kiran Kumar Manku
· 845 Views
article thumbnail
Building a Scope-Constrained AI Agent for Automated CI Failure Repair
Build a scope-constrained AI agent that diagnoses CI failures, limits code changes, generates minimal patches, and validates repairs through targeted tests and CI gates.
October 12, 2026
by Uthej Mopathi DZone Core CORE
· 1,414 Views · 1 Like
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • ...
  • Next
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook
×