Skip to content

Commit ea7dd81

Browse files
feat: Add support for Enterprise GitHub App Installation APIs (#3830)
1 parent 45fa28c commit ea7dd81

File tree

4 files changed

+375
-0
lines changed

4 files changed

+375
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2025 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
package github
7+
8+
import (
9+
"context"
10+
"fmt"
11+
)
12+
13+
// InstallableOrganization represents an organization in an enterprise in which a GitHub app can be installed.
14+
type InstallableOrganization struct {
15+
ID int64 `json:"id"`
16+
Login string `json:"login"`
17+
AccessibleRepositoriesURL *string `json:"accessible_repositories_url,omitempty"`
18+
}
19+
20+
// AccessibleRepository represents a repository that can be made accessible to a GitHub app.
21+
type AccessibleRepository struct {
22+
ID int64 `json:"id"`
23+
Name string `json:"name"`
24+
FullName string `json:"full_name"`
25+
}
26+
27+
// InstallAppRequest represents the request to install a GitHub app on an enterprise-owned organization.
28+
type InstallAppRequest struct {
29+
// The Client ID of the GitHub App to install.
30+
ClientID string `json:"client_id"`
31+
// The selection of repositories that the GitHub app can access.
32+
// Can be one of: all, selected, none
33+
RepositorySelection string `json:"repository_selection"`
34+
// A list of repository names that the GitHub App can access, if the repository_selection is set to selected.
35+
Repositories []string `json:"repositories,omitempty"`
36+
}
37+
38+
// ListAppInstallableOrganizations lists the organizations in an enterprise that are installable for an app.
39+
//
40+
// 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
41+
//
42+
//meta:operation GET /enterprises/{enterprise}/apps/installable_organizations
43+
func (s *EnterpriseService) ListAppInstallableOrganizations(ctx context.Context, enterprise string, opts *ListOptions) ([]*InstallableOrganization, *Response, error) {
44+
u := fmt.Sprintf("enterprises/%v/apps/installable_organizations", enterprise)
45+
46+
u, err := addOptions(u, opts)
47+
if err != nil {
48+
return nil, nil, err
49+
}
50+
51+
req, err := s.client.NewRequest("GET", u, nil)
52+
if err != nil {
53+
return nil, nil, err
54+
}
55+
56+
var orgs []*InstallableOrganization
57+
resp, err := s.client.Do(ctx, req, &orgs)
58+
if err != nil {
59+
return nil, resp, err
60+
}
61+
62+
return orgs, resp, nil
63+
}
64+
65+
// ListAppAccessibleOrganizationRepositories lists the repositories accessible to an app in an enterprise-owned organization.
66+
//
67+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#get-repositories-belonging-to-an-enterprise-owned-organization
68+
//
69+
//meta:operation GET /enterprises/{enterprise}/apps/installable_organizations/{org}/accessible_repositories
70+
func (s *EnterpriseService) ListAppAccessibleOrganizationRepositories(ctx context.Context, enterprise, org string, opts *ListOptions) ([]*AccessibleRepository, *Response, error) {
71+
u := fmt.Sprintf("enterprises/%v/apps/installable_organizations/%v/accessible_repositories", enterprise, org)
72+
73+
u, err := addOptions(u, opts)
74+
if err != nil {
75+
return nil, nil, err
76+
}
77+
78+
req, err := s.client.NewRequest("GET", u, nil)
79+
if err != nil {
80+
return nil, nil, err
81+
}
82+
83+
var repos []*AccessibleRepository
84+
resp, err := s.client.Do(ctx, req, &repos)
85+
if err != nil {
86+
return nil, resp, err
87+
}
88+
89+
return repos, resp, nil
90+
}
91+
92+
// ListAppInstallations lists the GitHub app installations associated with the given enterprise-owned organization.
93+
//
94+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#list-github-apps-installed-on-an-enterprise-owned-organization
95+
//
96+
//meta:operation GET /enterprises/{enterprise}/apps/organizations/{org}/installations
97+
func (s *EnterpriseService) ListAppInstallations(ctx context.Context, enterprise, org string, opts *ListOptions) ([]*Installation, *Response, error) {
98+
u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations", enterprise, org)
99+
100+
u, err := addOptions(u, opts)
101+
if err != nil {
102+
return nil, nil, err
103+
}
104+
105+
req, err := s.client.NewRequest("GET", u, nil)
106+
if err != nil {
107+
return nil, nil, err
108+
}
109+
110+
var installation []*Installation
111+
resp, err := s.client.Do(ctx, req, &installation)
112+
if err != nil {
113+
return nil, resp, err
114+
}
115+
116+
return installation, resp, nil
117+
}
118+
119+
// InstallApp installs any valid GitHub app on the specified organization owned by the enterprise.
120+
// 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.
121+
//
122+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#install-a-github-app-on-an-enterprise-owned-organization
123+
//
124+
//meta:operation POST /enterprises/{enterprise}/apps/organizations/{org}/installations
125+
func (s *EnterpriseService) InstallApp(ctx context.Context, enterprise, org string, request InstallAppRequest) (*Installation, *Response, error) {
126+
u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations", enterprise, org)
127+
req, err := s.client.NewRequest("POST", u, request)
128+
if err != nil {
129+
return nil, nil, err
130+
}
131+
132+
var installation *Installation
133+
resp, err := s.client.Do(ctx, req, &installation)
134+
if err != nil {
135+
return nil, resp, err
136+
}
137+
138+
return installation, resp, nil
139+
}
140+
141+
// UninstallApp uninstalls a GitHub app from an organization. Any app installed on the organization can be removed.
142+
//
143+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#uninstall-a-github-app-from-an-enterprise-owned-organization
144+
//
145+
//meta:operation DELETE /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}
146+
func (s *EnterpriseService) UninstallApp(ctx context.Context, enterprise, org string, installationID int64) (*Response, error) {
147+
u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations/%v", enterprise, org, installationID)
148+
req, err := s.client.NewRequest("DELETE", u, nil)
149+
if err != nil {
150+
return nil, err
151+
}
152+
153+
resp, err := s.client.Do(ctx, req, nil)
154+
if err != nil {
155+
return resp, err
156+
}
157+
158+
return resp, nil
159+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright 2025 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
package github
7+
8+
import (
9+
"fmt"
10+
"net/http"
11+
"testing"
12+
13+
"github.com/google/go-cmp/cmp"
14+
)
15+
16+
func TestEnterpriseService_ListAppInstallableOrganizations(t *testing.T) {
17+
t.Parallel()
18+
client, mux, _ := setup(t)
19+
20+
mux.HandleFunc("/enterprises/e/apps/installable_organizations", func(w http.ResponseWriter, r *http.Request) {
21+
testMethod(t, r, "GET")
22+
fmt.Fprint(w, `[{"id":1, "login":"org1"}]`)
23+
})
24+
25+
ctx := t.Context()
26+
opts := &ListOptions{Page: 1, PerPage: 10}
27+
got, _, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "e", opts)
28+
if err != nil {
29+
t.Fatalf("Enterprise.ListAppInstallableOrganizations returned error: %v", err)
30+
}
31+
32+
want := []*InstallableOrganization{
33+
{ID: int64(1), Login: "org1"},
34+
}
35+
36+
if !cmp.Equal(got, want) {
37+
t.Errorf("Enterprise.ListAppInstallableOrganizations = %+v, want %+v", got, want)
38+
}
39+
40+
const methodName = "ListAppInstallableOrganizations"
41+
testBadOptions(t, methodName, func() error {
42+
_, _, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "\n", opts)
43+
return err
44+
})
45+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
46+
got, resp, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "e", nil)
47+
if got != nil {
48+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
49+
}
50+
return resp, err
51+
})
52+
}
53+
54+
func TestEnterpriseService_ListAppAccessibleOrganizationRepositories(t *testing.T) {
55+
t.Parallel()
56+
client, mux, _ := setup(t)
57+
58+
mux.HandleFunc("/enterprises/e/apps/installable_organizations/org1/accessible_repositories", func(w http.ResponseWriter, r *http.Request) {
59+
testMethod(t, r, "GET")
60+
fmt.Fprint(w, `[{"id":10, "name":"repo1", "full_name":"org1/repo1"}]`)
61+
})
62+
63+
opts := &ListOptions{Page: 2, PerPage: 2}
64+
ctx := t.Context()
65+
repos, _, err := client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "e", "org1", opts)
66+
if err != nil {
67+
t.Errorf("Enterprise.ListAppAccessibleOrganizationRepositories returned error: %v", err)
68+
}
69+
70+
want := []*AccessibleRepository{
71+
{ID: int64(10), Name: "repo1", FullName: "org1/repo1"},
72+
}
73+
74+
if !cmp.Equal(repos, want) {
75+
t.Errorf("Enterprise.ListAppAccessibleOrganizationRepositories returned %+v, want %+v", repos, want)
76+
}
77+
78+
const methodName = "ListAppAccessibleOrganizationRepositories"
79+
80+
testBadOptions(t, methodName, func() (err error) {
81+
_, _, err = client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "\n", "org1", opts)
82+
return err
83+
})
84+
85+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
86+
got, resp, err := client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "e", "org1", nil)
87+
if got != nil {
88+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
89+
}
90+
return resp, err
91+
})
92+
}
93+
94+
func TestEnterpriseService_ListAppInstallations(t *testing.T) {
95+
t.Parallel()
96+
client, mux, _ := setup(t)
97+
98+
mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations", func(w http.ResponseWriter, r *http.Request) {
99+
testMethod(t, r, "GET")
100+
testFormValues(t, r, values{"per_page": "2", "page": "2"})
101+
fmt.Fprint(w, `[{"id":99}]`)
102+
})
103+
104+
opts := &ListOptions{Page: 2, PerPage: 2}
105+
ctx := t.Context()
106+
installations, _, err := client.Enterprise.ListAppInstallations(ctx, "e", "org1", opts)
107+
if err != nil {
108+
t.Errorf("ListAppInstallations returned error: %v", err)
109+
}
110+
want := []*Installation{
111+
{ID: Ptr(int64(99))},
112+
}
113+
114+
if !cmp.Equal(installations, want) {
115+
t.Errorf("ListAppInstallations returned %+v, want %+v", installations, want)
116+
}
117+
118+
const methodName = "ListAppInstallations"
119+
120+
testBadOptions(t, methodName, func() (err error) {
121+
_, _, err = client.Enterprise.ListAppInstallations(ctx, "\n", "org1", &ListOptions{})
122+
return err
123+
})
124+
125+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
126+
got, resp, err := client.Enterprise.ListAppInstallations(ctx, "e", "org1", nil)
127+
if got != nil {
128+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
129+
}
130+
return resp, err
131+
})
132+
}
133+
134+
func TestEnterpriseService_InstallApp(t *testing.T) {
135+
t.Parallel()
136+
client, mux, _ := setup(t)
137+
138+
mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations", func(w http.ResponseWriter, r *http.Request) {
139+
testMethod(t, r, "POST")
140+
testBody(t, r, `{"client_id":"cid","repository_selection":"selected","repositories":["r1","r2"]}`+"\n")
141+
fmt.Fprint(w, `{"id":555}`)
142+
})
143+
144+
req := InstallAppRequest{
145+
ClientID: "cid",
146+
RepositorySelection: "selected",
147+
Repositories: []string{"r1", "r2"},
148+
}
149+
150+
ctx := t.Context()
151+
installation, _, err := client.Enterprise.InstallApp(ctx, "e", "org1", req)
152+
if err != nil {
153+
t.Errorf("InstallApp returned error: %v", err)
154+
}
155+
156+
want := &Installation{ID: Ptr(int64(555))}
157+
158+
if !cmp.Equal(installation, want) {
159+
t.Errorf("InstallApp returned %+v, want %+v", installation, want)
160+
}
161+
162+
const methodName = "InstallApp"
163+
164+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
165+
got, resp, err := client.Enterprise.InstallApp(ctx, "e", "org1", req)
166+
if got != nil {
167+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
168+
}
169+
return resp, err
170+
})
171+
}
172+
173+
func TestEnterpriseService_UninstallApp(t *testing.T) {
174+
t.Parallel()
175+
client, mux, _ := setup(t)
176+
177+
mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations/123", func(w http.ResponseWriter, r *http.Request) {
178+
testMethod(t, r, "DELETE")
179+
w.WriteHeader(http.StatusNoContent)
180+
})
181+
182+
ctx := t.Context()
183+
resp, err := client.Enterprise.UninstallApp(ctx, "e", "org1", 123)
184+
if err != nil {
185+
t.Errorf("UninstallApp returned error: %v", err)
186+
}
187+
188+
if resp.StatusCode != http.StatusNoContent {
189+
t.Errorf("UninstallApp returned status %v, want %v", resp.StatusCode, http.StatusNoContent)
190+
}
191+
192+
const methodName = "UninstallApp"
193+
194+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
195+
return client.Enterprise.UninstallApp(ctx, "e", "org1", 123)
196+
})
197+
}

github/github-accessors.go

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

github/github-accessors_test.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)