DEV Community

Cover image for Your branch protection is quietly turning away first-time contributors
אחיה כהן
אחיה כהן

Posted on

Your branch protection is quietly turning away first-time contributors

Ten weeks ago I did the thing every "grow your open source project" guide tells you to do. I carved a few small, self-contained tasks out of my backlog, labeled them good first issue, wrote crisp descriptions, and waited for contributors to roll in.

They didn't roll in. The issues just sat there.

This morning, one of them finally got picked up. A first-time contributor opened a clean PR against my MCP server: a smoke-test suite, no new dependencies, green across the whole Node CI matrix. Exactly the contribution the label was advertising for.

And then my own repository spent the next twenty minutes trying to stop it from getting merged.

Not with anything dramatic. With three quiet, individually-reasonable "best practice" gates that, stacked together, form a gauntlet aimed squarely at the one person you spent ten weeks trying to attract. I want to walk through each gate, because almost everything written about contributors is about attracting them, and almost nothing is about the last hundred feet — the silent friction between a willing PR and a merged commit.

The advice is only half the story

"Add good first issues and contributors will come" is true in the same way "build it and they will come" is true: technically, eventually, for a small subset, with survivorship bias baked in. My good first issue opened on March 31. The PR that closed it merged on June 8. That's sixty-nine days of a clearly-labeled, beginner-friendly task sitting untouched.

I'm not complaining about the wait — that part is normal. I'm pointing out that the advice stops exactly where the interesting problem starts. Because the bottleneck was never finding someone willing. When someone willing finally showed up, the friction was entirely on my side of the fence.

Gate 1: the CI that silently refuses to run

GitHub Actions does not run workflows on pull requests from first-time contributors until a maintainer approves the run. This is a sane anti-abuse measure — fork PRs can run arbitrary code in your CI — and I'd keep it on. But look at it from the contributor's seat.

They open a careful PR. They watch the checks section. And nothing happens. No green check. No red X. No "running." Just a quiet:

no checks reported on the 'codex/add-smoke-tests-for-mcp-tools' branch
Enter fullscreen mode Exit fullscreen mode

From the maintainer's side it shows up as action_required:

27117992338  [completed/action_required]  pull_request  CI
Enter fullscreen mode Exit fullscreen mode

To you it's a one-click "Approve and run." To them it is indistinguishable from "this maintainer doesn't care / this repo is dead / I did something wrong." There is no message telling them it's waiting on you. A motivated first-timer who doesn't know this is a normal GitHub behavior will quietly assume the worst and never come back. The gate didn't reject their code — it rejected their confidence.

Gate 2: BLOCKED, reason unspecified

Approved the run, watched it go green on Node 20, 22, and 24. Posted a review. Hit merge. Got:

Pull request is not mergeable: the base branch policy prohibits the merge.
Enter fullscreen mode Exit fullscreen mode

So I checked the branch protection. The classic API was almost entirely empty:

{ "required_pull_request_reviews": null, "required_status_checks": null }
Enter fullscreen mode Exit fullscreen mode

Reviews: not required. Status checks: not required. The PR itself reported mergeable: MERGEABLE, reviewDecision: APPROVED, every check SUCCESS — and mergeStateStatus: BLOCKED. Blocked by what? Nothing the obvious endpoint admitted to. I had to pull the full protection object to find the real culprits:

{
  "required_signatures":               { "enabled": true },
  "required_conversation_resolution":  { "enabled": true },
  "enforce_admins":                    { "enabled": false }
}
Enter fullscreen mode Exit fullscreen mode

Here's the thing: I set those, and even I had to dig to remember they were on and reconcile them against a half-empty API response. A first-time contributor staring at "the base branch policy prohibits the merge" has zero ability to diagnose this. It's not their branch, not their settings, not their problem to solve — and yet it's their PR sitting in limbo.

Gate 3: the unsigned fork commit

required_signatures was the actual wall. My main requires verified commit signatures. A commit authored on someone else's fork is, naturally, not signed with anything my branch trusts. The escape hatch turned out to be the squash merge: when GitHub creates the squash commit server-side, it signs it with its own web-flow key, so the resulting commit on main is verified:

a4b29a054  verified=true  | test: add smoke tests for registered MCP tools (#35)
Enter fullscreen mode Exit fullscreen mode

It worked. But notice the shape of the solution: the contributor's commit was never going to satisfy the rule, and the only reason it merged is a side effect of how GitHub squashes. That's not a policy a contributor can reason about. It's tribal maintainer knowledge.

None of these are bugs. That's the problem.

Every gate here is defensible in isolation. Approval-gating fork CI prevents crypto-miners in your Actions minutes. Required signatures raise the bar on supply-chain tampering. Conversation resolution stops half-finished reviews from merging. I'm not telling you to turn them off.

I'm telling you that nobody adds them together on purpose, with the contributor experience in mind. They accrete. You enable one after a scare, another because a security checklist said so, a third because it was the default in a template. And the cumulative effect is an invisible obstacle course that your most valuable, least-experienced contributor runs blind — with no signposts, while you're not looking.

The "growth" content optimizes the funnel right up to the PR and then goes silent. The actual leak is after the PR.

What I'm changing

  • Document the approval gate. One line in CONTRIBUTING.md: "Your first PR's CI won't run until I approve it — that's a GitHub default, not you. Ping me if it sits." That single sentence converts "this repo is dead" into "ah, normal."
  • Watch the Actions tab, not just the PR list. action_required runs don't page you. If you're seeding good first issues, you've explicitly invited PRs from people whose CI will always start out gated. Treat that queue as a first-class inbox.
  • Re-audit protection rules as a set, from the contributor's chair. Pull the full protection object, not the two-field summary. Ask of each rule: "if a stranger's clean PR hits this, will they understand what happened?" If the answer is no, at minimum it needs documentation.
  • Reply fast and warm on the first one. The first external PR is a confidence transaction as much as a code transaction. I approved CI, left a specific review calling out what was good, and merged inside the hour. That contributor is far more likely to send a second one.

The smoke suite, by the way, is genuinely nice work: it boots the server over the real stdio transport — the same path actual MCP clients use — and asserts that all 91 registered tools show up with unique safari_* names and valid schemas. It derives the expected count from the source instead of hardcoding it, so it stays honest as the tool surface grows. It runs on safari-mcp, a browser-automation MCP server for Safari on macOS, if you want to see the shape of it.

Maintainers: how do you handle the first-time-contributor CI gate — pre-approve, automate it with a workflow, or just keep the Actions tab open? I'd genuinely like to steal a better answer than "watch it like a hawk."

Top comments (0)