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.

· 5 min read

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:

  1. Be a reader before becoming a writer. Spend time reading the issue tracker, existing PRs, CONTRIBUTING.md. Understand what the project actually needs.
  2. Look for bugs, not features. Bugs are objectively there. Features are subjective. Maintainers have a much harder time rejecting a reasonable bug fix.
  3. Minimize the diff. The less you change, the faster the review, the higher the merge rate.
  4. 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