GitHub Repository Hygiene ⚡ Automate Access Reviews
Boost GitHub repository hygiene with GitHub access reviews and outside collaborator management. Automate stale repository archival using GitHub automation—get started!

Make GitHub Repository Hygiene Routine, Not a Cleanup Sprint
Outside collaborators need access to repositories to get work done. The problem is what happens later: projects end, permissions change, and repositories stop being maintained. If nobody owns the follow-up, temporary access and dormant repositories can hang around long after their purpose is gone.
The usual fix is a spreadsheet, a calendar reminder, and a repository owner who remembers. That works for a small team. But across hundreds or thousands of repositories, it turns into a process that people put off - and then struggle to audit.
GitHub's Evergreen pattern takes a more structured approach. It uses a GitHub App with scheduled Actions workflows, repository issues, webhooks, custom properties, and state files stored in a controller repository. The goal isn't to build a governance platform. It's to make recurring access reviews and stale-repository archiving predictable, visible, and recoverable.
This article explains how the pattern works, where its safeguards matter, and how to adapt it without turning routine housekeeping into a fragile automation project.
Why repository hygiene needs a system
Repository hygiene can sound like busywork, but it has real security and operational consequences. An outside collaborator who no longer needs access is an unnecessary path in. And a repo that has slipped into "nobody's maintaining this" can still hold code, configuration, or context that someone assumes is still current.
Neither problem is solved by publishing a policy. The hard part is applying it consistently across repositories, making it clear who can make decisions, and handling cases where nobody responds.
Manual reviews do not scale with the organization
For a single repository, a human owner can check who's on the list, ask the project team, and remove access. That workflow is manageable because the context is right there.
Once you multiply it across many repositories, the work stops being "review a list" and becomes "track the system." Someone has to keep up with which repositories are in scope, when each review is due, who owns each repo, who actually replied, and whether the changes succeeded.
A spreadsheet can hold that information, but it creates another thing to maintain. If ownership changes and someone forgets to update a row, the review starts from stale data. If people handle decisions in email or chat, the decision is harder to find later. And if access only gets removed after a final reminder works its way through someone's inbox, silence becomes the default security posture.
The real lesson isn't "never do this manually." It's that repetitive work needs a reliable trigger and a record of what happened. People should make the decisions that require context; automation should handle reminders, deadlines, and consistent execution.
Make the outcome of silence explicit
Evergreen's central security choice is fail secure: if an access review deadline passes without a decision, the collaborators from the review snapshot are removed. That's a deliberate default, not a one-size-fits-all rule.
For some teams, external work depends on uninterrupted access. In that case, automatic revocation could disrupt delivery. You may need a different escalation path or a longer grace period. But either way, the process shouldn't quietly treat "no response" as "approval." Decide what silence means, tell teams up front, and spell out the consequences in each review request.
There's a second principle next to fail secure: don't destroy the work. Evergreen archives stale repositories rather than deleting them. Access changes and outcomes are recorded so mistakes can be understood and corrected. That distinction matters - security automation should reduce exposure, not make recovery unnecessarily hard.
How Evergreen's architecture works
Evergreen is a GitHub App backed by a small Go webhook server running in a container. Instead of introducing a separate dashboard, queue, and audit database, it leans on existing GitHub building blocks.
The controller repository is where the system keeps time and state. Scheduled GitHub Actions workflows send repository_dispatch events to wake the app up on a quarterly cadence. The app then evaluates repositories and runs the appropriate action. Actions can also be triggered manually with workflow_dispatch when an operator needs to start a step outside the regular schedule.
Keep state in files that can be reviewed
Evergreen stores per-repository state as JSON files in the controller repository, under state/repos/<repo_id>.json. A record can include the collaborator snapshot, when a review started, its deadline, the decision, and the actions taken.
This gives you a clear audit trail: the state file is versioned in Git. If someone asks why a collaborator was removed in a previous cycle, the commit history shows when the state changed. This isn't an argument to dump all kinds of app data into Git - it works here because the state is structured, relatively small, and benefits from reviewable history.
A file-based model also comes with trade-offs. You have to think about concurrent updates, commit permissions, error handling, and how state is recovered if a workflow is interrupted. A database may be a better fit if your volume or write pattern grows beyond what a controller repository can handle cleanly. The main idea is simple: pick the storage model that preserves the audit and recovery properties you actually need.
Use repository issues as the interface
For an access review, Evergreen opens an issue in each repository and lists the outside collaborators under review. The reviewer checks who should keep access and closes the issue.
When the app receives the issues.closed webhook event, it reads the selections, keeps the checked collaborators, removes the rest, and posts a comment that documents what happened.
This is a pragmatic interface choice. The request shows up where repository teams already work, and the issue becomes a visible record of both the request and the outcome. It also avoids asking owners to log into a separate tool just to finish a recurring task.
An issue isn't automatically the right interface everywhere. It depends on whether owners actually use issues, whether the information can be shared safely in that space, and whether the review cadence creates too much noise. Evergreen accounts for repositories where issues are disabled as an operational edge case. Before you adopt the pattern, test with real repository owners - not just assumptions about how "GitHub issues" should feel in practice.
Resolve ownership without maintaining a spreadsheet
The system needs to notify the people who can actually decide access. Evergreen first checks a repository custom property named evergreen_repo_owner to identify a user or an organization team. If that property doesn't exist, it falls back to recent issue and commit authors and selects those who belong to the access-review team.
The explicit property is more predictable. The activity-based fallback reduces the burden of keeping ownership data up to date everywhere. Neither should be treated as perfect. Contributors aren't necessarily accountable owners, and repository owners can drift out of date. Use the fallback to find likely reviewers, and give teams a clear way to correct ownership when the notification lands with the wrong person.
Design the workflows around safe decisions
The mechanics matter less than the guarantees around them. Access and archival automation can change things across an organization, so a useful design makes the safe path obvious, gives people time to respond, and doesn't pretend everything succeeded when part of it failed.
Separate review from cleanup
Evergreen spreads its quarterly work across the month:
| Day of the month | Workflow | Action |
|---|---|---|
| 1st | Stale repository review | Flag repositories with no recent human activity and open a warning issue. |
| 8th | Access review | Snapshot outside collaborators and open a checklist issue in each repository. |
| 16th | Stale repository cleanup | Archive repositories that remain stale after the grace period. |
| 23rd | Access cleanup | Revoke access for reviews that remain unanswered. |
That gap matters. It gives repository owners time to respond. A reminder comment arrives before the deadline, and the issue states what action is required, the exact deadline, and what happens if nobody responds.
A hard grace period is also built in, so a manual workflow run can't accidentally skip the response window and trigger cleanup immediately. It's a small design detail with a big operational effect: manual controls are useful, but they shouldn't bypass safety guarantees by default.
The schedule is just an example. Pick a cadence that fits your project cycle, risk tolerance, and response capacity. A quarterly review may work for a shared services organization; a higher-risk environment may need more frequent checks. The key is to separate notification from enforcement - and make the timing between them intentional.
Fail closed when a removal is only partly successful
External API calls can fail. A removal might succeed for one collaborator but hit a transient error for another. If the automation marks the review "done" anyway, your record no longer matches the actual access state.
Evergreen handles this by keeping the review pending if any removal fails. The issue stays open, and a later run retries whatever remains. The workflow records the review as complete only after every removal succeeds.
That distinction is important: an error means the task didn't actually finish. Retrying is only safe when the workflow can identify what's still outstanding and avoid writing misleading state. Log partial results, keep retries idempotent where you can, and show the operator whether the system is waiting, retrying, or finished.
Restrict who can approve access
Closing a review issue is a privileged action because it determines who keeps access. Evergreen checks whether the person closing the issue is authorized. If a non-Hubber closes it, the app reopens the issue and explains why the decision wasn't accepted.
This prevents the interface from turning into an accidental permission boundary. "Can click the issue UI" isn't the same thing as "can approve access." When you build a similar process, explicitly define who is allowed to make the decision and test what happens if an unauthorized person tries to complete the review.
For repositories that shouldn't participate, organization owners can set opt-out custom properties: evergreen_exempt_from_access_review or evergreen_exempt_from_archive. Exemptions are useful, but they need ownership and periodic review. Otherwise, an exception that was meant for one project can become a permanent blind spot.
Roll out automation without surprises
A system that changes permissions or repository status shouldn't go live across the whole organization right after a quick happy-path test. The first run should be uneventful because the decisions have been checked, the message has been communicated, and the failure modes are understood.
Start in dry-run mode
Evergreen includes a DRY_RUN mode that logs proposed writes without changing collaborator access or repository status. Running this against your real organization helps you catch mismatches between the policy and the actual repository data.
Review what it outputs: empty collaborator lists, unusual permissions, disabled issues, missing ownership, and repositories that look stale but are still being used. A dry run isn't just a technical test. It's a policy check you can see before anything is enabled.
Next, test the full workflow in a small set of repositories that represent reality. Include normal situations and awkward edge cases. Confirm that an authorized reviewer can complete a review, an unauthorized user can't, a missed deadline produces the intended outcome, and a failed API operation stays visible and retryable.
Communicate the process before the first issue arrives
Tell repository owners what's changing, when the first review will happen, what they need to do, and what happens if they take no action. Do this before you enable the automation - not in the first issue they receive.
This can feel repetitive in a large organization, but it still matters. People miss announcements, move between teams, and only encounter automated processes months after launch. Put the essential instructions in each issue so the request is understandable even if someone didn't see the earlier message.
Observe the workflow and respect platform limits
Evergreen emits structured logfmt logs to a dedicated Splunk index. Centralized logs make it easier to spot edge cases and tighten the logic before they turn into bigger problems.
Also consider platform pacing. Opening issues and posting comments across many repositories can hit GitHub's secondary rate limits. The Evergreen implementation was designed around limits cited in its source account - 80 content-creating requests per minute and 500 per hour - so check current GitHub REST API rate-limit guidance if you're implementing this pattern elsewhere.
Evergreen processes repositories in batches, spaces content-creating calls by a few seconds, and pauses between batches. It counts only repositories that need content, rather than treating every repository as a request. That means quiet cycles can finish quickly while busy ones get paced intentionally.
Proactive pacing is usually easier than hitting a limit and trying to recover after the fact. Plan for the expected workload: estimate how many repositories you'll touch, account for retries, and keep the schedule tolerant of larger organizations' request volumes.
What to reuse - and what to decide for yourself
The valuable part of Evergreen isn't the specific Go webhook server or a particular calendar. It's how the components fit together: a scheduled controller starts work, a GitHub App reacts to events, issues provide the human interface, repository metadata helps find owners, and version-controlled state records what happened.
Before implementing the same pattern, decide these points explicitly:
- Scope: Which repositories and collaborator types are included? How do you define stale activity?
- Authority: Who may approve access, and how does the system verify that authority?
- Default: What happens when a review is ignored? Is automatic revocation appropriate for every repository?
- Timing: How long is the response window, and what prevents manual runs from bypassing it?
- Recovery: How are changes recorded, retried, and reversed after an incorrect decision?
- Exceptions: Who can exempt repositories, and how are exemptions revisited?
- Scale: How will the workflow handle API failures, rate limits, and large batches?
- Operations: Who receives alerts when the automation cannot finish a review?
For the implementation details, see the original Evergreen engineering article. You can also review GitHub's documentation for GitHub Apps, scheduled workflows, and organization custom properties.
This pattern works best when the work is repetitive, the consequences are understood, and the platform features your teams already use can carry the interaction. It's a poor fit when ownership data is unreliable, repository owners don't actually engage with issues, or a missed review could disrupt critical work without a real escalation path. Fix the operating model first - automating a confusing process usually just makes it faster to repeat, not safer.
Conclusion
Access reviews and stale-repository cleanup are easy to postpone because each individual step looks small. The risk comes from doing that decision repeatedly across an organization without a process you can rely on.
Evergreen shows how to make the work routine using platform primitives: scheduled workflows for cadence, issues to involve repository owners, webhooks to carry out decisions, and versioned state to preserve an audit trail. The strongest ideas aren't flashy. They're operational: fail secure, avoid deleting data, retry incomplete work, and make every change understandable and reversible.
Start with one recurring task - reviewing outside collaborators in a small set of repositories. Dry-run the rules, confirm the owners and consequences, and watch a full cycle complete before expanding. The goal isn't to automate everything. It's to make the safe, boring action happen reliably.