Wink Pings

If no one replies, revoke: GitHub turns permission cleanup into a quarterly pipeline

Inside GitHub, they built an app called Evergreen using issues, webhooks, and a few JSON files. It reviews external collaborator permissions quarterly; if no one responds, access is revoked by default. git log serves directly as the audit log. The whole thing has no third-party dependencies, so the barrier to copying it is low.

External collaborator permissions are the kind of thing everyone knows should be cleaned up, but no one wants to clean up. Projects turn over, but the permissions stay. Someone on X put it well: every organization has a 2019 contractor who still has admin.

GitHub ran into this problem itself. They have a shared organization where internal employees work with customers and partners on product trials and delivery. People come and go, and the sources of permissions quickly stop matching reality. Their solution is a GitHub App called Evergreen. The author, Greg Mohler, wrote about the implementation details and rollout process on the GitHub blog.

Evergreen project image

Evergreen does two things.

  1. Every quarter, it reviews the external collaborators on each repository, keeps the people who are needed, and removes the rest.
  2. It detects repositories with no human activity for a long time, then marks them, warns about them, and finally archives them.

Two design principles run through everything. Fail safe: if no one responds to a review, do not gamble—revoke access directly. The safe outcome is the default outcome. Never destroy: do not delete repositories or content; archives are recoverable; every permission change is written into the repository's tracking issue, and fixing a mistake is just copy and paste.

The parts are all off the shelf

Underneath is a webhook service written in Go, running in a container. What is really interesting is the orchestration. Every part is a native GitHub feature: Apps, scheduled actions, webhooks, issues, custom properties, and ordinary files in repositories.

A dedicated controller repository serves as the clock and the memory. Four scheduled workflows trigger repository_dispatch events every quarter.

DayProcessAction
Day 1Stale repository reviewMark repositories with no recent human activity and open a warning issue
Day 8Permission reviewSnapshot external collaborators and open one checklist issue per repository
Day 16Stale repository cleanupArchive repositories that are still stale after the grace period
Day 23Permission cleanupAutomatically revoke permissions for reviews that received no response

Reviews open two weeks in advance, there is a hard grace period before cleanup, and even manual triggering cannot fast-forward the deadline. Each workflow also supports workflow_dispatch, so any step can be run manually when needed.

State is just a JSON file you can git blame. There is one per repository, committed under state/repos/<repo_id>.json in the controller repository. Who has access, when the review opened, the deadline, who decided, and what the result was—it is all in there. Because state is a file in git, the commit history itself is the audit log. Want to know why someone lost access last spring? Just git log that state file. No separate database, no custom audit pipeline.

All communication happens in issues

When a review starts, each repository gets a checklist issue. The interaction could not be simpler: check the people to keep, then close the issue. Evergreen listens for issues.closed, reads the checkboxes, keeps the checked people, revokes the rest, and posts a receipt comment listing who was kept and who was removed.

Permission review issue screenshot

Quarterly permission review flowchart

Closing a review issue is a privileged operation. If an unauthorized person closes it, the App quietly reopens it and explains why. Only decisions made by the people who are supposed to make them count.

Notifications follow a fallback chain. If the repository has the evergreen_repo_owner custom property set, notify the people and teams listed there. If it is not set, look at recent issue and commit authors, and cc the ones who are on the access review team. There is no table to maintain of who owns which repository.

Fail-closed behavior is an easy detail to overlook. If there is no response by the deadline, cleanup revokes everyone in the snapshot. But what if some removals succeed and others hit API errors? Marking the review complete would create silent inconsistency. Evergreen's choice is: if any removal fails, the review stays pending, the issue stays open, and the next round retries. Only when everything succeeds does it record revoked. Everyone who was revoked is written into the issue, and restoring access is just adding those handles back.

Rollout is harder than writing code

This organization has more than 1,400 employees, plus even more customers and partners. Automating permission management and archiving requires care. Their goal was for the first run to do nothing at all.

Start with a dry run. Watch what it would do in the logs, without touching real collaborators or repositories, and read the logs until you trust it.

Over-communicate. Broadcast ahead of time what will happen, when, and what people need to do. By the seventh time you say it, someone is often still hearing it for the first time.

Splunk observability. Structured logfmt logs go into a dedicated index. Empty collaborator lists, unusual permissions, and repositories that closed the issue were all edge cases caught in the logs after launch.

Rate limits have to be treated as a design constraint. Large-scale IssueOps runs into GitHub's secondary rate limits for content creation: 80 requests per minute and 500 per hour. Evergreen is rate-limited by construction: it processes in batches, waits a few seconds between each content-creating call, sleeps between batches, and counts only repositories that will actually create content. Quiet quarters run fast.

Where the real value is

The most valuable part of Evergreen is not the automation itself. It is defining inaction as the safe default: if you do not respond, access goes away, and risk does not sit there waiting for you to remember it next time. At the same time, there is an undo: the list is written into the issue, and copy-paste restores access.

Someone online said they wish the agent would only propose removals and leave the revoke button to a human. That idea is reasonable, but Evergreen chose another path: humans do the checking, and machines do the execution and record-keeping. Both can work; the difference is whether you trust the rule that no response means revoke by default.

Someone else mentioned that small teams need this more than large organizations. Freelancers come and go, and access to repositories, payment backends, and shared drives often outlives the contract. Spending ten minutes a month reviewing it can block most of the risk. The entire Evergreen setup uses native features, has no third-party dependencies, and is not hard to copy.

The blog post ends by saying that routine hygiene is the easiest thing to postpone and the best thing to automate. The trick is not a heroic cleanup sprint; it is making the boring but safe thing happen on schedule, with a friendly face and an undo button.

Original article: Staying Evergreen

X post: GitHub on X

发布时间: 2026-10-25 04:31