What Evergreen does
Shared organizations that host outside collaborators accumulate the same manual chores: confirming who still needs access, adjusting permissions as projects shift, and archiving repositories that have outlived their purpose. At org scale, tracking collaborators, deadlines, and follow-ups repository by repository is work that gets deferred.
Evergreen is a GitHub App built from native GitHub features to handle two of those chores:
- Outside collaborator access reviews. Quarterly, each repo's outside collaborators are reviewed; those who are needed stay, the rest are removed.
- Stale repository detection and archival. Repos with no human activity past a threshold are flagged, warned, and archived if they remain untouched.
Two principles govern the design. Fail secure: if nobody responds to an access review, access is revoked rather than left in place — the safe outcome is the default outcome. Never destroy anything: Evergreen never deletes a repository or its contents. Archived repos are recoverable, and every access change is recorded in the repo's tracking issue, so reverting a mistake means copy-pasting the affected handles.
Architecture: native primitives, choreographed
Evergreen itself is a small Go webhook server running in a container. The substance is in the choreography: a GitHub App, scheduled actions, webhooks, issues, custom properties, and plain files in a repo.
Clock and memory
A dedicated controller repo holds Evergreen's schedule and state. Four scheduled GitHub Actions workflows fire repository_dispatch events quarterly; the app wakes and works. Reviews open two weeks before cleanup runs, with a hard grace period so a manual run can never fast-forward the deadline. Each workflow also supports workflow_dispatch for hand-triggered steps.
| Day | Workflow | What happens |
|---|---|---|
| 1st | Stale repo review | Flag repos with no recent human activity, open a warning issue |
| 8th | Access review | Snapshot outside collaborators, open a checklist issue per repo |
| 16th | Stale repo cleanup | Archive repos still stale after the grace period |
| 23rd | Access cleanup | Auto‑revoke access for any review left unanswered |
State as committed JSON
Per-repo status lives in JSON files committed to the controller repo at state/repos/<repo_id>.json. Each snapshot records who had access, when the review opened, the deadline, who decided, and what happened.
{
"repo_id": 123456789,
"full_name": "the-org/acme-trial",
"last_seen_at": "2026-04-13T00:05:00Z",
"access_review": {
"deadline": "2026-04-27T00:00:00Z",
"status": "renewed",
"collaborators_at_open": [
{ "login": "alice", "name": "Alice Example", "permission": "admin" },
{ "login": "bob", "name": "Bob Example", "permission": "write" }
],
"renewed_logins": ["alice"],
"decided_by": "carol-hubber",
"decided_at": "2026-04-15T16:42:10Z",
"tracking_issue": { "repo_id": 123456789, "number": 42 }
},
"staleness": {
"last_activity_at": "2026-04-15T16:42:10Z",
"warned_at": null,
"archive_deadline": null,
"status": "active",
"tracking_issue": null
}
}
Because state is just files in git, the commit history is the audit trail. To learn why someone lost access last spring, git log the state file — there is no separate database or custom audit pipeline.
Issue-ops in each repo
Evergreen communicates through issues in the repositories themselves. An access review opener posts a checklist issue:
![Screenshot of an access review issue titled “[ACTION REQUIRED] Access Review – 2026-04-13.” Outside collaborators Alice Example, with admin access, and Bob Example, with write access, have unchecked boxes. Instructions say to select anyone who should keep access, then close the issue. A warning states that if no action is taken by April 27, all listed access will be automatically revoked.](https://github.blog/wp-content/uploads/2026/10/674986199-ee305d10-3419-4270-a829-41ed3da29fae.png?resize=528%2C221)
The interaction is deliberately minimal: check the boxes for collaborators who stay, close the issue. On the issues.closed webhook, Evergreen reads the checkboxes, retains the checked collaborators, revokes the rest, and posts a receipt comment listing what it kept and removed.

Authorization and notification
Closing a review issue is privileged. If a non-Hubber closes one, Evergreen reopens it and posts an explanation; only authorized decisions count.
Knowing which Hubber owns a given repository is a subtler problem. Evergreen resolves recipients through a fallback chain:
- An
evergreen_repo_ownercustom property on the repo (a@useror@org/team). - Otherwise, recent issue and commit authors who belong to the access review team — the Hubbers actually working in that repo.
Owners get pinged automatically without anyone maintaining a spreadsheet of ownership.
Failing closed on partial removal
When a deadline passes with no response, cleanup revokes every collaborator in the snapshot. Evergreen handles the case where some removals succeed and others hit transient API errors by failing closed: if any removal fails, the review stays pending, the issue stays open, and the next run retries the stragglers. A review is recorded as "revoked" only after every removal succeeded. Everything removed is listed in the issue, so restoring access means re-adding the listed handles.
Rollout practices
Automation that manages access and archives repositories warrants a careful rollout — this deployment served 1,400+ Hubbers plus customers and partners, and the goal was for the first run to be a non-event.
- Dry run first. A
DRY_RUNmode logs every write it would make without touching a collaborator or repo, so the planned decisions can be inspected against the real org before anything changes. - Overcommunicate. Announce in advance what is coming, when, and what people must do, so the first issue anyone sees is not a surprise.
- Observability. Structured
logfmtlogs go to a dedicated Splunk index. That is how edge cases surfaced in the wild — empty collaborator lists, unusual permissions, repos with issues disabled — and were fixed before they mattered. - Respect rate limits. Opening issues and posting comments across every repo in a large org runs into GitHub's secondary rate limits for content creation: 80 requests/minute and 500/hour. Evergreen paces itself by construction — repos are processed in batches, each content-creating call is spaced by a couple of seconds, and it sleeps between batches to clear the rolling window. Only repositories that actually create content are counted, so a quiet quarter runs fast. The choice was proactive avoidance over reactive back-off.
Design takeaways
- Make the safe path the default: fail secure, then make responding easy enough that failing secure rarely triggers.
- Use existing primitives — issues, webhooks, custom properties, and git history replaced a bespoke UI, a job queue, and an audit database.
- Meet people where they work: an issue in their repo beats a dashboard they never open.
- Logs serve as a revert button when every change is written into the issue.
- At org scale, rate limits are a design constraint rather than an edge case.
The user-facing experience follows the same intent: the work appears as an issue in your own repo, the right people are tagged, each issue states the ask and the exact deadline along with what happens if it is ignored, a reminder lands before the deadline so revocations are not surprises, and org owners can opt out via the evergreen_exempt_from_access_review or evergreen_exempt_from_archive custom properties.
Because it is assembled entirely from native GitHub features, the same pattern can be stood up elsewhere: start with one recurring access or archival task and build from GitHub Apps, actions, issues, and custom properties.



