We couldn't see what we gave back. So we built giveback.
giveback is an open source GitHub App that turns engineers' public pull requests into a live picture of what their company contributes upstream, with each person's consent.
Somewhere at Michelin, probably this week, an engineer hits a bug in a library one of our products depends on. They dig in, find the cause, and open a pull request upstream from their GitHub account. A maintainer asks for a test, they add it, and a few days later the fix is merged. The bug is gone for everyone who uses that library, us included.
And nobody at Michelin knows it happened.
Not their manager, not our Open Source Program Office. The pull request lives in someone else's repository, and GitHub's insights stop at the edge of our organization. Multiply that across every engineering team we have and you get a question we couldn't answer: how much do we actually give back to open source?
So I built a tool that can, and we've just open sourced it.
Why we care about open source
Michelin has been using and contributing to open source for about fifteen years. It started with our Java platforms, Jenkins arrived in 2010, and a few of the plugins we wrote back then are still in use. Today our teams publish their own projects on github.com/michelin, a lot of them around Kafka: kstreamplify, ns4kafka, or kafkagen, which you may have read about on this blog. Some of that work has gone all the way upstream: KIP-1033 and KIP-1034, two Kafka Streams improvement proposals about error handling and dead letter queues, were written by Michelin engineers.
Building on a healthy open source project is usually cheaper and faster than writing our own competitor to it, and it keeps us out of vendor lock-in. Working in the open makes us better engineers, and it makes Michelin a place good engineers want to join. And frankly, it's only fair: we've taken an enormous amount of value from open source over the years, and contributing back is how the projects we depend on stay alive.
So we want more of it, and we're putting effort behind that. Our OSPO maintains a legal framework that tells engineers how to contribute safely: under which license, on behalf of which entity, with which identity. It runs trainings. And it runs the OSS Champions, an internal program that recognizes people who push open source forward at Michelin, whether they contribute code, share what they know, or help colleagues take their first steps. Badges mark each kind of involvement, and one of them rewards pull requests to open source projects.
You can't hand out a badge for contributions you can't count.
The contributions we couldn't see
A GitHub account belongs to a person, not to a company. Engineers usually have one long before they join us and keep it when they leave, which is how GitHub intends it. Company-managed accounts do exist, but they can't open a pull request on a project outside the company at all. So when one of our engineers fixes a bug in Kubernetes because our platform needs that fix, there is no other way to do it: the pull request comes from their own account, in a repository Michelin doesn't own. The work is done for Michelin. From where we sit, it's invisible.
That's the work we're after, and only that. What someone does with that same account on a Sunday afternoon, for their own pleasure, is theirs. Our engineers' personal open source belongs to them, and we have no business taking credit for it.
This is also why our OSPO's framework asks people to contribute for Michelin as Michelin: with their professional email address, attached and verified on their GitHub account. It gives everyone a simple way to say "this one is for work" without giving up the account they've had for years, and it leaves the rest of their open source life alone.
You could ask people to fill in a nice shared spreadsheet... You'd get a dozen lines the first week and nothing after that. We tried that once.
We wanted to thank people for the work they already do for us in the open, see which upstream projects Michelin actually shows up in, and know whether things get better over time. With people's agreement, obviously.
Meet giveback
That's what giveback does, and it's a GitHub App. An engineer installs it on their GitHub account and proves they hold a verified Michelin email address. That's the whole onboarding. From then on, giveback collects their public pull requests and pushes to repositories they don't own, follows each pull request until it's merged or closed, and puts the result on a dashboard that every enrolled engineer can open.

