Three Merged PRs: Quality Over Quantity in Open Source Contributions
From 20 closed PRs to 3 merged PRs — what I learned about picking the right problem mattering more than writing the right code.
In my last blog post I said “from zero to two merged PRs.” Time to update the headline — it’s three now.
But this isn’t a “look how great I am” brag post. The opposite, actually. It’s a story about failure. Because before those three merged PRs, I had about 20 PRs closed. Twenty. Some ignored, some rejected, some dismissed by maintainers with a single “not needed.”
So when those three got merged in the same day — one after another — my first reaction wasn’t excitement. It was: I finally get it.
The First: Fixing CI No One Dared Touch
MinionTech/vexilon #404 — migrated the deprecated semgrep GitHub Action to the official Docker image.
This project’s CI was using semgrep/semgrep-action@v1, an Action the vendor had long since deprecated. Like that ten-year-old router in your house — it works, but the manufacturer stopped shipping security updates.
My change was pure and simple: swap uses: semgrep/semgrep-action@v1 in the workflow YAML for directly pulling the semgrep/semgrep official image and running the CLI. Scanning rules untouched. SARIF output and GitHub Security tab upload logic unchanged.
Maintainer DerekRoberts approved it directly and even left a thank-you note.
What I learned: CI/infrastructure maintenance is open source’s blind spot. Everyone wants to write features. Nobody wants to fix the pipeline. But an outdated security scanning tool is a real risk — these PRs aren’t sexy, but maintainers know their value deep down.
The Second: Deleting Dead Code Nobody Noticed
sktime/sktime #10129 — removed two stale test exclusion configs from tests/_config.py.
sktime is a large ML project. Its test config file had two exclusion rules for SeriesToPrimitivesRowTransformer and SeriesToSeriesRowTransformer. Problem: those two classes had been deleted from the codebase ages ago. The exclusion configs became ghosts — nobody would ever trigger them, but nobody noticed they were still there either.
The change: deleted 4 lines. Two configs. Two comments. That’s it.
sktime core maintainer fkiraly’s review was two words:
Good find.
Two words. I stared at the screen for a while.
What I learned: In large projects, “cleaning up residue” is an underrated form of contribution. The bigger the codebase, the easier these dead configs slip through. And getting approved by fkiraly himself — that told me they value not the volume of code, but how carefully you read it.
The Third: 9 Lines Fixing a Crash Bug
Sakura520222/Sakura-AI-Reviewer #228 — added directory path protection to the read_file tool.
This project has a read_file tool that calls GitHub’s get_contents() API to read files. But get_contents() has a trap: pass a file path, it returns a single file object; pass a directory path, it returns a list. The original code only handled the file case, calling content_file.size directly — hit a directory and boom: 'list' object has no attribute 'size'.
Classic boundary condition bug.
After the existing null guard, I added an isinstance(content_file, list) check — if it’s a directory, return a clear error message telling the user to use list_directory. 9 lines of code, fixing a crash bug.
The project’s AI reviewer gave it 9/10, and the project author manually approved.
What I learned: GitHub API return types aren’t what you expect. get_contents() returning different types for files vs directories — that kind of “implicit polymorphism” is the easiest pitfall to overlook. And fixing these bugs has incredible ROI: tiny change, big impact, anyone can understand it.
What 20 Rejected PRs Taught Me
Looking back at those 20 closed PRs, the reasons varied but boiled down to a few categories:
- Wrong project. Some projects don’t need external contributions, or the maintainers have their own roadmap and your direction doesn’t align.
- Wrong problem. Fixed a typo, tweaked formatting, changed something because “I think this is better” — but the maintainer didn’t agree.
- Too big a change. Refactored half a module in one PR — reviewer took one look and closed it.
- Didn’t read CONTRIBUTING.md. Every project has its own rules. Submit without reading = wasted effort.
And those three merged PRs share one thing in common: precise changes, solving real problems, minimal diffs.
Not “I think this is better.” It was “there’s a bug here” or “here’s some dead code.” Not refactoring half a module. Deleting 4 lines, adding 9 lines, changing a YAML.
For CS Students Thinking About Starting Open Source
If you’re considering submitting your first PR to an open source project, here’s my advice:
- Be a reader before becoming a writer. Spend time reading the issue tracker, existing PRs, CONTRIBUTING.md. Understand what the project actually needs.
- Look for bugs, not features. Bugs are objectively there. Features are subjective. Maintainers have a much harder time rejecting a reasonable bug fix.
- Minimize the diff. The less you change, the faster the review, the higher the merge rate.
- Accept rejection. Those 20 closed PRs weren’t wasted. Each one taught me more about “what not to do.”
Open source contribution isn’t a code-writing contest. It’s about understanding a project, respecting maintainers’ time, and finding where you can actually help.
— Jiahao Ren, May 2026