
Merged Is Not Shipped: Why a Year-Old Fix Never Reached You
A bug fix can be written, reviewed, merged, tagged, and correct, and still never reach a single user. The fix for a real iOS connectivity bug had existed upstream for over a year while the people hitting the bug filed me too comments on an issue, because one git submodule pointer between the library and the client was never advanced. Merging changes the source of truth. It does not move the pointers, and in a dependency graph the last move is owned by no one.
Table of Contents
There is a version of a bug that is more frustrating than an unfixed one. It is a bug that was fixed, correctly, and then sat behind glass for a year while the people it was hurting kept reporting it, because the fix and the users were in two different repositories and nothing moved the one pointer between them.
I ran into exactly this. The connectivity failure I wrote up separately, where an IPv6-only cellular network quietly rewrites a VPN address and one code path dies, has a clean upstream fix. It was merged in July 2025. It is small, it is correct, and its commit message describes the exact situation I spent a night diagnosing. And more than a year later, the users hitting the bug still could not get it, including if they built the app from source. Not because the fix was hard, or hidden, or controversial. Because one git submodule pointer was never advanced.
Merging changes the source of truth. It does not move the pointers between that source and a user’s install. A fix is shipped only when every pinned reference on the path from the commit to the running binary has been advanced, and in a dependency graph that last move is owned by no one.
This is the delivery half of a two-part field note. The first part was about recognizing a failure that logs no error. This part is about a fix that shows every sign of being done and reaches nobody, which is the same illusion one layer up. It is also, underneath, the completion-ownership problem again: merged is the software-supply-chain version of a 202 Accepted. It says the request was received, not that the effect happened. Somebody has to own the claim that the fix actually reached users, and in an open dependency graph, nobody does.
The shape of the gap
The project here is structured the way a great many projects are. There is a core library that does the real work, and there are several client applications that each embed that library as a git submodule pinned to a specific commit. The pin is deliberate and correct engineering. It makes every client’s build reproducible and keeps an upstream change from silently altering a released app. It is also, for the same reason, a freeze: the client is stuck at whatever point in the library’s history the pointer names, no matter what has been fixed since.
The fix landed in the library. Then look at where the three clients had their submodule pinned:
| client | pinned library commit | has the fix |
|---|---|---|
| desktop (Qt) | mid-2026 | yes |
| Android | early 2024 | no |
| iOS | early 2024 | no |
The iOS client’s pointer was two dozen commits behind the fix. The App Store build was older still. So an iOS user could not get the fix by updating the app, and could not get it by building from source either, because building from source faithfully builds the pinned commit, which does not contain it.
Then the detail that makes the whole thing land. The fix’s own commit message says it is a workaround for iOS. It was written for iOS. And the only one of the three clients carrying it was the desktop client, which does not run on iOS. The fix reached every platform except the one it was written to help.
Meanwhile the issue tracker for the client had the bug open for a year and a half, collecting me too comments and workaround folklore, with no one having connected it to a commit that had been sitting in a sibling repository the entire time. The answer existed. It was one git log away. The distance between the fix and the people who needed it was not technical. It was organizational, and it was invisible from both ends.
Why the gap is invisible from both ends
The delivery gap survives because everyone who could close it is looking at a view where it does not appear.
The library maintainer merges the fix, the tests pass, the issue in their tracker closes, and by every signal available to them the work is done. Their instrument does not show downstream pins. Nothing tells them that a dependent client is twenty-four commits behind and that real users are stranded on the far side of a pointer only that client’s maintainer can move.
The client maintainer is not watching the library’s commit log commit by commit. Bumping a submodule is a deliberate, manual, and slightly risky act, because moving the pointer can drag in unrelated changes and API drift along with the one fix you wanted. It is unglamorous, it is easy to defer, and nothing schedules it. There is no notification that says a dependency you pin has fixed a bug your users are reporting.
The user sees only the bug, and an issue tracker full of other people seeing the same bug, which reads as strong evidence that the problem is simply unsolved. The one place the answer actually lived, the commit history of a dependency, is not a place most users think to look, and the tracker gives them no reason to.
So the fix sits in the seam. Merging felt like the finish line to the one person positioned to know the fix existed, and moving the pointer was nobody’s defined job, and the people motivated to close the gap were the only ones who could not see where it was.
This is every lockfile you have
It would be comforting to file this under git submodules, which have a reputation for exactly this kind of pain, and move on. But the submodule is only the most visible instance of a mechanism that is everywhere in modern software, and that mechanism is the pin.
Every reproducibility guarantee you rely on is a frozen pointer. package-lock.json, go.sum, Cargo.lock, a pinned requirements.txt, a Docker FROM image:tag, a vendored module, a Helm chart’s image digest. Each one exists to protect you from unwanted change, and each one, by protecting you from unwanted change, also holds you at a known-good point that can quietly become a known-stale point the moment an upstream fix lands above it. The property that makes your build trustworthy is the same property that makes it a place fixes go to wait. You do not get deterministic builds and automatic fix delivery from the same pin. You get one, and you buy the other separately, with deliberate effort, or you do not get it at all.
Which means the question is a fix I need already merged upstream is the wrong question, and answering it yes is how you end up stranded with false comfort. The right question is has that fix crossed every pin between its commit and the thing I actually run. Those are very different questions, and the gap between them is measured in the months a user spends believing a bug is unsolved.
What to actually do
For the person chasing a bug, the discipline is small and it saves real time. When you find that a bug is fixed upstream, do not stop at merged. Compare the fix commit against the exact revision your project pins, not against the upstream tip, and read the number of commits you are behind. Read commit history rather than the issue tracker, because the tracker is where the duplicate confusion pools and the commit log is where the resolution actually sits. And if you can, cherry-pick the fix onto your pinned revision and test it, which is what turns a guess into a verified claim and, incidentally, gives a maintainer a concrete reason to bump the pointer. That last step is the same move as everywhere else on this site: do not assert that it works, prove it in a loop closed against reality.
For the person maintaining a project that pins dependencies, the fix is structural, because relying on a frustrated user to eventually investigate is not a delivery process. Give the pointer an owner. Make advancing pins a tracked, routine task rather than an act of archaeology, and let automation open the update pull requests so that a dependency’s fixes become visible events in your own repository instead of silence. The goal is that the gap between merged and shipped becomes something a system watches, rather than something only a stranded user ever notices.
What was scarce, again
The fix was never the scarce thing. It existed, correct and complete, for over a year. What was missing was the single unglamorous act of moving a pointer, an act that no dashboard flagged, no notification raised, and no person had been assigned. Everyone involved was behaving reasonably inside their own view. The maintainer had merged. The user had reported. The pointer sat between them, load-bearing and unowned, and a working fix helped no one for a year because of it.
Merged is not shipped. The commit is only the beginning of the fix’s journey to a user, and most of that journey runs through pins that someone has to advance on purpose. Until the last of them moves, the fix is real, and it is doing nothing.
This is the delivery companion to Your VPN Is Fine. iOS Swapped the Address., the diagnosis that turned up the year-old fix. It shares its spine with Tool Calls Need Completion Ownership, where merged plays the role of accepted and shipped plays the role of observed, and with Validation Is a Loop, Not an Assertion on proving delivery rather than assuming it.
🎧 More Ways to Consume This Content
I occasionally advise small teams on backend reliability, Go performance, and production AI systems. Learn more: /services
Comments
This space is waiting for your voice.
Comments will be supported shortly. Stay connected for updates!
This section will display user comments from various platforms like X, Reddit, YouTube, and more. Comments will be curated for quality and relevance.
Have questions? Reach out through:
Want to see your comment featured? Mention us on X or tag us on Reddit.