September 4, 2026
GSoC Journal: Weeks 11–13 — The Bug That Kept Finding New Places to Hide
There’s a particular kind of relief in merging a fix and seeing the tests go green.

By Sandesh Raj
4 min read
There's a particular kind of humility in finding out, a week later, that you'd only found the first floor of the bug.
That's roughly the shape of these three weeks.
The Bug: Play Console Doesn't Want Two Release Entries
The frozen version code problem had already come up once before, back when I fixed the logic that could silently drop old builds from a track update. I thought that was the whole story.
It wasn't.
The Play Developer API has another constraint I hadn't accounted for: it rejects track updates that send the frozen builds and the new release as separate release entries.
The fix was to stop treating "new release" and "preserved frozen builds" as two things being sent together, and instead merge every version code, new and frozen, into a single release entry with one versionCodes array. One statement, not two.
I also added something stricter than a fix: an invariant check. If a frozen version code that's supposed to be on the live track is unexpectedly missing when the script goes to read it, the script now hard-crashes instead of quietly continuing. That felt like the right trade. A loud failure on a Tuesday morning is a much better outcome than a silent one that only gets discovered when someone notices a build disappeared from Play Console weeks later.
Alongside that, the documentation work kept going. I added an in-depth release reference covering the script architecture and WIF auth flow, along with a feature development guide for anyone building something new that needs to interact with the release automation. Less eventful than the API fix, but it's the kind of thing that only stays true if someone keeps updating it as the system changes, so I'd rather write it while the details are still fresh than let it drift.
The Ripple: One Fix, Three More Places That Assumed the Old Behavior
Here's the part I didn't expect. Fixing how frozen version codes get sent to Play Console didn't finish the frozen-version-code problem. It just revealed how many other parts of the system had quietly built assumptions on top of the old, broken behavior.
First casualty: the version inversion checker, which exists to catch a very real class of bug, releasing a version that's numerically older than what's already live. Except the permanent KitKat build, kept alive forever on alpha for old API-16 devices, has a version code of 16. Every real release has a version code in the tens of thousands. So the checker looked at a completely legitimate state, current release at 37301, frozen KitKat build at 16, and confidently declared it an inversion.
Technically correct by the letter of "is this number smaller." Completely wrong by the actual intent of the check.
The fix was to filter frozen version codes out of the ordering comparison entirely. They're not part of the release cycle the checker is supposed to be reasoning about, so they shouldn't be able to trip it.
Then the same category of bug showed up again in the cross-track ordering checks. Same root cause, same fix, different place in the codebase that had made the same unstated assumption.
And then a third place: the test helpers. FakePlayConsoleClient's test setup had a hardcoded list of frozen version codes sitting inside it, which meant every time the real FrozenReleaseConfig changed, tests broke immediately, not because anything was actually wrong, but because the fake had its own separate, stale copy of the truth.
I fixed that by deriving the test setup directly from the same config the production code reads from, so there's exactly one source of truth instead of two that occasionally disagree.
The common problem was that "frozen version codes are special" existed as an assumption in my head, but not consistently in the codebase.
A Different Kind of Week: Debugging in Layers
Week 13 brought a different kind of problem: one in a workflow I thought I'd already finished.
The weekly lesson version workflow, the one that quietly refreshes pinned content from the Oppia server, started failing. Fixing it meant peeling back three separate layers, each one hiding the next.
Layer one: the underlying Bazel script needed six arguments to run, and the workflow was only passing five. Missing a required config argument doesn't always fail loudly, and this one didn't. It just failed silently for both the alpha and prod download steps until someone went looking.
Layer two, right after fixing that: the PR the workflow opens was targeting the wrong base branch, and it was also getting flagged by a strict issue-tracking CI check that auto-generated PRs have no way of satisfying, since there's no human writing an issue reference for a cron job's commit. Fixed the base branch, exempted the automated branch from that check.
Layer three, immediately after that: my fix to the base branch had quietly broken something else. The script the workflow depends on doesn't actually live on develop, it lives on a different branch entirely. So the checkout needed to stay pointed at that branch even while the PR itself correctly targeted develop. Two different refs doing two different jobs, and I'd collapsed them into one by mistake while fixing the first problem.
Each fix was small. Stacked together, they were a reminder that a workflow with several moving parts doesn't fail once, it fails once per part, and you only find the next one after you've fixed the one in front of it.
Somewhere in the same week, build_and_sign also started failing for an unrelated reason: the asset pipeline was still pinned to a commit from before the Oppia server added support for worked examples content, and the server had moved on without the app's pin catching up. I bumped the pin to a version that understood the new content type, and the build started passing again.
What Three Weeks of This Actually Taught Me
I think that's what these three weeks actually taught me. A fix isn't really finished when the original test turns green. Sometimes you have to keep following the assumption behind the bug until you've found every place that depended on it.
That's slower than it sounds like it should be. It's also, I think, the difference between automation that happens to work today and automation you can trust next month.
Stay tuned for more updates on my journey with Oppia Foundation in GSoC'26…
Find me on social media:
- LinkedIn: https://www.linkedin.com/in/sandesh282/
- GitHub: https://github.com/sandesh282/