Two builds shared one version tag. I found it after a release I had just verified, and the collision meant the release tag could no longer answer the one question I needed it to answer: which build is actually in production?

On June 29, we shipped a 2 hour 40 minute release that restored the Show Seed toggle, made seed numbers visible in every round of the public bracket, and added a per team Seed/Rank Override. The feature work was verified live. The release record carried a single version number. But the versioning path had already created ambiguity around that number.

The deploy did not explode. There was no compiler failure to chase and no production exception pointing at a bad line. The failure was subtler. We had two builds associated with one version identifier. If somebody asked whether a particular fix was live, the tag was supposed to be the shortest path to an answer. Instead, it obscured the answer.

A release tag is not a label

I had been treating versioning as release decoration. The code is the thing that runs. The tag is just a human friendly name attached afterward.

That idea falls apart the moment I need to reconstruct a production state. A tag is part of the release record. It connects a deployed artifact to a commit, a set of changes, verification work, and the operational claim that this is what is live. Once two builds can share it, all of those links become suspect.

The risk was not that the bracket feature itself had been misbuilt. We had checked the public behavior. The risk was that a later failure would send us backward through deployment history with a false key. We could see the version number, but we could not use it as a unique identifier for the code behind the behavior we were investigating.

That is an expensive problem to discover during an incident. It turns a precise question, “when did this change ship?”, into archaeology.

The bad source of truth

I went looking at the deployment target first, which was the wrong end of the problem. What actually mattered was the version calculation. The next release number was being derived by grepping merge messages. That looked workable while merge text and release history happened to move in lockstep.

They are not the same record.

Merge messages describe development history. They can be written in different forms, omitted, reordered by the shape of a branch, or simply fail to carry the one marker a parser expects. A grep over that text is an inference about releases. It is not the release ledger.

Git tags are the ledger. A tag is the artifact I deliberately place on a specific commit to denote a release. The source that names the prior release must be the same source that records prior releases.

The hardening rule was therefore small and specific: derive the next release version from Git tags, not from a merge message grep.

old: inspect merge messages, infer the latest version
new: inspect Git tags, identify the latest version, derive the next version

This was not a change to the application schema or a rewrite of deployment. It was a correction to the boundary where release metadata becomes a decision. The new calculation asks the repository’s release history for the latest release, then advances from that value. It does not try to reconstruct release history from prose written for a different purpose.

The alternatives that lost

Leaving the grep in place and adding a stricter message convention would have preserved the same flaw. It would still make merge prose authoritative. We could require a particular string, but then a release would depend on every path into the branch producing exactly that string.

Using an independent counter would have moved the problem elsewhere. A counter can be unique, but it still has to be reconciled with the commit it represents. We already had an object designed to provide that relationship: a tag attached to a commit in the repository.

The tag based path wins because it uses the most direct available fact. The question is, “what was the previous release?” The answer should come from the set of previous release tags.

What changed after the collision

I filed the hardening work as soon as I saw the collision, instead of treating it as an annoying release anomaly. The implementation lesson in the work record is blunt: derive the next release version from Git tags, not merge message grep.

That lesson is narrower than a generic rule about automation, which is why it is useful. Automation fails in the gaps between two records that look equivalent until they are not. Our merge history and our release history were close enough to look interchangeable, right up until one duplicated a version number.

A version number should identify one build. Now the release logic is built around making that true.