-
Notifications
You must be signed in to change notification settings - Fork 212
feat: add app installation plugin for managing GitHub App repo access #1012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
decyjphr
wants to merge
9
commits into
yadhav/fix-recent-issues
Choose a base branch
from
decyjphr-app-installation-plugin
base: yadhav/fix-recent-issues
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
5894eae
feat: add app installation plugin for managing GitHub App repo access
decyjphr a51ab87
fix: use enterprise installation token for app installation API calls
decyjphr 8fee0dd
perf: cache enterprise installation ID to avoid repeated lookups
decyjphr d0be177
Wire syncAppInstallations into syncSelectedRepos for delta processing
decyjphr 24fb684
Compute repository_unselection by diffing previous vs current configs
decyjphr 4b2cec7
Fix delta bugs: org-all precedence + repo config path; update schema
decyjphr b731d54
Address app installation review: remove bad drift handler, fix orderi…
decyjphr 9a25bdc
Skip redundant app installation churn for unchanged delta
decyjphr eacef7f
Use correct Enterprise Org Installations API (names, toggle, PATCH ad…
decyjphr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| const BATCH_SIZE = 50 | ||
|
|
||
| /** | ||
| * AppOctokitClient wraps an Octokit client authenticated as the GitHub App | ||
| * (JWT) and provides methods for managing app installation repository access | ||
| * via the Enterprise Organization Installations API. | ||
| * | ||
| * Prerequisites: | ||
| * - safe-settings must be installed on the enterprise with | ||
| * "Enterprise organization installations" permission. | ||
| * - The enterprise slug is obtained from the webhook event payload | ||
| * (payload.enterprise.slug). | ||
| * | ||
| * @param {object} options | ||
| * @param {object} options.github - Octokit client authenticated as the app (via robot.auth()) | ||
| * @param {string} options.enterpriseSlug - Enterprise slug from webhook payload | ||
| * @param {object} options.log - Logger instance | ||
| */ | ||
| class AppOctokitClient { | ||
| constructor ({ github, enterpriseSlug, log }) { | ||
| this.github = github | ||
| this.enterpriseSlug = enterpriseSlug | ||
| this.log = log | ||
| } | ||
|
|
||
| /** | ||
| * List all app installations in the enterprise for a given org. | ||
| * Returns array of installation objects with { id, app_slug, app_id, ... } | ||
| * | ||
| * @param {string} org - Organization login name | ||
| * @returns {Promise<Array>} List of installations | ||
| */ | ||
| async listOrgInstallations (org) { | ||
| try { | ||
| const options = this.github.request.endpoint.merge( | ||
| 'GET /enterprises/{enterprise}/apps/installations', | ||
| { | ||
| enterprise: this.enterpriseSlug, | ||
| headers: { 'X-GitHub-Api-Version': '2026-03-10' } | ||
| } | ||
| ) | ||
| const installations = await this.github.paginate(options) | ||
| // Filter to installations for the specified org | ||
| return installations.filter(i => | ||
| i.account && i.account.login === org | ||
| ) | ||
| } catch (e) { | ||
| if (e.status === 403 || e.status === 404) { | ||
| throw new Error( | ||
| `Cannot access enterprise installations API. Ensure safe-settings is installed on the enterprise '${this.enterpriseSlug}' with 'Enterprise organization installations' permission. Error: ${e.message}` | ||
| ) | ||
| } | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * List repositories accessible to an app installation. | ||
| * | ||
| * @param {number} installationId - The installation ID | ||
| * @returns {Promise<Array>} List of repository objects | ||
| */ | ||
| async listInstallationRepos (installationId) { | ||
| try { | ||
| const options = this.github.request.endpoint.merge( | ||
| 'GET /enterprises/{enterprise}/apps/installations/{installation_id}/repositories', | ||
| { | ||
| enterprise: this.enterpriseSlug, | ||
| installation_id: installationId, | ||
| headers: { 'X-GitHub-Api-Version': '2026-03-10' } | ||
| } | ||
| ) | ||
| return this.github.paginate(options) | ||
| } catch (e) { | ||
| this.log.error(`Error listing repos for installation ${installationId}: ${e.message}`) | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Grant repository access to an app installation. | ||
| * Automatically batches into chunks of 50 (API limit). | ||
| * | ||
| * @param {number} installationId - The installation ID | ||
| * @param {number[]} repositoryIds - Array of repository IDs to add | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async addReposToInstallation (installationId, repositoryIds) { | ||
| if (!repositoryIds || repositoryIds.length === 0) return | ||
|
|
||
| const batches = this._chunk(repositoryIds, BATCH_SIZE) | ||
| for (const batch of batches) { | ||
| this.log.debug(`Adding ${batch.length} repos to installation ${installationId}`) | ||
| await this.github.request( | ||
| 'POST /enterprises/{enterprise}/apps/installations/{installation_id}/repositories', | ||
| { | ||
| enterprise: this.enterpriseSlug, | ||
| installation_id: installationId, | ||
| repository_ids: batch, | ||
| headers: { 'X-GitHub-Api-Version': '2026-03-10' } | ||
| } | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Remove repository access from an app installation. | ||
| * Automatically batches into chunks of 50 (API limit). | ||
| * | ||
| * @param {number} installationId - The installation ID | ||
| * @param {number[]} repositoryIds - Array of repository IDs to remove | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async removeReposFromInstallation (installationId, repositoryIds) { | ||
| if (!repositoryIds || repositoryIds.length === 0) return | ||
|
|
||
| const batches = this._chunk(repositoryIds, BATCH_SIZE) | ||
| for (const batch of batches) { | ||
| this.log.debug(`Removing ${batch.length} repos from installation ${installationId}`) | ||
| await this.github.request( | ||
| 'DELETE /enterprises/{enterprise}/apps/installations/{installation_id}/repositories', | ||
| { | ||
| enterprise: this.enterpriseSlug, | ||
| installation_id: installationId, | ||
| repository_ids: batch, | ||
| headers: { 'X-GitHub-Api-Version': '2026-03-10' } | ||
| } | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Split an array into chunks of the given size. | ||
| * @private | ||
| */ | ||
| _chunk (array, size) { | ||
| const chunks = [] | ||
| for (let i = 0; i < array.length; i += size) { | ||
| chunks.push(array.slice(i, i + size)) | ||
| } | ||
| return chunks | ||
| } | ||
| } | ||
|
|
||
| module.exports = AppOctokitClient |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should cache the enterpriseInstallation ID so that next time we can just reuse it.