@@ -552,7 +552,8 @@ plugins for a given scope. Each entry is either:
552552
553553Valid plugin names : ` repository` , `labels`, `collaborators`, `teams`,
554554` milestones` , `branches`, `autolinks`, `validator`, `rulesets`, `environments`,
555- ` custom_properties` , `custom_repository_roles`, `variables`, `archive`.
555+ ` custom_properties` , `custom_repository_roles`, `variables`, `archive`,
556+ ` app_installations` .
556557
557558# ### Strip matrix (which source layers are removed before merge)
558559
@@ -661,6 +662,105 @@ additive_plugins:
661662 - collaborators
662663` ` `
663664
665+ # ## App installation management (`app_installations`)
666+
667+ Most safe-settings plugins target a **repository**. The `app_installations`
668+ plugin is different : its target is a **GitHub App installation**. It lets you
669+ declaratively manage *which repositories a GitHub App can access* (the app's
670+ ` repository_selection` ), using the same `org` → `suborg` → `repo` config
671+ hierarchy you already use for repository settings.
672+
673+ This is useful for controlling, as code, which repos apps such as Copilot,
674+ Dependabot, or your own internal apps are installed on across the org.
675+
676+ # ### Prerequisites
677+
678+ - Safe-settings must be installed on the **enterprise** with the **Enterprise
679+ organization installations** permission (see the
680+ [Enterprise organization installations API](https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/organization-installations)).
681+ Managing app installations requires an enterprise-level token; the regular
682+ org installation token is not sufficient. If safe-settings is not installed
683+ on the enterprise with this permission, app installation sync is reported as
684+ an error and skipped.
685+ - The enterprise slug is read from the webhook event payload
686+ (`payload.enterprise.slug`); no extra environment variable is required.
687+
688+ # ### How repository selection is resolved
689+
690+ The config layer where `app_installations` is declared determines which repos
691+ are selected for the app :
692+
693+ | Layer | File | Repos selected for the app |
694+ | --- | --- | --- |
695+ | Org | `settings.yml` | All repos in the org (`repository_selection : all`) |
696+ | Suborg | `suborgs/*.yml` | Repos matching the suborg's targeting (`suborgrepos`, `suborgteams`, `suborgproperties`) |
697+ | Repo | `repos/<repo>.yml` | That specific repo |
698+
699+ > [!important]
700+ > An app configured with `repository_selection: all` at the **org** level takes
701+ > precedence. Suborg/repo-level selections for that same app are ignored, and
702+ > repos are never removed from it by incremental (suborg/repo) changes — it is
703+ > reconciled only by the full (scheduled) sync.
704+
705+ # ### Examples
706+
707+ Org-level `settings.yml` — give an app access to **all** repos in the org :
708+
709+ ` ` ` yaml
710+ app_installations:
711+ - app_slug: my-internal-app
712+ repository_selection: all
713+ ` ` `
714+
715+ Suborg-level `suborgs/backend.yml` — give an app access to the repos targeted
716+ by this suborg (here, all repos with the `Team=backend` custom property) :
717+
718+ ` ` ` yaml
719+ suborgproperties:
720+ - Team: backend
721+ app_installations:
722+ - app_slug: my-internal-app
723+ ` ` `
724+
725+ Repo-level `repos/my-repo.yml` — add this specific repo to the app :
726+
727+ ` ` ` yaml
728+ app_installations:
729+ - app_slug: my-internal-app
730+ ` ` `
731+
732+ Removing an app from a suborg/repo config (or changing the suborg's targeting)
733+ removes the affected repos from that app on the next sync, unless another layer
734+ still selects them.
735+
736+ # ### Sync behavior
737+
738+ - **Incremental (delta) sync** runs when a `suborgs/*.yml` or `repos/*.yml`
739+ file changes. Only the apps affected by the changed file are reconciled : the
740+ previous version of the file is compared with the new one to compute repos to
741+ add (`repository_selection`) and repos to remove (`repository_unselection`).
742+ Removals are applied before additions, so a repo removed by one config and
743+ added by another ends up present.
744+ - **Full sync** runs on the schedule (cron), on manual sync, and when
745+ ` settings.yml` changes. It recomputes the full desired state for every managed
746+ app across all layers and reconciles it against the live installation state.
747+ This is the mechanism that corrects any configuration drift.
748+ - Add/remove operations are automatically batched in chunks of 50 repos (the
749+ API limit).
750+
751+ > [!note]
752+ > Drift on managed apps is reconciled by the **full (cron) sync**, not by
753+ > webhooks. A GitHub App only receives `installation` repository events for its
754+ > *own* installation, so safe-settings cannot detect — via webhooks — when a
755+ > human changes another app's repository access. Keep the scheduled sync enabled
756+ > for timely drift correction.
757+
758+ # ### Disabling and additive mode
759+
760+ ` app_installations` honors both [`disable_plugins`](#disabling-plugins-disable_plugins)
761+ and [`additive_plugins`](#additive-plugins-additive_plugins). In additive mode
762+ the plugin only **adds** repos to installations and never removes them.
763+
664764# ## The Settings Files
665765
666766The settings files can be used to set the policies at the `org`, `suborg` or `repo` level.
@@ -680,6 +780,7 @@ The following can be configured:
680780- ` Repository name validation` using regex pattern
681781- ` Rulesets`
682782- ` Environments` - wait timer, required reviewers, prevent self review, protected branches deployment branch policy, custom deployment branch policy, variables, deployment protection rules
783+ - ` App installations` - which repositories a GitHub App installation can access (see [App installation management](#app-installation-management-app_installations))
683784
684785See [`docs/sample-settings/settings.yml`](docs/sample-settings/settings.yml) for a sample settings file.
685786
0 commit comments