{"id":5285,"date":"2026-10-15T22:16:44","date_gmt":"2026-10-15T22:16:44","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/10\/15\/staying-evergreen-how-we-automated-secure-access-and-repo-hygiene-in-a-shared-org\/"},"modified":"2026-10-15T22:16:44","modified_gmt":"2026-10-15T22:16:44","slug":"staying-evergreen-how-we-automated-secure-access-and-repo-hygiene-in-a-shared-org","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/10\/15\/staying-evergreen-how-we-automated-secure-access-and-repo-hygiene-in-a-shared-org\/","title":{"rendered":"Staying Evergreen: How we automated secure access and repo hygiene in a shared org"},"content":{"rendered":"<p class=\"wp-block-paragraph\">Working with outside collaborators can add more work for a repository owner. You need to confirm who still needs access, change permissions as projects evolve, and archive repositories once they\u2019ve served their purpose.<\/p>\n<p class=\"wp-block-paragraph\">To keep up, repository owners need to track collaborators, deadlines, and follow-ups for each repository. As a project grows, that manual housekeeping becomes harder to keep up with and easier to defer.<\/p>\n<p class=\"wp-block-paragraph\">We faced this problem in a shared organization where GitHub employees (\u201cHubbers\u201d) collaborate with customers and partners on product trials and professional services engagements. Our answer was <strong>Evergreen<\/strong>, a GitHub App built with native GitHub features. The workflows below exhibit a pattern you can adapt for your own organization.<\/p>\n<h2 class=\"wp-block-heading\">Meet Evergreen<\/h2>\n<p class=\"wp-block-paragraph\">Evergreen is a GitHub App that automates organization hygiene through two workflows:<\/p>\n<ol class=\"wp-block-list\">\n<li><strong>Outside collaborator access reviews.<\/strong> Every quarter, each repo\u2019s outside collaborators are reviewed. Keep who you need, and everyone else is removed.<\/li>\n<li><strong>Stale repository detection and archival.<\/strong> Repos with no human activity past a threshold get flagged, warned, and (if still untouched) archived.<\/li>\n<\/ol>\n<p class=\"wp-block-paragraph\">Two design principles shaped everything:<\/p>\n<p class=\"wp-block-paragraph\"><strong>Fail secure.<\/strong> If nobody responds to an access review, we don\u2019t leave access in place and hope for the best. We revoke it. The safe outcome is the <em>default<\/em> outcome.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Never destroy anything.<\/strong> Evergreen will <strong>never<\/strong> delete a repository or its contents. Archived repos are recoverable, and every access change is written into the repo\u2019s tracking issue, so reverting a mistake is as easy as a copy-paste to Copilot.<\/p>\n<h2 class=\"wp-block-heading\">How it works: One brain, many hands<\/h2>\n<p class=\"wp-block-paragraph\">Under the hood, Evergreen is a small Go webhook server running in a container. But the interesting part is the <em>choreography<\/em>. Every moving piece is a stock GitHub feature: a GitHub App, scheduled actions, webhooks, issues, custom properties, and plain files in a repo. Nothing exotic to reinvent.<\/p>\n<h3 class=\"wp-block-heading\">A controller that keeps time<\/h3>\n<p class=\"wp-block-paragraph\">A dedicated <strong>controller repo<\/strong> acts as Evergreen\u2019s clock and its memory. Four scheduled GitHub Actions workflows fire <code>repository_dispatch<\/code> events on a quarterly cadence, and the app wakes up and does the work:<\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Day<\/strong><\/th>\n<th><strong>Workflow<\/strong><\/th>\n<th><strong>What happens<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>1st<\/td>\n<td>Stale repo review<\/td>\n<td>Flag repos with no recent human activity, open a warning issue<\/td>\n<\/tr>\n<tr>\n<td>8th<\/td>\n<td>Access review<\/td>\n<td>Snapshot outside collaborators, open a checklist issue per repo<\/td>\n<\/tr>\n<tr>\n<td>16th<\/td>\n<td>Stale repo cleanup<\/td>\n<td>Archive repos still stale after the grace period<\/td>\n<\/tr>\n<tr>\n<td>23rd<\/td>\n<td>Access cleanup<\/td>\n<td>Auto\u2011revoke access for any review left unanswered<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p class=\"wp-block-paragraph\">Reviews open two weeks before their cleanup runs, and there\u2019s a hard grace period baked in so a stray manual run can never fast\u2011forward the guillotine. Each workflow also supports <code>workflow_dispatch<\/code>, so we can trigger any step by hand as needed.<\/p>\n<h3 class=\"wp-block-heading\">State you can <code>git blame<\/code><\/h3>\n<p class=\"wp-block-paragraph\">Evergreen tracks every repo\u2019s status as a JSON file (one per repository) committed to the controller repo under <code>state\/repos\/&lt;repo_id&gt;.json<\/code>. The snapshot of who had access, when the review opened, the deadline, who decided, and what happened all lives there. Here\u2019s a completed review where a Hubber kept Alice\u2019s access and let Bob\u2019s lapse:<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code has-custom-css wp-custom-css-685b1e25\"><code>{\n  \"repo_id\": 123456789,\n  \"full_name\": \"the-org\/acme-trial\",\n  \"last_seen_at\": \"2026-04-13T00:05:00Z\",\n  \"access_review\": {\n    \"deadline\": \"2026-04-27T00:00:00Z\",\n    \"status\": \"renewed\",\n    \"collaborators_at_open\": [\n      { \"login\": \"alice\", \"name\": \"Alice Example\", \"permission\": \"admin\" },\n      { \"login\": \"bob\", \"name\": \"Bob Example\", \"permission\": \"write\" }\n    ],\n    \"renewed_logins\": [\"alice\"],\n    \"decided_by\": \"carol-hubber\",\n    \"decided_at\": \"2026-04-15T16:42:10Z\",\n    \"tracking_issue\": { \"repo_id\": 123456789, \"number\": 42 }\n  },\n  \"staleness\": {\n    \"last_activity_at\": \"2026-04-15T16:42:10Z\",\n    \"warned_at\": null,\n    \"archive_deadline\": null,\n    \"status\": \"active\",\n    \"tracking_issue\": null\n  }\n}<\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">The best part: because state is just files in git, <strong>the commit history <em>is<\/em> the audit trail.<\/strong> Want to know why someone lost access last spring? <code>git log<\/code> the state file. There\u2019s no separate database or bespoke audit pipeline to track.<\/p>\n<h3 class=\"wp-block-heading\">Issue\u2011ops, everywhere it matters<\/h3>\n<p class=\"wp-block-paragraph\">Evergreen does its talking through issues inside each repository. When an access review kicks off, every repo gets a checklist issue:<\/p>\n<figure class=\"wp-block-image size-full\"><img data-opt-id=596740062  fetchpriority=\"high\" decoding=\"async\" data-recalc-dims=\"1\" width=\"528\" height=\"221\" src=\"https:\/\/github.blog\/wp-content\/uploads\/2026\/10\/674986199-ee305d10-3419-4270-a829-41ed3da29fae.png?resize=528%2C221\" alt=\"Screenshot of an access review issue titled \u201c[ACTION REQUIRED] Access Review \u2013 2026-04-13.\u201d 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.\" class=\"wp-image-99535\" \/><\/figure>\n<p class=\"wp-block-paragraph\">The interaction model is delightfully simple: <strong>check the boxes for who stays, then close the issue.<\/strong> That\u2019s it. When the issue closes, Evergreen listens for the <code>issues.closed<\/code> webhook event, reads the checkboxes, retains the checked collaborators, and revokes the rest, then posts a receipt comment listing exactly what it kept and what it removed.<\/p>\n<figure class=\"wp-block-image size-full\"><img data-opt-id=2088330212  fetchpriority=\"high\" decoding=\"async\" data-recalc-dims=\"1\" width=\"2000\" height=\"417\" src=\"https:\/\/github.blog\/wp-content\/uploads\/2026\/10\/674985578-53474bf2-cd36-4345-96ef-24c79268a858.png?resize=2000%2C417\" alt=\"Quarterly outside-collaborator access review flowchart. A dispatch snapshots outside collaborators and opens a checklist issue in each repository. If a team member closes the issue, checked collaborators retain access and unchecked collaborators have access revoked. If no one responds by the deadline, everyone\u2019s access is automatically revoked. Both paths log the decision in the issue and state.\" class=\"wp-image-99534\" \/><\/figure>\n<h3 class=\"wp-block-heading\">Only the right humans can pull the lever<\/h3>\n<p class=\"wp-block-paragraph\">Closing an access review issue is a privileged action, so Evergreen guards it. If someone who <em>isn\u2019t<\/em> a Hubber closes an issue, the app quietly reopens it and posts a note explaining why. Decisions only count when they come from the people authorized to make them.<\/p>\n<h3 class=\"wp-block-heading\">Informing the right people<\/h3>\n<p class=\"wp-block-paragraph\">Here\u2019s a subtle problem: we know the people who need to respond to a review are Hubbers, but we didn\u2019t always know <em>which<\/em> Hubber(s) owned a given repository.<\/p>\n<p class=\"wp-block-paragraph\">Evergreen resolves the right people to notify through a fallback chain:<\/p>\n<ol class=\"wp-block-list\">\n<li>If a repo sets an <code>evergreen_repo_owner<\/code> custom property (a <code>@user<\/code> or <code>@org\/team<\/code>), use it.<\/li>\n<li>Otherwise, look at recent issue and commit authors and cc the ones who are on the access review team, i.e., the Hubbers actually working in that repo.<\/li>\n<\/ol>\n<p class=\"wp-block-paragraph\">The result: the right owners get pinged automatically, without anyone maintaining a spreadsheet of \u201cwho owns what.\u201d<\/p>\n<h3 class=\"wp-block-heading\">Fail secure, and fail <em>closed<\/em><\/h3>\n<p class=\"wp-block-paragraph\">When a review deadline passes with no response, cleanup revokes every collaborator in the snapshot. We were careful about a subtle edge case: what if some removals succeed while others hit a transient API error? Marking the review \u201cdone\u201d while a removal is still outstanding would be a quiet inconsistency, and we didn\u2019t want that.<\/p>\n<p class=\"wp-block-paragraph\">So Evergreen fails <strong>closed<\/strong>. If any removal fails, the review stays pending and the issue stays open, and the next run retries the stragglers. It only records \u201crevoked\u201d once every removal actually succeeded. And whatever gets removed is written into the issue, so restoring access is a matter of re\u2011adding the handles it listed.<\/p>\n<h2 class=\"wp-block-heading\">The user experience: automation that feels considerate<\/h2>\n<p class=\"wp-block-paragraph\">Automated housekeeping has a reputation for being the office smoke alarm: loud, easy to ignore, and usually going off while you\u2019re cooking something perfectly fine. We worked hard to make Evergreen feel like the opposite:<\/p>\n<ul class=\"wp-block-list\">\n<li><strong>It comes to you.<\/strong> The work happens as an issue in <em>your<\/em> repo.<\/li>\n<li><strong>It tags the right people.<\/strong> No \u201cwho owns this?\u201d archaeology.<\/li>\n<li><strong>It\u2019s crystal clear about the ask.<\/strong> Every issue states what to do, the exact deadline, and precisely what happens if you ignore it.<\/li>\n<li><strong>It nudges before it acts.<\/strong> A reminder comment lands before the deadline: no surprise revocations.<\/li>\n<li><strong>It has an escape hatch.<\/strong>\u00a0Org owners (Hubbers) can opt out by setting a custom property (<code>evergreen_exempt_from_access_review<\/code> or <code>evergreen_exempt_from_archive<\/code>).<\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">Rolling it out carefully<\/h2>\n<p class=\"wp-block-paragraph\">Automation that manages access and archives repositories deserves a careful rollout, especially into a large, active org (ours has 1,400+ Hubbers, and many more customers and partners). We wanted the very first run to be a complete non\u2011event. A few practices got us there:<\/p>\n<p class=\"wp-block-paragraph\"><strong>Dry\u2011run first.<\/strong> Evergreen ships with a <code>DRY_RUN<\/code> mode that logs every write it <em>would<\/em> make without touching a single collaborator or repo. We ran it against the real org and read the logs like tea leaves until we trusted the decisions it wanted to make.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Overcommunicate.<\/strong> Before flipping the switch, we broadcast what was coming, when, and what people needed to do, so the first issue anyone saw wasn\u2019t a surprise. We found that when you\u2019re telling a large group the same thing for the seventh time, it\u2019s often when many are still hearing it for the first time.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Watch everything with Splunk.<\/strong> Evergreen emits structured <code>logfmt<\/code> logs to a dedicated Splunk index. That observability is how we caught edge cases in the wild (empty collaborator lists, unusual permissions, repos with issues disabled) and tightened the logic before they mattered.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Respect the platform\u2019s speed limits.<\/strong> When you open issues and post comments across <em>every<\/em> repo in a big org, you slam into GitHub\u2019s <strong>secondary rate limits for content creation<\/strong>: 80 requests\/minute and 500\/hour. While this is a thoughtful preventative control to avoid system abuse, it requires a bit of intention when automating IssueOps at large scale.<\/p>\n<p class=\"wp-block-paragraph\">So Evergreen paces itself <em>by construction<\/em>. It processes repos in batches, spaces out each content\u2011creating call by a couple of seconds, and sleeps between batches to let the rolling window clear, and it only counts the repos that actually create content, so a quiet quarter where nothing\u2019s eligible still runs fast. We opted for proactive avoidance over reactive back\u2011off.<\/p>\n<h2 class=\"wp-block-heading\">Lessons we\u2019re taking with us<\/h2>\n<ul class=\"wp-block-list\">\n<li><strong>Make the safe path the default path.<\/strong> Fail secure, then make responding easy enough that failing secure rarely triggers.<\/li>\n<li><strong>Use the primitives you already have.<\/strong> Issues, webhooks, custom properties, and git history did the work of a bespoke UI, a job queue, and an audit database.<\/li>\n<li><strong>Meet people where they work.<\/strong> An issue in your repo beats a dashboard you\u2019ll never open.<\/li>\n<li><strong>Logs are a revert button.<\/strong> Writing every change into the issue turned \u201coops\u201d into \u201cpaste these three handles into a Copilot session.\u201d<\/li>\n<li><strong>Respect the limits.<\/strong> At org scale, rate limits aren\u2019t an edge case: they\u2019re the design constraint.<\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">Keep your org evergreen<\/h2>\n<p class=\"wp-block-paragraph\">Routine hygiene is the kind of work that\u2019s easy to defer and perfect to automate. The trick isn\u2019t heroic cleanup sprints: it\u2019s making the boring, safe thing happen on a schedule, with a friendly face and an undo button.<\/p>\n<p class=\"wp-block-paragraph\">That\u2019s Evergreen. And because it\u2019s built entirely from native GitHub features, you can stand up the same pattern yourself.<\/p>\n<div class=\"wp-block-group post-content-cta has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/docs.github.com\/apps\/creating-github-apps\">Ready to make repository hygiene routine?<\/a> Start with one recurring access or archival task, then use GitHub Apps, actions, issues, and custom properties to automate the problem away.<\/p>\n<\/div>\n<p>The post <a href=\"https:\/\/github.blog\/engineering\/platform-security\/staying-evergreen-how-we-automated-secure-access-and-repo-hygiene-in-a-shared-org\/\">Staying Evergreen: How we automated secure access and repo hygiene in a shared org<\/a> appeared first on <a href=\"https:\/\/github.blog\/\">The GitHub Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Working with outside collaborators can add more work for a repository owner. You need to confirm who still needs access, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":5286,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[8],"tags":[],"class_list":["post-5285","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-github-engineering"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/5285","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=5285"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/5285\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/5286"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=5285"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=5285"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=5285"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}