How Force Pushes Saved Our Release Pipeline

When you’re building a CI/CD system, you learn quickly that release automation is deceptively fragile. We discovered this the hard way with releasekit-uv.yml — our release orchestrator for the ai-agents-genkit project kept failing when trying to create consecutive release PRs.
The problem seemed simple at first: the prepare_release() function was recreating the release branch from scratch on each run using git checkout -B, which essentially resets the branch to the current HEAD. This is by design — we want a clean slate for each release attempt. But here’s where it got tricky: when the remote repository already had that branch from a previous run, Git would reject our push as non-fast-forward. The local branch and remote branch had diverged, and Git wasn’t going to let us overwrite the remote without explicit permission.
The fix was deceptively elegant. We added a force parameter to our VCS abstraction layer’s push() method. Rather than using the nuclear option of --force, we implemented --force-with-lease, which is the safer cousin — it fails if the remote has unexpected changes we don’t know about. This keeps us from accidentally clobbering work we didn’t anticipate.
This change rippled through our codebase in interesting ways. Our Git backend in git.py now handles the force flag, our Mercurial backend got the parameter for protocol compatibility, and we had to update seven different test files to match the new VCS protocol signature. That last part is a good reminder that abstractions have a cost — but they’re worth it when you need to support multiple version control systems.
We also tightened our error handling in cli.py, catching RuntimeError and Exception from the prepare stage and logging structured events instead of raw tracebacks. When something goes wrong in GitHub Actions, you want visibility immediately — not buried in logs. So we made sure the last 50 lines of output print outside the collapsed group block.
While we were in there, I refactored the setup.sh script to replace an O(M×N) grep-in-loop pattern with associative arrays — a tiny optimization, but when you’re checking which Ollama models are already pulled on every CI run, every millisecond counts.
The real lesson here wasn’t just about force pushes or VCS abstractions. It was that release automation demands thinking through failure modes upfront: What happens when this runs twice? What if the network hiccups mid-push? What error messages will actually help developers debug at 2 AM?
Getting release infrastructure right means fewer surprises in production. And honestly, that’s worth the extra engineering overhead.
Why do programmers prefer using the dark mode? Because light attracts bugs. 😄
Metadata
- Branch:
- main
- Dev Joke
- Что общего у Selenium и подростка? Оба непредсказуемы и требуют постоянного внимания