It counts contributions to Michelin's own repositories and to everyone else's, and keeps the two apart. Both matter, but they don't mean the same thing. A merge in one of our own repos is nice, but a fix accepted by maintainers who owe us nothing means a lot more, at least to me.
Consent first, and nothing private
Since the accounts are personal, I wanted the rules to be obvious, and I wanted them enforced by the design rather than promised in a policy.
Installing the App is the consent. Nobody is tracked until they install it, and uninstalling it stops the collection. If someone installs it without a Michelin address, the App uninstalls itself instead of sitting on their account doing nothing.
The App asks GitHub for exactly one permission: reading the user's email addresses. It uses it once, at enrollment, to find a verified address on one of the company's domains. It stores that single address and throws the token away. It has no access to any repository or organization, so it can't see code, private repositories, or issues. Everything it collects is already public on github.com. giveback just gathers it in one place.
Your own repositories never count. Anything under your own name (side projects, dotfiles, that game you've been writing at night) is skipped before giveback even looks at it. Pull requests opened by bots like Dependabot or Renovate don't count either.
None of this is about keeping an eye on people's free time. The goal is to give Michelin's open source work a frame, and the credit it deserves.
How it works

One constraint shaped the whole design: the contributions we care about live in repositories the App has no access to. GitHub only sends webhooks for repositories an installation covers, so webhooks were out. What's left is each user's public events feed (GET /users/{login}/events), which lists what they did anywhere on GitHub. So giveback polls it.
The feed has a catch, though. It lists what someone did, not what they wrote. A maintainer of a busy project labels, closes and merges other people's pull requests all day, and every one of those actions lands in their feed. Taken at face value, all of those pull requests would count as theirs. On our production data, 491 of the 1,144 pull requests giveback had stored were opened by someone else. That's why giveback asks GitHub who opened each pull request, and only counts the ones you opened yourself.
Four scheduled jobs share the work:
sync_contributionsreads each enrolled user's events feed and records new pull requests and pushes.poll_non_closed_pull_requestsre-checks every open pull request until it's merged or closed. A pull request that takes six weeks to land still counts.reconcile_installationskeeps the list of installations in line with GitHub: suspended accounts, uninstalls, renamed logins.sync_repositoriesrefreshes the repositories people contribute to, with their current name, their stars, and whether they still exist.
Polling thousands of accounts every few minutes would be wasteful, so I made the schedule adapt. Someone who just contributed gets checked every 15 minutes. Each time nothing new shows up, their interval doubles, up to a ceiling of 4 hours. Requests are conditional, so an unchanged feed comes back as a 304 that doesn't count against GitHub's rate limit.
I also wanted it to be boring to run. The whole thing is one Node.js process, built on Probot with Bree as the scheduler, and one PostgreSQL database. No message broker or cache required. If you do run several instances, they coordinate through the database: each job claims its rows with a lease, so two processes never do the same work twice.
What the dashboard shows
The dashboard has three tabs.
Me is your own track record: what you've merged, what's still waiting for a review, and how your time to merge compares with everyone else's. Every line links back to the pull request on GitHub.

Overall is the company view. Month by month, it splits contributions to our own repositories from contributions elsewhere, with merge rates, time to merge, and pull request sizes. It also lists the biggest projects we've landed code in, and who got it there. That list is my favorite part. You open it and the big names are right there, projects with thousands of stars where one of us got a pull request merged, and that feels pretty good. Next to it, a second list shows where we contribute the most, which isn't necessarily the same thing.

Operations is for the few people who run giveback. It shows job health, the queue, every enrolled account, and a button to force a sync. When something fails, the page says what failed and why, which saves a trip to the logs.

Because not everything happens on github.com, or on an enrolled account, an operator can also record a contribution by hand. They paste the address of a GitHub pull request or a public GitLab merge request, giveback fills in the title, dates and counts, and the operator picks who gets the credit.
Make it yours
I kept Michelin out of the code on purpose. If you'd like to measure what your own company gives back, everything that differs from one company to the next is configuration:
| Setting | What it does |
|---|---|
ALLOWED_EMAIL_DOMAINS | The email domains allowed to enroll. There's no default on purpose: a wrong one would leave the bot looking healthy while enrolling nobody. |
OWN_GITHUB_ORGS | Your GitHub organizations. It only splits the figures between "ours" and "elsewhere" and never limits what gets collected. |
DASHBOARD_ALLOWED_LOGINS | The GitHub logins of your operators, who get the Operations tab. Everyone else sees Me and Overall. |
EVENTS_CRON and the other *_CRON settings | How often each job wakes up. |
SYNC_MIN_INTERVAL_MINUTES, SYNC_MAX_INTERVAL_MINUTES | The floor and ceiling of the adaptive schedule, 15 minutes and 4 hours by default. |
GITLAB_API_HOSTS | The GitLab instances the manual entry form may read merge requests from. |
For a company called Acme, a minimal setup might look like this:
ALLOWED_EMAIL_DOMAINS=acme.com,acme-labs.com
OWN_GITHUB_ORGS=acme,acme-oss
DASHBOARD_ALLOWED_LOGINS=jane-doe,john-ops
Swap in your logo, which is a single SVG file, and you're done. The defaults are sized for a few thousand enrolled engineers on a single instance.
git clone to your own dashboard in seven steps.What it doesn't do (yet)
giveback counts pull requests and pushes. Reviews, issues and comments are real contributions too, and they aren't in the numbers today.
It also relies on GitHub's public events feed, which only goes back about 90 days. Whatever you did before that window won't show up unless an operator records it by hand. GitLab contributions are manual for now as well.
If one of those gaps matters to you, open an issue, or better, a pull request.
Over to you
We started with a question we couldn't answer. giveback won't make anyone contribute more. It just makes sure the work people already do for Michelin in the open stops going unnoticed, and gives our OSPO and the OSS Champions real numbers to work with.
It's open source under the Apache 2.0 license, at github.com/michelin/giveback. Try it on your own company and tell us what breaks. You'll find our other projects and the way we work with open source on opensource.michelin.io.
And if you're a Michelin engineer reading this, go install the App. I'm really curious how much of your work for Michelin we've been missing all along.