A stranger approved my code, and the project's maintainer merged it. That sentence still feels strange to type, because a few weeks ago I had never sent a single pull request to anyone's project.
I'm a software engineering apprentice at Zone01 Kisumu, I write mostly Go, and I had always treated open source as something other people did. This is the story of how my first contribution actually happened: the wrong turns, the red test output, and the review that made my code better. If you have been staring at a repository wondering whether you are allowed to touch it, this is for you.
Why I wanted in, and what scared me
School projects teach you to write code that only your team ever reads. I wanted to find out what happens when someone who owes me nothing reads my code and decides whether it belongs in their project.
I was also afraid, and the fears were specific:
- I would pick an issue I could not finish and waste a maintainer's time.
- I would break something in a codebase I did not understand.
- I would not know the rules, and someone would tell me so in public. None of these turned out to be a reason not to try. They turned out to be a checklist of things to do carefully.
Finding a project was harder than the code
I expected the hard part to be writing the fix. It was choosing where to write it. I went through four projects before one worked, and each dead end taught me something about how open source really runs.
| Project | What stopped me | What I learned |
|---|---|---|
| Pallets (Click) | Their policy does not accept AI-assisted contributions from new contributors | Read the contributing guide and any AI policy before touching code |
| cli/cli | Issues need a help wanted label and clear acceptance criteria, and every eligible one already had competing pull requests | A popular project can be a crowded one |
| Trivy | The issues I found were years old, and the open ones are created and claimed by maintainers | An old open issue is not necessarily an invitation |
| network-doctor | Nothing. It had clear issues, most unassigned, and a contributing guide | Smaller, active projects can welcome you faster |
The project I landed on, network-doctor, is a Go diagnostic tool that tells you what is wrong with a network connection. It had recent activity, a maintainer who answered, and issues written clearly enough that I could tell what "done" looked like. I picked issue #422, a bug about IPv6 addresses losing part of their identity in a report.
The constraints I was working inside
Every contribution happens inside limits. Naming mine early would have saved me some anxiety.
- A strict repository. network-doctor has its own rules: short imperative commit subjects with no prefixes like
fix:, no co-author trailers, no em dashes anywhere (a test fails the build if one appears), and a pull request template with fixed sections. - A newer toolchain. The project needs a recent Go version, and I had to make sure my own machine matched before any result meant anything.
- Guard tests I did not write. The maintainer had built tests that force every field in the report to be classified, described in the schema, and proven to reach the output. They cannot be argued with, only satisfied.
- Being new. Everything was a first: forking, remotes, branches, pull requests, and the etiquette around all of them. ## The bug, in plain words
Some IPv6 addresses, the ones that start with fe80, only work on one local network link. Like saying "the kitchen" without saying which house, the address alone is not enough: you also need the network interface, written after a percent sign, as in fe80::1234%eth0. That extra piece is called the zone.
network-doctor knew about the zone while it ran its checks. But when it wrote its report, it rebuilt the target from the address alone, so you typed [fe80::1234%eth0]:22 and the report printed [fe80::1234]:22. The zone had been dropped on the way out.
My fix added an optional zone field to the report, filled it from the target, and showed it in the text output. The address field stayed clean on purpose, because the zone must never leak into name lookups. Targets without a zone print exactly as before. I also updated the published schema and the reference docs, and added tests for both the zoned and unzoned cases.
The proof I was proudest of was small: I ran the real program before and after the change and pasted both lines into the pull request. Before, the zone was gone. After, it was there.
Where it got hard
The fix was a handful of lines. Almost everything difficult happened around it.
A test that said [no tests to run]. I had pasted my new test into the wrong file, in the wrong package. Go did not complain, it just quietly ran nothing. A green or empty result is not proof, so I learned to check that the test name I wrote actually appears in the output.
Guard tests that fought back. The moment I added the new field, the maintainer's tests failed three different ways: the field was unclassified, the schema did not describe it, and no scenario filled it in. Each failure message told me exactly what was missing. Reading those messages slowly turned out to be the whole skill.
Noise that was not mine. A formatting check complained about a file I had never touched. I had to learn to separate "my change broke this" from "this was already like this", and to leave the second kind alone.
An empty variable. One of my before-and-after runs failed with invalid target "[fe80::1%]:22" because my interface variable was empty in a fresh terminal. The program was right to reject it, and I was the bug.
Review, twice. After I opened the pull request, an automated reviewer pointed out that a table in the tests still labeled the zone as deliberately left out of the report, which contradicted my change. I fixed that. Then the maintainer asked for something sharper: my test fixture used a made-up combination, a normal address with a zone attached, that the program's own parser would reject. I replaced it with a valid scoped link-local address, so the test now represents input a real person could type.
What I learned
- Read the rules before the code. Contributing guides, AI policies and pull request templates decide whether your work is welcome at all. A project that bans something is not being unfair; it is telling you the truth early.
- Pick a project by its pulse, not its fame. Recent commits, replies from maintainers and clearly written issues mattered more than star counts.
- Prove the bug before you fix it. I wrote failing tests first, so I knew the fix worked and the maintainer could see why it was needed.
- Make the smallest change that fully solves the issue. I noticed other places with the same flaw and wrote them up as out of scope instead of widening the pull request. Maintainers review small, finished things faster.
- Failing tests are instructions, not insults. The project's own guard tests taught me how its authors think.
- Report honestly. When I was unsure whether something was my change or already broken, I said so. When an earlier explanation of mine turned out wrong, I corrected it instead of defending it.
- Review feedback is a gift. Both the bot and the maintainer made the change better, and neither comment was about me. ## What I set out to do, and what I did
When I started, my goals were simple: find a project that would take me, fix a real bug, and get it merged. Here is how that went.
| Objective | Outcome |
|---|---|
| Choose a suitable project and issue | Done, after four tries and one clear fit |
| Learn the fork, branch, commit, push and pull request flow | Done, with every command run by me |
| Fix issue #422 without breaking the project's own rules | Done, including schema, docs and guard tests |
| Satisfy automated and human review | Done, in two rounds of changes |
| Get the pull request merged | Done |
If you are about to start
Pick one small, clearly described issue in a project that is alive. Read its contributing guide twice. Ask in a short comment before you start if the issue is unclaimed, and be honest about how you work, including any tools that helped you.
Then go slowly. Run the tests before you change anything, so you know what normal looks like. Make a small change, make it pass, and explain it plainly in the pull request. When someone asks you to change something, change it and thank them.
You do not need permission to be a beginner. You need one issue, one branch and the patience to read an error message to the end. Mine is merged now, and the next one is already on my list.
