Skip to content
159 changes: 159 additions & 0 deletions github/enterprise_app_installation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// 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"`
Login string `json:"login"`
AccessibleRepositoriesURL *string `json:"accessible_repositories_url,omitempty"`
}

// AccessibleRepository represents a repository that can be made accessible to a GitHub app.
type AccessibleRepository struct {
ID int64 `json:"id"`
Name string `json:"name"`
FullName string `json:"full_name"`
}

// InstallAppRequest represents the request to install a GitHub app on an enterprise-owned organization.
type InstallAppRequest 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.
Repositories []string `json:"repositories,omitempty"`
}

// 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 InstallAppRequest) (*Installation, *Response, error) {
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
}
197 changes: 197 additions & 0 deletions github/enterprise_app_installation_test.go
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: int64(1), Login: "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: int64(10), Name: "repo1", FullName: "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","repositories":["r1","r2"]}`+"\n")
fmt.Fprint(w, `{"id":555}`)
})

req := InstallAppRequest{
ClientID: "cid",
RepositorySelection: "selected",
Repositories: []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)
})
}
8 changes: 8 additions & 0 deletions github/github-accessors.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions github/github-accessors_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading