Spend a week on call for a platform team and you will meet the same outage twice. Someone bumps a base image, retags a shared Terraform module, edits a CI template that forty pipelines include. The PR is green. The repo it lives in is fine. Something two repositories away falls over an hour later, and the person who shipped the change had no way to know that repository existed, because nothing in the repo they were editing points outward to the things that depend on it.
I wanted to know how common that shape really is, so I stopped guessing and measured it. Across nine public platform and infrastructure organisations and 1,297 active repositories, roughly one in four repos is a dependency of at least one sibling repo in the same organisation, and that edge is invisible from inside the repo you are changing.
I should say the obvious thing first. I build Riftmap, and Riftmap is the scanner I used. That is a conflict of interest, so this post is written so you can check me rather than trust me. The sample rule is stated before the results. The confidence model is stated, including what it throws away and why. The two false positives my own tool produced are in here, named, with the mechanism. The one organisation that did not fit the finding is in here too, along with the spot-check I ran to make sure the result was not a bug in my own parser. The raw edge list is published. If any of it is wrong, it is wrong in public.
What I scanned, and what counts as an edge
I scanned nine public GitHub organisations in the platform and cloud-native space and parsed the dependency edges their manifests actually declare. The sample: cloudposse, prometheus, open-telemetry, fluxcd, argoproj, ansible-collections, crossplane-contrib, terraform-aws-modules, and grafana.
The rule, fixed before I looked at any result: public organisations in the platform, infrastructure, or cloud-native space, chosen to span three shapes rather than one. Module farms that exist to publish reusable components (cloudposse, ansible-collections, terraform-aws-modules, crossplane-contrib). Frameworks (fluxcd, open-telemetry). Product organisations that ship a thing (prometheus, argoproj, grafana). They range from 28 to 583 active repositories. I picked them for ecosystem and shape spread, not because I knew what they would look like.
The important word is parsed, not inferred. For every repo I read the edges declared in real manifest constructs. Terraform source blocks, including registry coordinates like terraform-aws-modules/vpc/aws. go.mod requires and replaces. Helm Chart.yaml dependencies. Dockerfile FROM lines. kustomization.yaml resources. GitHub Actions uses: references. npm package dependencies. Each reference is then resolved to the repo in the same organisation that actually produces it, so a Terraform source or a FROM becomes a real repo-to-repo edge rather than a string. This is not name-matching and it is not embeddings. It is the graph the repositories already describe about themselves.
Every edge carries a confidence score, and the score is a statement about how much resolution the edge needed, not a probability that a model assigned to it. A GitHub Actions uses: grafana/shared-workflows/actions/... reference has the organisation and the repository sitting in the string. There is nothing to resolve, so there is nothing to get wrong. A package coordinate is one step removed. A go.mod require, an npm package name, a Terraform registry source all have to be resolved to the repository that publishes them, which is deterministic but not free. A bare git URL sitting in a shell script or a README is a text signal rather than a declared dependency, so it scores 0.6 and never gets counted. Open-telemetry is the clearest illustration of what that bar throws away: 413 of its candidate edges were dropped, and 412 of them were bare git URL references.
Across the nine organisations the confidence bar discarded 1,698 of 4,843 candidate edges, 35% of everything the scanner saw. Every number below is computed on what survived, at confidence 0.8 or above, with archived repositories excluded, deduplicated to distinct repo-to-repo pairs, and counting direct dependents only.
One integrity check before the results, because it is cheap and it is the first thing I would ask. Every organisation’s scanned repo count matched GitHub’s live public_repos count. Grafana came back 586 of 586 with zero errors, of which 583 are active once the archived ones come out. No self-loops, and no workspace: or file: local-protocol residue leaking in as fake edges.
The most depended-on repo at Grafana isn’t Grafana
Grafana is the largest organisation in the sample, 583 active repos. Here is its top ten by direct dependents, with the manifest construct each count came from, so you can see for yourself which rows have room to be wrong.
| # | Repo | Direct dependents | What it is | Edge basis |
|---|---|---|---|---|
| 1 | grafana/shared-workflows | 188 | CI workflow definitions | Actions uses:, org and repo in the string |
| 2 | grafana/scenes | 143 | frontend framework | npm @grafana/scenes |
| 3 | grafana/grafana | 92 | the product | npm @grafana/* and go.mod |
| 4 | grafana/plugin-tools | 83 | plugin scaffolding | npm @grafana/plugin-tools |
| 5 | grafana/pyroscope-go | 75 | profiling SDK | go.mod require |
| 6 | grafana/otel-profiling-go | 72 | profiling bridge | go.mod require |
| 7 | grafana/plugin-ci-workflows | 63 | CI workflow definitions | Actions uses: |
| 8 | grafana/grafana-plugin-sdk-go | 60 | plugin SDK | go.mod require |
| 9 | grafana/plugin-actions | 53 | GitHub Actions | Actions uses: |
| 10 | grafana/k6 | 39 | load testing | first-party product |
The single most load-bearing repository in Grafana’s organisation is a repo of CI workflow definitions, with 188 repos importing it. The flagship product is third, at 92. Three of the top ten are CI plumbing.

I want to be precise about why the top row is worth trusting more than the rest of the post. shared-workflows is consumed through uses: grafana/shared-workflows/actions/.... The organisation and the repository are literally in the string. There is no name resolution to get wrong, no upstream package to collide with, no heuristic anywhere in the path. It is the least ambiguous edge type there is, and it produced the largest hub in the sample.
Those ten repos also absorb 868 of the 1,596 cross-repo dependency edges inside Grafana’s organisation, 54% of the coupling in an org of 583 repositories. The load is not spread evenly. It never is.
The hub is never the repo you would nominate
Ask a platform engineer which repo in their organisation is the scariest to change and they will name a service. Something with traffic. Something with a runbook.
In eight of the nine organisations I scanned, the most-depended-on repo is plumbing. grafana/shared-workflows, CI definitions, 188 dependents. cloudposse/terraform-null-label, a module whose entire job is generating consistent names and tags, 146. ansible-collections/ansible.netcommon, network utilities, 27. crossplane-contrib/provider-workflows, CI definitions again, 23. prometheus/common and prometheus/client_model, shared Go libraries and protobuf definitions, tied at 23, with procfs one behind them. fluxcd/pkg, a shared Go package, 16. open-telemetry/shared-workflows, CI definitions, 15. argoproj/pkg, a shared Go package, 4.
None of these are services. They are the naming module, the base image, the internal action, the shared utility package. They are also, and this is the part that matters operationally, exactly the repositories that nobody remembers owning. The blast radius concentrates in the things that were set up once, years ago, by an engineer who has since moved teams or left the company. The dependency that used to live only in someone’s head is the same dependency that turns out to have 188 repos riding on it.
The ninth organisation, and the check I ran on my own parser
Terraform-aws-modules is the one organisation in the sample with no hub at all. Its busiest repos top out at two direct dependents apiece — a three-way tie between terraform-aws-lambda, terraform-aws-acm and terraform-aws-kms, which is another way of saying there is no hub. The entire organisation contains eleven same-org edges.
That is precisely the result you should be suspicious of, because it is also what a broken parser looks like. Modules in that org reference each other through registry coordinates, source = "terraform-aws-modules/alb/aws" rather than a git URL, and if my resolver quietly failed to follow registry coordinates back to the repository that publishes them, I would produce a near-empty graph and then write a confident paragraph explaining what it meant.
So I checked it against ground truth before I believed it. terraform-aws-atlantis/main.tf calls four sibling modules by registry source: terraform-aws-modules/alb/aws at 10.2.0, acm/aws at 6.1.1, ecs/aws at 6.7.0, and efs/aws at 2.0.0. Riftmap’s graph contains all four, with the versions matching. The resolver follows registry coordinates. The sparsity is real.
The reason for it is dull and true. Most of the modules in that organisation are leaves. vpc, s3 and iam do not compose siblings, they compose AWS resources. Only the composers do, atlantis, eks, route53, notify-slack, and there are not many of them. So terraform-aws-modules is neither support nor counterexample for the hub finding. It is an organisation with no hub to find. I am telling you that rather than dropping it, because dropping the org that does not fit is how you end up with a finding that is not true.
One in four repos is load-bearing, and that is a floor
Across the sample, 320 of 1,297 active repositories are a dependency of at least one sibling in the same organisation. That is 24.7%, roughly one in four.
It holds three ways. Pooled across all repos it is 24.7%. The median organisation is 25.3%. The mean organisation is 24.8%. Drop the two largest organisations, grafana and cloudposse, which together are 64% of the sample, and the remaining seven still come to 20.5%. Grafana, the largest org in the sample, sits at 27.8%, above pooled, so this is not an artefact of small organisations either.
Here is every organisation, so you can do your own arithmetic on it.
| Organisation | Shape | Active repos | Load-bearing | % | Busiest repo | Direct dependents | Largest SCC |
|---|---|---|---|---|---|---|---|
| grafana | product | 583 | 162 | 27.8% | shared-workflows | 188 | 53 |
| cloudposse | module farm | 241 | 61 | 25.3% | terraform-null-label | 146 | 1 |
| ansible-collections | module farm | 137 | 26 | 19.0% | ansible.netcommon | 27 | 1 |
| crossplane-contrib | contrib farm | 86 | 4 | 4.7% | provider-workflows | 23 | 1 |
| open-telemetry | framework | 80 | 19 | 23.8% | shared-workflows | 15 | 6 |
| terraform-aws-modules | module farm | 59 | 8 | 13.6% | terraform-aws-lambda | 2 | 1 |
| prometheus | product and libraries | 51 | 17 | 33.3% | common / client_model | 23 | 2 |
| fluxcd | framework | 32 | 15 | 46.9% | pkg | 16 | 2 |
| argoproj | product | 28 | 8 | 28.6% | pkg | 4 | 1 |
| Pooled | 1,297 | 320 | 24.7% | grafana/shared-workflows | 188 |
One thing not to do with that table: do not average the percentages by shape. Two of the four module farms carry known measurement gaps. Crossplane-contrib’s real hub lives in a different organisation, and terraform-aws-modules has eleven same-org edges in total. A class mean computed on top of two structural undercounts is arithmetic dressed up as a finding, and I would have to take it back the first time somebody actually looked.
What the number is not is stable. The per-org range runs from 4.7% to 46.9%, and anyone telling you a single percentage generalises to your estate is selling something. Treat 24.7% as the shape of the problem, not as an estimate of your number.
Now the objection I would raise if I were reading this. You picked organisations that publish shared modules, so of course their repos depend on each other. Except it runs the other way. Flux (46.9%) and Prometheus (33.3%), neither of which exists to publish reusable modules for siblings, top the table. Cloudposse, 241 Terraform modules built for nothing but reuse, sits at 25.3%, below both. The organisations built around sharing are not the ones that turned out to be most coupled.
And every methodological choice I made pushes the number down, not up. Archived repositories are excluded. The confidence bar dropped 35% of candidate edges before anything was counted. Only direct dependents are counted, not transitive. Only declared manifest edges are counted, which means runtime coupling is completely invisible: shared databases, internal APIs, message topics, none of it appears. Only same-organisation edges are counted, which is why crossplane-contrib reads a suspiciously low 4.7%. Its real hub, crossplane-runtime, lives in the crossplane organisation rather than crossplane-contrib, so a same-org scan structurally cannot see it. The organisational boundary is an accident of GitHub, and the dependency does not care.
Every choice makes the number smaller. It is still one in four.
Two false positives I had to throw out
Two entries in my first Grafana top ten were wrong, and both were wrong the same way. A fork that preserves the upstream package identity will absorb every same-org import of the upstream. That is the failure mode, stated up front, because it bites hardest on exactly the repos that look most important, and because if you are building this yourself you have the bug until you prove you do not.
grafana/opentelemetry-go came out at number two with 182 dependents. It is Grafana’s fork of upstream OpenTelemetry Go, and a fork keeps the upstream module path, go.opentelemetry.io/otel. So when Grafana repos put a plain go.mod require on upstream otel at v1.43 and v1.44, the resolver matched that module path to the repo in the organisation that declares it, which is the fork. The repo carried 184 inbound edges from 182 distinct repositories. Exactly one of those edges was a genuine replace directive pointing at the fork. The other 183 were repos importing upstream and being told they depended on a sibling.
grafana/k6-DefinitelyTyped, number four at 137, is the same bug in npm. It is a fork of DefinitelyTyped, so it declares @types/lodash, @types/react, @types/jest and several hundred more. Every TypeScript repo at Grafana with @types/* in its devDependencies got an edge to it. That is not a hub. That is a fork of the type registry collecting an edge from everything that has ever installed types.
Both are resolver bugs in my own product and both are now filed.
Neither one touched the headline. shared-workflows is an Actions path, which is why I lead with it. Removing both leaves the pooled figure unchanged at 24.7%, takes Grafana’s load-bearing count from 163 to 162, and moves grafana/grafana from fifth to third, which is a better story than the one I started with. In the other eight organisations the busiest repo is a first-party project with no upstream twin to collide with, so the bug had nowhere to bite.
I found this because I went looking for it. If I had not, the number two entry in my headline table would have been an artefact, and the first Grafana engineer to read the post would have said so.
Why I report direct dependents and not transitive
The first version of this analysis reported transitive blast radius, and the numbers were nonsense in a way that took me a while to see. Ninety-one repos in Grafana’s organisation, more than half of everything I had counted as load-bearing, each came back with roughly the same figure. About 315 transitive dependents. Ninety-one different repositories, one answer.
That is not a distribution, it is a fixed point, and it has an obvious cause once you go looking for one. Grafana’s Go repositories form a 53-repo strongly connected component. Alloy, Beyla, augurs, dataplane, Grafana core, the AWS and Azure SDK families, they all mutually import. Inside a strongly connected component every member reaches every other member, so reverse reachability collapses to roughly the same value for all of them, and a “transitive blast radius” stops measuring the repo and starts measuring the mesh. Removing the two forked repos above only took that component from 55 repos to 53, so the mesh is real and not an artefact of my own bug.
It splits cleanly by ecosystem. The Terraform and Ansible organisations are acyclic, largest strongly connected component of size 1. The Go organisations cycle: grafana 53, open-telemetry 6, prometheus 2, fluxcd 2. Terraform module graphs cannot contain import cycles. Go module graphs across sibling repositories very much can.
So I report direct dependents only. A transitive count would overstate blast radius in a Go mesh, and I would rather publish a smaller number I can defend than a bigger one I cannot.
A repository declares what it depends on, never what depends on it
Every manifest points outward. go.mod lists what this repo imports. Chart.yaml lists what this chart pulls in. The Dockerfile names the base image it starts from. Not one of them lists the repos on the other side of the arrow, because that half of every edge only exists at the organisation level. So no amount of looking inside a single repo, however good the linter or the agent, reconstructs it. This is structural. It is not a tooling gap that someone forgot to close.
A hand-maintained catalogue does not close it either, and this scan is a concrete demonstration of why. Nobody was ever going to file a catalogue entry for a CI workflow file. shared-workflows is not a service, it has no owner in anybody’s mental model, and it accumulated 188 dependents without a single person deciding it was important enough to write down. A catalogue is only ever as accurate as the last engineer who remembered to update it, and the repos with the largest blast radius are precisely the ones nobody remembers. Parsed, never catalogued, is the only version of this that survives contact with a real estate.
Symbol graphs do not close it either, and I want to be fair here, because they are genuinely excellent at what they do. Sourcegraph indexes programming-language symbols across repositories, which is a real and hard thing to get right, and it answers “where is this function used” better than anything else. GitLab Orbit is the most serious thing to arrive in this space, a properly resourced property graph of the whole SDLC, and it is validating rather than threatening to see GitLab build it. But a Terraform module resolving to a repo, a base image built by another repo, a Helm value feeding a downstream chart, a GitHub Actions workflow imported by 188 repositories, those are artifact edges. They do not live in the symbol layer and they do not live in the SDLC metadata layer. Symbol graphs and artifact graphs are different categories, and everything above is entirely in the artifact layer. The 188 that started this post is an Actions uses: reference, which no symbol indexer has any reason to model.
Give an agent every repo in your organisation and you have solved access. You have not given it the map of which repos hold each other up. Those are different gifts, and the second one is the one that pages you at 2am.
What this is not
This is public open-source data, and it is public because it is the only data I can show my work on. I cannot publish a scan of a bank.
So be careful about the transfer. Cloudposse is a module vendor. Grafana is largely a plugin ecosystem. Neither is your enterprise polyrepo, and the 24.7% is not an estimate of your organisation. What does transfer is the mechanism, because the mechanism has nothing to do with whether your code is public. A repository declares what it imports and never what imports it. That is true of your private GitLab estate for exactly the same reason it is true of Grafana’s.
Which means the honest conclusion is not a number. It is that you cannot know your number without looking, and almost nobody has looked.
Where this leaves you
If you operate more than about fifty repositories, the organisation-level dependency graph is not a nice-to-have visualisation. It is the missing half of every shared-component change you make. Before you bump that base image or retag that module, the question “which repos break” has a real answer, and it is very probably not the one in your head, because the answer is a CI workflow file that somebody set up in 2022 and nobody has thought about since.
That is the problem Riftmap exists to make boring. It scans a whole GitHub or GitLab organisation with one read-only token, parses the artifact edges, and answers the blast-radius question directly, for you and for the agents you point at your code. The data in this post came out of exactly that scan, bugs included. The raw edge list is here, together with a script that recomputes every number in the post from it. A live, clickable Grafana graph is planned and on the way; until it lands, you can poke a real one — the Cloud Posse org, where a single naming module holds up 146 of its repos.
About Riftmap
Riftmap maps cross-repo dependencies across your entire GitLab or GitHub organisation — Terraform, Docker, CI templates, Helm, and more. One read-only token. No YAML to maintain.
Frequently asked
What percentage of repos are shared dependencies in a typical organisation? In a scan of nine public platform organisations covering 1,297 active repositories, 320 repos, or 24.7%, were a dependency of at least one sibling repo in the same organisation. The per-org range was wide, from 4.7% to 46.9%, so treat it as the shape of the problem rather than an estimate of your own organisation. Every methodological choice in the scan biases the figure downward, including a confidence bar that discarded 35% of candidate edges before anything was counted, so 24.7% is a floor.
How do I find every repo that consumes a shared Terraform module?
You cannot find it from the module’s own repository, because a repo declares what it imports and never what imports it. You need the organisation-level dependency graph: parse the Terraform source blocks across every repo in the org, including registry coordinates like terraform-aws-modules/vpc/aws, and resolve each one to the repo that produces it. In the scan above, cloudposse/terraform-null-label turned out to have 146 direct consumers this way.
Can an AI coding agent see the blast radius of a cross-repo change? Not from a single repository’s checkout. The edges that make a shared change risky are inbound cross-repo edges, and those do not exist anywhere inside the repo being edited. An agent needs an organisation-level artifact dependency graph served to it as a tool call to answer “what breaks if I change this”.
Why not infer cross-repo dependencies from repo names or code search?
Because the failure modes are subtle and they bite hardest on exactly the repos that look most important. In this scan, two of Grafana’s apparent top hubs, grafana/opentelemetry-go and grafana/k6-DefinitelyTyped, were forks that preserved the upstream package identity, so they absorbed every same-org import of the upstream package. Parsing declared manifest edges and resolving them to a producing repo is what makes a count trustworthy, and even then you have to audit the hubs.