diff --git a/github/enterprise_licenses.go b/github/enterprise_licenses.go new file mode 100644 index 00000000000..f00a63a3f37 --- /dev/null +++ b/github/enterprise_licenses.go @@ -0,0 +1,139 @@ +// 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" +) + +// EnterpriseLicensedUsers represents users that hold an enterprise license. +type EnterpriseLicensedUsers struct { + GithubComLogin string `json:"github_com_login"` + GithubComName string `json:"github_com_name"` + EnterpriseServerUserIDs []string `json:"enterprise_server_user_ids"` + GithubComUser bool `json:"github_com_user"` + EnterpriseServerUser bool `json:"enterprise_server_user"` + VisualStudioSubscriptionUser bool `json:"visual_studio_subscription_user"` + LicenseType string `json:"license_type"` + GithubComProfile string `json:"github_com_profile"` + GithubComMemberRoles []string `json:"github_com_member_roles"` + GithubComEnterpriseRoles []string `json:"github_com_enterprise_roles"` + GithubComVerifiedDomainEmails []string `json:"github_com_verified_domain_emails"` + GithubComSamlNameID string `json:"github_com_saml_name_id"` + GithubComOrgsWithPendingInvites []string `json:"github_com_orgs_with_pending_invites"` + GithubComTwoFactorAuth []string `json:"github_com_two_factor_auth"` + EnterpriseServerEmails []string `json:"enterprise_server_emails"` + VisualStudioLicenseStatus string `json:"visual_studio_license_status"` + VisualStudioSubscriptionEmail string `json:"visual_studio_subscription_email"` + TotalUserAccounts int `json:"total_user_accounts"` + GithubComEnterpriseRole string `json:"github_com_enterprise_role,omitempty"` +} + +// EnterpriseConsumedLicenses represents information about users with consumed enterprise licenses. +type EnterpriseConsumedLicenses struct { + TotalSeatsConsumed int `json:"total_seats_consumed"` + TotalSeatsPurchased int `json:"total_seats_purchased"` + Users []*EnterpriseLicensedUsers `json:"users,omitempty"` +} + +// EnterpriseLicenseSyncStatus represents the synchronization status of +// GitHub Enterprise Server instances with an enterprise account. +type EnterpriseLicenseSyncStatus struct { + Title string `json:"title"` + Description string `json:"description"` + Properties *ServerInstanceProperties `json:"properties,omitempty"` +} + +// ServerInstanceProperties contains the collection of server instances. +type ServerInstanceProperties struct { + ServerInstances *ServerInstances `json:"server_instances,omitempty"` +} + +// ServerInstances represents a collection of GitHub Enterprise Server instances +// and their synchronization status. +type ServerInstances struct { + Type string `json:"type"` + Items *ServiceInstanceItems `json:"items,omitempty"` +} + +// ServiceInstanceItems defines the structure and properties of individual server instances +// in the collection. +type ServiceInstanceItems struct { + Type string `json:"type"` + Properties *ServerItemProperties `json:"properties,omitempty"` +} + +// ServerItemProperties represents the properties of a GitHub Enterprise Server instance, +// including its identifier, hostname, and last synchronization status. +type ServerItemProperties struct { + ServerID string `json:"server_id"` + Hostname string `json:"hostname"` + LastSync *LastLicenseSync `json:"last_sync,omitempty"` +} + +// LastLicenseSync contains information about the most recent license synchronization +// attempt for a server instance. +type LastLicenseSync struct { + Type string `json:"type"` + Properties *LastLicenseSyncProperties `json:"properties,omitempty"` +} + +// LastLicenseSyncProperties represents the details of the last synchronization attempt, +// including the date, status, and any error that occurred. +type LastLicenseSyncProperties struct { + Date *Timestamp `json:"date,omitempty"` + Status string `json:"status"` + Error string `json:"error"` +} + +// GetConsumedLicenses collect information about the number of consumed licenses and a collection with all the users with consumed enterprise licenses. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/license#list-enterprise-consumed-licenses +// +//meta:operation GET /enterprises/{enterprise}/consumed-licenses +func (s *EnterpriseService) GetConsumedLicenses(ctx context.Context, enterprise string, opts *ListOptions) (*EnterpriseConsumedLicenses, *Response, error) { + u := fmt.Sprintf("enterprises/%v/consumed-licenses", 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 + } + + consumedLicenses := &EnterpriseConsumedLicenses{} + resp, err := s.client.Do(ctx, req, &consumedLicenses) + if err != nil { + return nil, resp, err + } + + return consumedLicenses, resp, nil +} + +// GetLicenseSyncStatus collects information about the status of a license sync job for an enterprise. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/license#get-a-license-sync-status +// +//meta:operation GET /enterprises/{enterprise}/license-sync-status +func (s *EnterpriseService) GetLicenseSyncStatus(ctx context.Context, enterprise string) (*EnterpriseLicenseSyncStatus, *Response, error) { + u := fmt.Sprintf("enterprises/%v/license-sync-status", enterprise) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + syncStatus := &EnterpriseLicenseSyncStatus{} + resp, err := s.client.Do(ctx, req, &syncStatus) + if err != nil { + return nil, resp, err + } + + return syncStatus, resp, nil +} diff --git a/github/enterprise_licenses_test.go b/github/enterprise_licenses_test.go new file mode 100644 index 00000000000..cb2b1ab69b5 --- /dev/null +++ b/github/enterprise_licenses_test.go @@ -0,0 +1,187 @@ +// 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" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestEnterpriseService_GetConsumedLicenses(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/consumed-licenses", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"page": "2", "per_page": "10"}) + fmt.Fprint(w, `{ + "total_seats_consumed": 20, + "total_seats_purchased": 25, + "users": [{ + "github_com_login": "user1", + "github_com_name": "User One", + "enterprise_server_user_ids": ["123", "456"], + "github_com_user": true, + "enterprise_server_user": false, + "visual_studio_subscription_user": false, + "license_type": "Enterprise", + "github_com_profile": "https://github.com/user1", + "github_com_member_roles": ["member"], + "github_com_enterprise_roles": ["member"], + "github_com_verified_domain_emails": ["user1@example.com"], + "github_com_saml_name_id": "saml123", + "github_com_orgs_with_pending_invites": [], + "github_com_two_factor_auth": ["enabled"], + "enterprise_server_emails": ["user1@enterprise.local"], + "visual_studio_license_status": "active", + "visual_studio_subscription_email": "user1@visualstudio.com", + "total_user_accounts": 1, + "github_com_enterprise_role": "owner" + }] + }`) + }) + + opt := &ListOptions{Page: 2, PerPage: 10} + ctx := t.Context() + licenses, _, err := client.Enterprise.GetConsumedLicenses(ctx, "e", opt) + if err != nil { + t.Errorf("Enterprise.GetConsumedLicenses returned error: %v", err) + } + + want := &EnterpriseConsumedLicenses{ + TotalSeatsConsumed: 20, + TotalSeatsPurchased: 25, + Users: []*EnterpriseLicensedUsers{ + { + GithubComLogin: "user1", + GithubComName: "User One", + EnterpriseServerUserIDs: []string{"123", "456"}, + GithubComUser: true, + EnterpriseServerUser: false, + VisualStudioSubscriptionUser: false, + LicenseType: "Enterprise", + GithubComProfile: "https://github.com/user1", + GithubComMemberRoles: []string{"member"}, + GithubComEnterpriseRoles: []string{"member"}, + GithubComVerifiedDomainEmails: []string{"user1@example.com"}, + GithubComSamlNameID: "saml123", + GithubComOrgsWithPendingInvites: []string{}, + GithubComTwoFactorAuth: []string{"enabled"}, + EnterpriseServerEmails: []string{"user1@enterprise.local"}, + VisualStudioLicenseStatus: "active", + VisualStudioSubscriptionEmail: "user1@visualstudio.com", + TotalUserAccounts: 1, + GithubComEnterpriseRole: "owner", + }, + }, + } + + if !cmp.Equal(licenses, want) { + t.Errorf("Enterprise.GetConsumedLicenses returned %+v, want %+v", licenses, want) + } + + const methodName = "GetConsumedLicenses" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.GetConsumedLicenses(ctx, "\n", opt) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.GetConsumedLicenses(ctx, "e", opt) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_GetLicenseSyncStatus(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/license-sync-status", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "title": "Enterprise License Sync Status", + "description": "Status of license synchronization", + "properties": { + "server_instances": { + "type": "array", + "items": { + "type": "object", + "properties": { + "server_id": "ghes-1", + "hostname": "github.enterprise.local", + "last_sync": { + "type": "object", + "properties": { + "date": "2025-10-30T10:30:00Z", + "status": "success", + "error": "" + } + } + } + } + } + } + }`) + }) + + ctx := t.Context() + syncStatus, _, err := client.Enterprise.GetLicenseSyncStatus(ctx, "e") + if err != nil { + t.Errorf("Enterprise.GetLicenseSyncStatus returned error: %v", err) + } + + want := &EnterpriseLicenseSyncStatus{ + Title: "Enterprise License Sync Status", + Description: "Status of license synchronization", + Properties: &ServerInstanceProperties{ + ServerInstances: &ServerInstances{ + Type: "array", + Items: &ServiceInstanceItems{ + Type: "object", + Properties: &ServerItemProperties{ + ServerID: "ghes-1", + Hostname: "github.enterprise.local", + LastSync: &LastLicenseSync{ + Type: "object", + Properties: &LastLicenseSyncProperties{ + Date: &Timestamp{time.Date(2025, 10, 30, 10, 30, 0, 0, time.UTC)}, + Status: "success", + Error: "", + }, + }, + }, + }, + }, + }, + } + + fmt.Printf("%v\n", cmp.Diff(want, syncStatus)) + + if !cmp.Equal(syncStatus, want) { + t.Errorf("Enterprise.GetLicenseSyncStatus returned %+v, want %+v", syncStatus, want) + } + + const methodName = "GetLicenseSyncStatus" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.GetLicenseSyncStatus(ctx, "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.GetLicenseSyncStatus(ctx, "e") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index 0a8f83ee2e1..8359aeb80ef 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -9078,6 +9078,14 @@ func (e *Enterprise) GetWebsiteURL() string { return *e.WebsiteURL } +// GetProperties returns the Properties field. +func (e *EnterpriseLicenseSyncStatus) GetProperties() *ServerInstanceProperties { + if e == nil { + return nil + } + return e.Properties +} + // GetAllowsPublicRepositories returns the AllowsPublicRepositories field if it's non-nil, zero value otherwise. func (e *EnterpriseRunnerGroup) GetAllowsPublicRepositories() bool { if e == nil || e.AllowsPublicRepositories == nil { @@ -13422,6 +13430,22 @@ func (l *LargeFile) GetSize() int { return *l.Size } +// GetProperties returns the Properties field. +func (l *LastLicenseSync) GetProperties() *LastLicenseSyncProperties { + if l == nil { + return nil + } + return l.Properties +} + +// GetDate returns the Date field if it's non-nil, zero value otherwise. +func (l *LastLicenseSyncProperties) GetDate() Timestamp { + if l == nil || l.Date == nil { + return Timestamp{} + } + return *l.Date +} + // GetBody returns the Body field if it's non-nil, zero value otherwise. func (l *License) GetBody() string { if l == nil || l.Body == nil { @@ -26526,6 +26550,38 @@ func (s *SelfHostRunnerPermissionsEnterprise) GetDisableSelfHostedRunnersForAllO return *s.DisableSelfHostedRunnersForAllOrgs } +// GetServerInstances returns the ServerInstances field. +func (s *ServerInstanceProperties) GetServerInstances() *ServerInstances { + if s == nil { + return nil + } + return s.ServerInstances +} + +// GetItems returns the Items field. +func (s *ServerInstances) GetItems() *ServiceInstanceItems { + if s == nil { + return nil + } + return s.Items +} + +// GetLastSync returns the LastSync field. +func (s *ServerItemProperties) GetLastSync() *LastLicenseSync { + if s == nil { + return nil + } + return s.LastSync +} + +// GetProperties returns the Properties field. +func (s *ServiceInstanceItems) GetProperties() *ServerItemProperties { + if s == nil { + return nil + } + return s.Properties +} + // GetFrom returns the From field if it's non-nil, zero value otherwise. func (s *SignatureRequirementEnforcementLevelChanges) GetFrom() string { if s == nil || s.From == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 3c78a1436b8..de28843d2fe 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -11749,6 +11749,14 @@ func TestEnterprise_GetWebsiteURL(tt *testing.T) { e.GetWebsiteURL() } +func TestEnterpriseLicenseSyncStatus_GetProperties(tt *testing.T) { + tt.Parallel() + e := &EnterpriseLicenseSyncStatus{} + e.GetProperties() + e = nil + e.GetProperties() +} + func TestEnterpriseRunnerGroup_GetAllowsPublicRepositories(tt *testing.T) { tt.Parallel() var zeroValue bool @@ -17392,6 +17400,25 @@ func TestLargeFile_GetSize(tt *testing.T) { l.GetSize() } +func TestLastLicenseSync_GetProperties(tt *testing.T) { + tt.Parallel() + l := &LastLicenseSync{} + l.GetProperties() + l = nil + l.GetProperties() +} + +func TestLastLicenseSyncProperties_GetDate(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + l := &LastLicenseSyncProperties{Date: &zeroValue} + l.GetDate() + l = &LastLicenseSyncProperties{} + l.GetDate() + l = nil + l.GetDate() +} + func TestLicense_GetBody(tt *testing.T) { tt.Parallel() var zeroValue string @@ -34159,6 +34186,38 @@ func TestSelfHostRunnerPermissionsEnterprise_GetDisableSelfHostedRunnersForAllOr s.GetDisableSelfHostedRunnersForAllOrgs() } +func TestServerInstanceProperties_GetServerInstances(tt *testing.T) { + tt.Parallel() + s := &ServerInstanceProperties{} + s.GetServerInstances() + s = nil + s.GetServerInstances() +} + +func TestServerInstances_GetItems(tt *testing.T) { + tt.Parallel() + s := &ServerInstances{} + s.GetItems() + s = nil + s.GetItems() +} + +func TestServerItemProperties_GetLastSync(tt *testing.T) { + tt.Parallel() + s := &ServerItemProperties{} + s.GetLastSync() + s = nil + s.GetLastSync() +} + +func TestServiceInstanceItems_GetProperties(tt *testing.T) { + tt.Parallel() + s := &ServiceInstanceItems{} + s.GetProperties() + s = nil + s.GetProperties() +} + func TestSignatureRequirementEnforcementLevelChanges_GetFrom(tt *testing.T) { tt.Parallel() var zeroValue string