📝 Originally published (in Japanese) at forge.workstyle.tech.
Two teams were working on the same set of microservices in parallel: one on the audio pipeline and the other on conversation quality. Their responsibilities were separate, and the files they worked on barely overlapped.
Yet, three times, they overwrote each other's work in production.
1st Incident: 5 Frontend Commits Disappeared
To apply a frontend fix, I created a branch from main, built, and deployed it.
Then, a user reported:
The record is gone, and a screen I thought was removed has reappeared.
A screen that was supposed to be removed was back, and a feature I added was missing. Upon investigation, the running image wasn't built from main.
$ grep -l "virtualpv:1.0.407" ~/kaniko-builds/*.yaml | xargs grep -- --branch
--branch feat/character-list-preview
That branch was 5 commits ahead of main. By building from main, those 5 changes were removed from production.
In this repository, main wasn't the source of truth for production. Unmerged branches piled up: 54, 12, 6, and so on. The common assumption that "main is the latest" didn't hold here.
I restored the previous tag using kubectl set image, cherry-picked the changes to the correct base, and rebuilt.
2nd Incident: 17 Backend Versions Disappeared
After the first incident, we established this procedure:
# Before building, check if you've missed any commits from the production base branch
git log --oneline HEAD..<production-base>
Despite following this procedure, I caused another incident during the next build.
Before building the backend, I checked the running Pod to ensure my changes were included.
>>> inspect.signature(judge_transcript).parameters['max_inserted'].default
1 # My value. The running version included my changes.
Feeling confident, I built from main. As a result, the other team's 17 versions disappeared, halting conversation logging and losing 60 minutes of data.
I checked if the running image included my changes, but I should have checked if my build included the running image's changes. These are two different things, yet I treated them as the same.
A simple diff would have revealed the issue:
$ git log --oneline <my-HEAD>..<production-base> | wc -l
20
Running this once before building would have prevented the incident. Although we had a procedure, I didn't follow it when switching repositories. For the frontend, I even checked the manifest's --branch, but for the backend, I only verified a single value inside the Pod.
3rd Incident: The Team Following the Procedure Was Affected
Later, the opposite happened.
I fixed the backend and deployed it. Then, I switched to monitoring the rebuild but forgot to push to main.
The other team followed the correct procedure, comparing their branch to the production base and ensuring no commits were missed. Since my changes weren't in main or their branch at that time, their check passed. As a result, my changes were overwritten.
No matter how correctly one team checks, if the other doesn't push, incidents can't be prevented.
Branch Checks Weren't Enough
After the third incident, the other team nearly caused a fourth incident in another repository, which they caught just in time. They compared the SHA256 hashes of files in the running Pod with their branch:
pipeline.py Match
brain_talk.py ⚠️ Mismatch Running=1a02… Local=341e…
app.py ⚠️ Mismatch Running=8a41… Local=4110…
The mismatches were three delivery recovery fixes I had added. If they had done a full build, all would have been lost.
At that time, they had mistaken the base branch (they thought it was a feature branch, but it was actually built from main). The git log HEAD..<base> procedure assumes the base is known, so it fails if there's a misunderstanding. SHA256 comparison works without knowing the branch name.
Another lesson was that checking for file existence isn't enough. After the second incident, I started checking if the other team's conversation_log.py existed, but in this case, the files existed but had different contents. Existence checks wouldn't catch this.
Conclusion: Two Steps Are Enough
From the three incidents, two essential steps emerged:
1. Push to main immediately after deployment.
This is the core. If both teams follow this, main will always match production, and step 2 becomes automatic. The third incident occurred because I failed to do this.
2. Before building, compare the running Pod and your branch using SHA256.
This is the safety net. Even if the other team doesn't follow step 1, this will catch issues.
kubectl exec -n <ns> deploy/<dep> -- sh -c \
'cd /app && find apps/services -name "*.py" | sort | xargs sha256sum' > /tmp/pod.txt
(cd <repo> && find apps/services -name "*.py" | sort | xargs sha256sum) > /tmp/br.txt
diff /tmp/pod.txt /tmp/br.txt # Ensure differences are only your changes
Step 2 alone couldn't prevent the third incident, and step 1 alone couldn't prevent the first. Both are necessary.
Additional Pitfalls Encountered
kubectl rollout status completion can't be trusted. "Successfully rolled out" appeared immediately, but two Pods were running simultaneously:
exista-voicepipe-<rs-old>-lpqtx Running ...:0.7.14
exista-voicepipe-<rs-new>-cv92x Running ...:0.7.15
They converged after a few seconds, but during that time, testing would have resulted in inconsistent behavior. After deployment, count Pods directly.
A similar incident occurred in a batch job generating 200 clips. The Pod was replaced mid-generation, resulting in half the clips using different settings. I only noticed when logs showed the new gate hadn't triggered once.
Don't reuse build definitions. I once modified an old manifest with sed but forgot to update the --branch, causing the first incident. It's better to keep separate definitions for each purpose. The other team now includes a note to use the running tag as the base.
Beware of tag overwrites. Once, a chain of && broke, skipping a tag bump and pushing new content to an old tag. Rolling back to that tag no longer restores the original behavior. Without knowing this history, troubleshooting becomes impossible.
Generalizable Lessons
These incidents seem specific to parallel work, but they can happen even solo.
The first incident's root cause was the common assumption that main is the source of truth didn't apply here. The deployment branch was the source of truth, and main was outdated. This is common in environments with unstructured release flows.
Even solo, if past-you built from a different branch, the same issue arises. Your past self is as unpredictable as another team member.
The key is to check if your build includes what's currently running, not what you added. You remember what you added but not what's running. That's why mechanical comparison is essential.
Series: Mass-Producing Practical Voices from Diffusion TTS
This series documents designing voices from a single caption, creating training corpora, and mass-producing role-specific practical voices. This article is Part 4: Operations.
← Previous: Four Registration Paths, Zero Management Screens
→ Next: Failing When Chasing Unmeasured Metrics with Thresholds
All 18 Articles in the Series
- The High-Quality TTS Was Too Slow for Conversations
- Voice Gacha: Designing Voices Deterministically
- Selecting "Narrator-Like" Voices from 24 Candidates Using Metrics, Not Ears
- Stricter Quality Gates Let More Monotone Voices Through
- Speaking Speed Can't Be Changed After Training
- TTS That Changes "Recording Room" Every Generation
- One Rough Clip Ruins the Entire Style
- Where Did the AI's Habit of Stretching "Hello" Come From?
- The Character That Broke the TTS Input
- Hallucination Guard That Never Fired
- Three Characters Became a Verbal Tic
- Measuring Factory Defects as Product Traits
- Defects Invisible to Transcription
- 70 Minutes Lost to a Network Blink
- "ja" vs "JP": The Babbling Model
- Four Registration Paths, Zero Management Screens 17. Overwriting Each Other's Work with Every Deployment ← You are here
- Failing When Chasing Unmeasured Metrics with Thresholds
The insights are compiled in the Practical Voice Mass Production Pipeline from Diffusion TTS.
Top comments (0)