-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat: Add support for Enterprise GitHub App Installation APIs #3830
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
Merged
gmlewis
merged 10 commits into
google:master
from
Not-Dhananjay-Mishra:issue-3829-part-1
Nov 18, 2025
Merged
Changes from 7 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
110c09c
add five endpoints for managing organization GitHub app installations
Not-Dhananjay-Mishra 6dc9163
fix test
Not-Dhananjay-Mishra 838cf9b
change method names
Not-Dhananjay-Mishra 17147f6
fix test
Not-Dhananjay-Mishra 12b34c6
change method name
Not-Dhananjay-Mishra a3b2fc2
change ListOrganizationAccessibleAppRepositories to ListOrganizationA…
Not-Dhananjay-Mishra 45be8c3
method name change
Not-Dhananjay-Mishra 25c6413
added AccessibleRepositoriesURL to InstallableOrganization and change…
Not-Dhananjay-Mishra 226694b
change *string to string
Not-Dhananjay-Mishra 884c2d1
fix *string and omitempty issue
Not-Dhananjay-Mishra 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| // Copyright 2025 The go-github AUTHORS. All rights reserved. | ||
| // | ||
| // Use of this source code is governed by a BSD-style | ||
| // license that can be found in the LICENSE file. | ||
|
|
||
| package github | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| ) | ||
|
|
||
| // InstallableOrganization represents an organization in an enterprise in which a GitHub app can be installed. | ||
| type InstallableOrganization struct { | ||
| ID *int64 `json:"id,omitempty"` | ||
| Login *string `json:"login,omitempty"` | ||
| } | ||
|
|
||
| // AccessibleRepository represents a repository that can be made accessible to a GitHub app. | ||
| type AccessibleRepository struct { | ||
| ID *int64 `json:"id,omitempty"` | ||
| Name *string `json:"name,omitempty"` | ||
| FullName *string `json:"full_name,omitempty"` | ||
Not-Dhananjay-Mishra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // AppInstallationRequest represents the request to install a GitHub app on an enterprise-owned organization. | ||
| type AppInstallationRequest struct { | ||
| // The Client ID of the GitHub App to install. | ||
| ClientID string `json:"client_id"` | ||
| // The selection of repositories that the GitHub app can access. | ||
| // Can be one of: all, selected, none | ||
| RepositorySelection string `json:"repository_selection"` | ||
| // A list of repository names that the GitHub App can access, if the repository_selection is set to selected. | ||
| Repository []string `json:"repository,omitempty"` | ||
Not-Dhananjay-Mishra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // ListAppInstallableOrganizations lists the organizations in an enterprise that are installable for an app. | ||
| // | ||
| // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#get-enterprise-owned-organizations-that-can-have-github-apps-installed | ||
| // | ||
| //meta:operation GET /enterprises/{enterprise}/apps/installable_organizations | ||
| func (s *EnterpriseService) ListAppInstallableOrganizations(ctx context.Context, enterprise string, opts *ListOptions) ([]*InstallableOrganization, *Response, error) { | ||
| u := fmt.Sprintf("enterprises/%v/apps/installable_organizations", enterprise) | ||
|
|
||
| u, err := addOptions(u, opts) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| req, err := s.client.NewRequest("GET", u, nil) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| var orgs []*InstallableOrganization | ||
| resp, err := s.client.Do(ctx, req, &orgs) | ||
| if err != nil { | ||
| return nil, resp, err | ||
| } | ||
|
|
||
| return orgs, resp, nil | ||
| } | ||
|
|
||
| // ListAppAccessibleOrganizationRepositories lists the repositories accessible to an app in an enterprise-owned organization. | ||
| // | ||
| // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#get-repositories-belonging-to-an-enterprise-owned-organization | ||
| // | ||
| //meta:operation GET /enterprises/{enterprise}/apps/installable_organizations/{org}/accessible_repositories | ||
| func (s *EnterpriseService) ListAppAccessibleOrganizationRepositories(ctx context.Context, enterprise, org string, opts *ListOptions) ([]*AccessibleRepository, *Response, error) { | ||
| u := fmt.Sprintf("enterprises/%v/apps/installable_organizations/%v/accessible_repositories", enterprise, org) | ||
|
|
||
| u, err := addOptions(u, opts) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| req, err := s.client.NewRequest("GET", u, nil) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| var repos []*AccessibleRepository | ||
| resp, err := s.client.Do(ctx, req, &repos) | ||
| if err != nil { | ||
| return nil, resp, err | ||
| } | ||
|
|
||
| return repos, resp, nil | ||
| } | ||
|
|
||
| // ListAppInstallations lists the GitHub app installations associated with the given enterprise-owned organization. | ||
| // | ||
| // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#list-github-apps-installed-on-an-enterprise-owned-organization | ||
| // | ||
| //meta:operation GET /enterprises/{enterprise}/apps/organizations/{org}/installations | ||
| func (s *EnterpriseService) ListAppInstallations(ctx context.Context, enterprise, org string, opts *ListOptions) ([]*Installation, *Response, error) { | ||
| u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations", enterprise, org) | ||
|
|
||
| u, err := addOptions(u, opts) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| req, err := s.client.NewRequest("GET", u, nil) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| var installation []*Installation | ||
| resp, err := s.client.Do(ctx, req, &installation) | ||
| if err != nil { | ||
| return nil, resp, err | ||
| } | ||
|
|
||
| return installation, resp, nil | ||
| } | ||
|
|
||
| // InstallApp installs any valid GitHub app on the specified organization owned by the enterprise. | ||
| // If the app is already installed on the organization, and is suspended, it will be unsuspended. If the app has a pending installation request, they will all be approved. | ||
| // | ||
| // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#install-a-github-app-on-an-enterprise-owned-organization | ||
| // | ||
| //meta:operation POST /enterprises/{enterprise}/apps/organizations/{org}/installations | ||
| func (s *EnterpriseService) InstallApp(ctx context.Context, enterprise, org string, request AppInstallationRequest) (*Installation, *Response, error) { | ||
Not-Dhananjay-Mishra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations", enterprise, org) | ||
| req, err := s.client.NewRequest("POST", u, request) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| var installation *Installation | ||
| resp, err := s.client.Do(ctx, req, &installation) | ||
| if err != nil { | ||
| return nil, resp, err | ||
| } | ||
|
|
||
| return installation, resp, nil | ||
| } | ||
|
|
||
| // UninstallApp uninstalls a GitHub app from an organization. Any app installed on the organization can be removed. | ||
| // | ||
| // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#uninstall-a-github-app-from-an-enterprise-owned-organization | ||
| // | ||
| //meta:operation DELETE /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id} | ||
| func (s *EnterpriseService) UninstallApp(ctx context.Context, enterprise, org string, installationID int64) (*Response, error) { | ||
| u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations/%v", enterprise, org, installationID) | ||
| req, err := s.client.NewRequest("DELETE", u, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| resp, err := s.client.Do(ctx, req, nil) | ||
| if err != nil { | ||
| return resp, err | ||
| } | ||
|
|
||
| return resp, nil | ||
| } | ||
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,197 @@ | ||
| // Copyright 2025 The go-github AUTHORS. All rights reserved. | ||
| // | ||
| // Use of this source code is governed by a BSD-style | ||
| // license that can be found in the LICENSE file. | ||
|
|
||
| package github | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/http" | ||
| "testing" | ||
|
|
||
| "github.com/google/go-cmp/cmp" | ||
| ) | ||
|
|
||
| func TestEnterpriseService_ListAppInstallableOrganizations(t *testing.T) { | ||
| t.Parallel() | ||
| client, mux, _ := setup(t) | ||
|
|
||
| mux.HandleFunc("/enterprises/e/apps/installable_organizations", func(w http.ResponseWriter, r *http.Request) { | ||
| testMethod(t, r, "GET") | ||
| fmt.Fprint(w, `[{"id":1, "login":"org1"}]`) | ||
| }) | ||
|
|
||
| ctx := t.Context() | ||
| opts := &ListOptions{Page: 1, PerPage: 10} | ||
| got, _, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "e", opts) | ||
| if err != nil { | ||
| t.Fatalf("Enterprise.ListAppInstallableOrganizations returned error: %v", err) | ||
| } | ||
|
|
||
| want := []*InstallableOrganization{ | ||
| {ID: Ptr(int64(1)), Login: Ptr("org1")}, | ||
| } | ||
|
|
||
| if !cmp.Equal(got, want) { | ||
| t.Errorf("Enterprise.ListAppInstallableOrganizations = %+v, want %+v", got, want) | ||
| } | ||
|
|
||
| const methodName = "ListAppInstallableOrganizations" | ||
| testBadOptions(t, methodName, func() error { | ||
| _, _, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "\n", opts) | ||
| return err | ||
| }) | ||
| testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { | ||
| got, resp, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "e", nil) | ||
| if got != nil { | ||
| t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) | ||
| } | ||
| return resp, err | ||
| }) | ||
| } | ||
|
|
||
| func TestEnterpriseService_ListAppAccessibleOrganizationRepositories(t *testing.T) { | ||
| t.Parallel() | ||
| client, mux, _ := setup(t) | ||
|
|
||
| mux.HandleFunc("/enterprises/e/apps/installable_organizations/org1/accessible_repositories", func(w http.ResponseWriter, r *http.Request) { | ||
| testMethod(t, r, "GET") | ||
| fmt.Fprint(w, `[{"id":10, "name":"repo1", "full_name":"org1/repo1"}]`) | ||
| }) | ||
|
|
||
| opts := &ListOptions{Page: 2, PerPage: 2} | ||
| ctx := t.Context() | ||
| repos, _, err := client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "e", "org1", opts) | ||
| if err != nil { | ||
| t.Errorf("Enterprise.ListAppAccessibleOrganizationRepositories returned error: %v", err) | ||
| } | ||
|
|
||
| want := []*AccessibleRepository{ | ||
| {ID: Ptr(int64(10)), Name: Ptr("repo1"), FullName: Ptr("org1/repo1")}, | ||
| } | ||
|
|
||
| if !cmp.Equal(repos, want) { | ||
| t.Errorf("Enterprise.ListAppAccessibleOrganizationRepositories returned %+v, want %+v", repos, want) | ||
| } | ||
|
|
||
| const methodName = "ListAppAccessibleOrganizationRepositories" | ||
|
|
||
| testBadOptions(t, methodName, func() (err error) { | ||
| _, _, err = client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "\n", "org1", opts) | ||
| return err | ||
| }) | ||
|
|
||
| testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { | ||
| got, resp, err := client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "e", "org1", nil) | ||
| if got != nil { | ||
| t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) | ||
| } | ||
| return resp, err | ||
| }) | ||
| } | ||
|
|
||
| func TestEnterpriseService_ListAppInstallations(t *testing.T) { | ||
| t.Parallel() | ||
| client, mux, _ := setup(t) | ||
|
|
||
| mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations", func(w http.ResponseWriter, r *http.Request) { | ||
| testMethod(t, r, "GET") | ||
| testFormValues(t, r, values{"per_page": "2", "page": "2"}) | ||
| fmt.Fprint(w, `[{"id":99}]`) | ||
| }) | ||
|
|
||
| opts := &ListOptions{Page: 2, PerPage: 2} | ||
| ctx := t.Context() | ||
| installations, _, err := client.Enterprise.ListAppInstallations(ctx, "e", "org1", opts) | ||
| if err != nil { | ||
| t.Errorf("ListAppInstallations returned error: %v", err) | ||
| } | ||
| want := []*Installation{ | ||
| {ID: Ptr(int64(99))}, | ||
| } | ||
|
|
||
| if !cmp.Equal(installations, want) { | ||
| t.Errorf("ListAppInstallations returned %+v, want %+v", installations, want) | ||
| } | ||
|
|
||
| const methodName = "ListAppInstallations" | ||
|
|
||
| testBadOptions(t, methodName, func() (err error) { | ||
| _, _, err = client.Enterprise.ListAppInstallations(ctx, "\n", "org1", &ListOptions{}) | ||
| return err | ||
| }) | ||
|
|
||
| testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { | ||
| got, resp, err := client.Enterprise.ListAppInstallations(ctx, "e", "org1", nil) | ||
| if got != nil { | ||
| t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) | ||
| } | ||
| return resp, err | ||
| }) | ||
| } | ||
|
|
||
| func TestEnterpriseService_InstallApp(t *testing.T) { | ||
| t.Parallel() | ||
| client, mux, _ := setup(t) | ||
|
|
||
| mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations", func(w http.ResponseWriter, r *http.Request) { | ||
| testMethod(t, r, "POST") | ||
| testBody(t, r, `{"client_id":"cid","repository_selection":"selected","repository":["r1","r2"]}`+"\n") | ||
| fmt.Fprint(w, `{"id":555}`) | ||
| }) | ||
|
|
||
| req := AppInstallationRequest{ | ||
| ClientID: "cid", | ||
| RepositorySelection: "selected", | ||
| Repository: []string{"r1", "r2"}, | ||
| } | ||
|
|
||
| ctx := t.Context() | ||
| installation, _, err := client.Enterprise.InstallApp(ctx, "e", "org1", req) | ||
| if err != nil { | ||
| t.Errorf("InstallApp returned error: %v", err) | ||
| } | ||
|
|
||
| want := &Installation{ID: Ptr(int64(555))} | ||
|
|
||
| if !cmp.Equal(installation, want) { | ||
| t.Errorf("InstallApp returned %+v, want %+v", installation, want) | ||
| } | ||
|
|
||
| const methodName = "InstallApp" | ||
|
|
||
| testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { | ||
| got, resp, err := client.Enterprise.InstallApp(ctx, "e", "org1", req) | ||
| if got != nil { | ||
| t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) | ||
| } | ||
| return resp, err | ||
| }) | ||
| } | ||
|
|
||
| func TestEnterpriseService_UninstallApp(t *testing.T) { | ||
| t.Parallel() | ||
| client, mux, _ := setup(t) | ||
|
|
||
| mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations/123", func(w http.ResponseWriter, r *http.Request) { | ||
| testMethod(t, r, "DELETE") | ||
| w.WriteHeader(http.StatusNoContent) | ||
| }) | ||
|
|
||
| ctx := t.Context() | ||
| resp, err := client.Enterprise.UninstallApp(ctx, "e", "org1", 123) | ||
| if err != nil { | ||
| t.Errorf("UninstallApp returned error: %v", err) | ||
| } | ||
|
|
||
| if resp.StatusCode != http.StatusNoContent { | ||
| t.Errorf("UninstallApp returned status %v, want %v", resp.StatusCode, http.StatusNoContent) | ||
| } | ||
|
|
||
| const methodName = "UninstallApp" | ||
|
|
||
| testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { | ||
| return client.Enterprise.UninstallApp(ctx, "e", "org1", 123) | ||
| }) | ||
| } |
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.
Uh oh!
There was an error while loading. Please reload this page.