diff --git a/Octokit.Reactive/Clients/Copilot/IObservableCopilotClient.cs b/Octokit.Reactive/Clients/Copilot/IObservableCopilotClient.cs new file mode 100644 index 0000000000..6366ccdcab --- /dev/null +++ b/Octokit.Reactive/Clients/Copilot/IObservableCopilotClient.cs @@ -0,0 +1,23 @@ +using System; + +namespace Octokit.Reactive +{ + /// + /// Access GitHub's Copilot for Business API. + /// + public interface IObservableCopilotClient + { + /// + /// Returns a summary of the Copilot for Business configuration for an organization. Includes a seat + /// details summary of the current billing cycle, and the mode of seat management. + /// + /// the organization name to retrieve billing settings for + /// A instance + IObservable GetSummaryForOrganization(string organization); + + /// + /// For checking and managing licenses for GitHub Copilot for Business + /// + IObservableCopilotLicenseClient Licensing { get; } + } +} diff --git a/Octokit.Reactive/Clients/Copilot/IObservableCopilotLicenseClient.cs b/Octokit.Reactive/Clients/Copilot/IObservableCopilotLicenseClient.cs new file mode 100644 index 0000000000..333bd93f29 --- /dev/null +++ b/Octokit.Reactive/Clients/Copilot/IObservableCopilotLicenseClient.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace Octokit.Reactive +{ + /// + /// A client for managing licenses for GitHub Copilot for Business + /// + public interface IObservableCopilotLicenseClient + { + /// + /// Removes a license for a user + /// + /// The organization name + /// The github users profile name to remove a license from + /// A instance with results + IObservable Remove(string organization, string userName); + + /// + /// Removes a license for one or many users + /// + /// The organization name + /// A instance, containing the names of the user(s) to remove licenses for + /// A instance with results + IObservable Remove(string organization, UserSeatAllocation userSeatAllocation); + + /// + /// Assigns a license to a user + /// + /// The organization name + /// The github users profile name to add a license to + /// A instance with results + IObservable Assign(string organization, string userName); + + /// + /// Assigns a license for one or many users + /// + /// The organization name + /// A instance, containing the names of the user(s) to add licenses to + /// A instance with results + IObservable Assign(string organization, UserSeatAllocation userSeatAllocation); + + /// + /// Gets all of the currently allocated licenses for an organization + /// + /// The organization + /// The api options to use when making the API call, such as paging + /// A instance containing the currently allocated user licenses + IObservable GetAll(string organization, ApiOptions options); + } +} diff --git a/Octokit.Reactive/Clients/Copilot/ObservableCopilotClient.cs b/Octokit.Reactive/Clients/Copilot/ObservableCopilotClient.cs new file mode 100644 index 0000000000..bd27fb3a37 --- /dev/null +++ b/Octokit.Reactive/Clients/Copilot/ObservableCopilotClient.cs @@ -0,0 +1,47 @@ +using System; +using System.Reactive.Threading.Tasks; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Copilot for Business API. + /// Allows listing, creating, and deleting Copilot licenses. + /// + /// + /// See the Copilot for Business API documentation for more information. + /// + public class ObservableCopilotClient : IObservableCopilotClient + { + private readonly ICopilotClient _client; + + /// + /// Instantiates a new GitHub Copilot API client. + /// + /// + public ObservableCopilotClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, nameof(client)); + + _client = client.Copilot; + Licensing = new ObservableCopilotLicenseClient(client); + } + + /// + /// Returns a summary of the Copilot for Business configuration for an organization. Includes a seat + /// details summary of the current billing cycle, and the mode of seat management. + /// + /// the organization name to retrieve billing settings for + /// A instance + public IObservable GetSummaryForOrganization(string organization) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + + return _client.GetSummaryForOrganization(organization).ToObservable(); + } + + /// + /// Client for maintaining Copilot licenses for users in an organization. + /// + public IObservableCopilotLicenseClient Licensing { get; private set; } + } +} \ No newline at end of file diff --git a/Octokit.Reactive/Clients/Copilot/ObservableCopilotLicenseClient.cs b/Octokit.Reactive/Clients/Copilot/ObservableCopilotLicenseClient.cs new file mode 100644 index 0000000000..ead9377e91 --- /dev/null +++ b/Octokit.Reactive/Clients/Copilot/ObservableCopilotLicenseClient.cs @@ -0,0 +1,90 @@ +using System; +using System.Reactive.Threading.Tasks; +using Octokit; +using Octokit.Reactive; +using Octokit.Reactive.Internal; + +/// +/// A client for managing licenses for GitHub Copilot for Business +/// +public class ObservableCopilotLicenseClient : IObservableCopilotLicenseClient +{ + private readonly ICopilotLicenseClient _client; + private readonly IConnection _connection; + + public ObservableCopilotLicenseClient(IGitHubClient client) + { + _client = client.Copilot.Licensing; + _connection = client.Connection; + } + + /// + /// Removes a license for a user + /// + /// The organization name + /// The github users profile name to remove a license from + /// A instance with results + public IObservable Remove(string organization, string userName) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(userName, nameof(userName)); + + return _client.Remove(organization, userName).ToObservable(); + } + + /// + /// Removes a license for one or many users + /// + /// The organization name + /// A instance, containing the names of the user(s) to remove licenses for + /// A instance with results + public IObservable Remove(string organization, UserSeatAllocation userSeatAllocation) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(userSeatAllocation, nameof(userSeatAllocation)); + + return _client.Remove(organization, userSeatAllocation).ToObservable(); + } + + /// + /// Assigns a license to a user + /// + /// The organization name + /// The github users profile name to add a license to + /// A instance with results + public IObservable Assign(string organization, string userName) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(userName, nameof(userName)); + + return _client.Assign(organization, userName).ToObservable(); + } + + /// + /// Assigns a license for one or many users + /// + /// The organization name + /// A instance, containing the names of the user(s) to add licenses to + /// A instance with results + public IObservable Assign(string organization, UserSeatAllocation userSeatAllocation) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(userSeatAllocation, nameof(userSeatAllocation)); + + return _client.Assign(organization, userSeatAllocation).ToObservable(); + } + + /// + /// Gets all of the currently allocated licenses for an organization + /// + /// The organization + /// Options to control page size when making API requests + /// A list of instance containing the currently allocated user licenses. + public IObservable GetAll(string organization, ApiOptions options) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(options, nameof(options)); + + return _connection.GetAndFlattenAllPages( ApiUrls.CopilotAllocatedLicenses(organization), options); + } +} diff --git a/Octokit.Reactive/IObservableGitHubClient.cs b/Octokit.Reactive/IObservableGitHubClient.cs index bc610f7fca..68fb81a6a5 100644 --- a/Octokit.Reactive/IObservableGitHubClient.cs +++ b/Octokit.Reactive/IObservableGitHubClient.cs @@ -43,5 +43,6 @@ public interface IObservableGitHubClient : IApiInfoProvider IObservableMetaClient Meta { get; } IObservableActionsClient Actions { get; } IObservableCodespacesClient Codespaces { get; } + IObservableCopilotClient Copilot { get; } } } diff --git a/Octokit.Reactive/ObservableGitHubClient.cs b/Octokit.Reactive/ObservableGitHubClient.cs index 7b8da221a3..2713ad12ce 100644 --- a/Octokit.Reactive/ObservableGitHubClient.cs +++ b/Octokit.Reactive/ObservableGitHubClient.cs @@ -58,6 +58,7 @@ public ObservableGitHubClient(IGitHubClient gitHubClient) Meta = new ObservableMetaClient(gitHubClient); Actions = new ObservableActionsClient(gitHubClient); Codespaces = new ObservableCodespacesClient(gitHubClient); + Copilot = new ObservableCopilotClient(gitHubClient); } public IConnection Connection @@ -105,8 +106,9 @@ public void SetRequestTimeout(TimeSpan timeout) public IObservableRateLimitClient RateLimit { get; private set; } public IObservableMetaClient Meta { get; private set; } public IObservableActionsClient Actions { get; private set; } - public IObservableCodespacesClient Codespaces { get; private set; } + public IObservableCopilotClient Copilot { get; set; } + /// /// Gets the latest API Info - this will be null if no API calls have been made /// diff --git a/Octokit.Tests.Integration/Clients/Copilot/CopilotClientTests.cs b/Octokit.Tests.Integration/Clients/Copilot/CopilotClientTests.cs new file mode 100644 index 0000000000..2a42d0fc81 --- /dev/null +++ b/Octokit.Tests.Integration/Clients/Copilot/CopilotClientTests.cs @@ -0,0 +1,117 @@ +using System.Threading.Tasks; +using Octokit.Tests.Integration.Helpers; +using Xunit; + +namespace Octokit.Tests.Integration.Clients.Copilot +{ + public class CopilotClientTests + { + public class TheGetBillingSettingsMethod + { + private readonly IGitHubClient _gitHub; + + public TheGetBillingSettingsMethod() + { + _gitHub = Helper.GetAuthenticatedClient(); + } + + [OrganizationTest] + public async Task ReturnsBillingSettingsData() + { + var billingSettings = await _gitHub.Copilot.GetSummaryForOrganization(Helper.Organization); + + Assert.NotNull(billingSettings.SeatManagementSetting); + Assert.NotNull(billingSettings.PublicCodeSuggestions); + } + } + + public class TheGetAllLicensesMethod + { + private readonly IGitHubClient _gitHub; + + public TheGetAllLicensesMethod() + { + _gitHub = Helper.GetAuthenticatedClient(); + } + + [OrganizationTest] + public async Task ReturnsUserCopilotLicenseDetailsAsList() + { + using (var context = await _gitHub.CreateCopilotUserLicenseContext(Helper.Organization, Helper.UserName)) + { + var licenses = await _gitHub.Copilot.Licensing.GetAll(Helper.Organization, new ApiOptions()); + + Assert.True(licenses.Count > 0); + } + } + } + + public class TheAddLicenseMethod + { + private readonly IGitHubClient _gitHub; + + public TheAddLicenseMethod() + { + _gitHub = Helper.GetAuthenticatedClient(); + } + + [OrganizationTest] + public async Task AddsLicenseForUser() + { + using (var context = await _gitHub.CreateCopilotUserLicenseContext(Helper.Organization, Helper.UserName)) + { + var allocation = await _gitHub.Copilot.Licensing.Assign(Helper.Organization, Helper.UserName); + + Assert.True(allocation.SeatsCreated > 0); + } + } + + [OrganizationTest] + public async Task AddsLicenseForUsers() + { + using (var context = await _gitHub.CreateCopilotUserLicenseContext(Helper.Organization, Helper.UserName)) + { + var seatAllocation = new UserSeatAllocation() { SelectedUsernames = new[] { Helper.UserName } }; + + var allocation = await _gitHub.Copilot.Licensing.Assign(Helper.Organization, seatAllocation); + + Assert.True(allocation.SeatsCreated > 0); + } + } + } + + public class TheDeleteLicenseMethod + { + private readonly IGitHubClient _gitHub; + + public TheDeleteLicenseMethod() + { + _gitHub = Helper.GetAuthenticatedClient(); + } + + [OrganizationTest] + public async Task RemovesLicenseForUser() + { + using (var context = await _gitHub.CreateCopilotUserLicenseContext(Helper.Organization, Helper.UserName)) + { + var allocation = await _gitHub.Copilot.Licensing.Remove(Helper.Organization, Helper.UserName); + + Assert.True(allocation.SeatsCancelled > 0); + } + } + + [OrganizationTest] + public async Task RemovesLicenseForUsers() + { + using (var context = await _gitHub.CreateCopilotUserLicenseContext(Helper.Organization, Helper.UserName)) + { + var seatAllocation = new UserSeatAllocation() { SelectedUsernames = new[] { Helper.UserName } }; + + var allocation = await _gitHub.Copilot.Licensing.Remove(Helper.Organization, seatAllocation); + + Assert.True(allocation.SeatsCancelled > 0); + } + } + } + } +} diff --git a/Octokit.Tests.Integration/Helpers/CopilotHelper.cs b/Octokit.Tests.Integration/Helpers/CopilotHelper.cs new file mode 100644 index 0000000000..6c18153c2a --- /dev/null +++ b/Octokit.Tests.Integration/Helpers/CopilotHelper.cs @@ -0,0 +1,13 @@ +using System; + +namespace Octokit.Tests.Integration.Helpers +{ + internal sealed class CopilotHelper + { + public static void RemoveUserLicense(IConnection connection, string organization, string userLogin) + { + var client = new GitHubClient(connection); + client.Copilot.Licensing.Remove(organization, userLogin).Wait(TimeSpan.FromSeconds(15)); + } + } +} diff --git a/Octokit.Tests.Integration/Helpers/CopilotUserLicenseContext.cs b/Octokit.Tests.Integration/Helpers/CopilotUserLicenseContext.cs new file mode 100644 index 0000000000..a6e7488984 --- /dev/null +++ b/Octokit.Tests.Integration/Helpers/CopilotUserLicenseContext.cs @@ -0,0 +1,24 @@ +using System; + +namespace Octokit.Tests.Integration.Helpers +{ + internal sealed class CopilotUserLicenseContext : IDisposable + { + internal CopilotUserLicenseContext(IConnection connection, string organization, string user) + { + _connection = connection; + Organization = organization; + UserLogin = user; + } + + private readonly IConnection _connection; + + internal string Organization { get; } + internal string UserLogin { get; private set; } + + public void Dispose() + { + CopilotHelper.RemoveUserLicense(_connection, Organization, UserLogin); + } + } +} diff --git a/Octokit.Tests.Integration/Helpers/GithubClientExtensions.cs b/Octokit.Tests.Integration/Helpers/GithubClientExtensions.cs index 941aeb9371..37e5e9599e 100644 --- a/Octokit.Tests.Integration/Helpers/GithubClientExtensions.cs +++ b/Octokit.Tests.Integration/Helpers/GithubClientExtensions.cs @@ -141,6 +141,13 @@ internal static async Task CreateEnterpriseUserContext(th return new EnterpriseUserContext(client.Connection, user); } + + internal static async Task CreateCopilotUserLicenseContext(this IGitHubClient client, string organization, string userName) + { + await client.Copilot.Licensing.Assign(organization, userName); + + return new CopilotUserLicenseContext(client.Connection, organization, userName); + } internal static async Task CreatePublicKeyContext(this IGitHubClient client) { diff --git a/Octokit.Tests/Clients/Copilot/CopilotClientTests.cs b/Octokit.Tests/Clients/Copilot/CopilotClientTests.cs new file mode 100644 index 0000000000..bac7083935 --- /dev/null +++ b/Octokit.Tests/Clients/Copilot/CopilotClientTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using NSubstitute; +using Xunit; + +namespace Octokit.Tests.Clients +{ + public class CopilotClientTests + { + private const string orgName = "test"; + + public class TheGetCopilotBillingSettingsMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new CopilotClient(connection); + + var expectedUri = $"orgs/{orgName}/copilot/billing"; + client.GetSummaryForOrganization("test"); + + connection.Received().Get(Arg.Is(u => u.ToString() == expectedUri)); + } + } + + public class TheGetAllCopilotLicensesMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new CopilotClient(connection); + + var expectedUri = $"orgs/{orgName}/copilot/billing/seats"; + client.Licensing.GetAll("test", new ApiOptions()); + + connection.Received().GetAll(Arg.Is(u => u.ToString() == expectedUri), Arg.Any()); + } + } + + public class TheAssignCopilotLicenseMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new CopilotClient(connection); + var expectedUri = $"orgs/{orgName}/copilot/billing/selected_users"; + + client.Licensing.Assign(orgName, "copilot-user"); + + connection.Received().Post(Arg.Is(u => u.ToString() == expectedUri), Arg.Any()); + } + } + + public class TheAssignCopilotLicensesMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new CopilotClient(connection); + var expectedUri = $"orgs/{orgName}/copilot/billing/selected_users"; + + var payloadData = new UserSeatAllocation() { SelectedUsernames = new[] { "copilot-user" } }; + client.Licensing.Assign(orgName, payloadData); + + connection.Received().Post(Arg.Is(u => u.ToString() == expectedUri), payloadData); + } + } + + public class TheRemoveCopilotLicenseMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new CopilotClient(connection); + var expectedUri = $"orgs/{orgName}/copilot/billing/selected_users"; + + client.Licensing.Remove(orgName, "copilot-user" ); + + connection.Received().Delete(Arg.Is(u => u.ToString() == expectedUri), Arg.Any()); + } + } + + public class TheRemoveCopilotLicensesMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var connection = Substitute.For(); + var client = new CopilotClient(connection); + var expectedUri = $"orgs/{orgName}/copilot/billing/selected_users"; + + var payloadData = new UserSeatAllocation() { SelectedUsernames = new[] { "copilot-user" } }; + client.Licensing.Remove(orgName, payloadData); + + connection.Received().Delete(Arg.Is(u => u.ToString() == expectedUri), payloadData); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws( + () => new CopilotClient(null)); + } + } + } +} \ No newline at end of file diff --git a/Octokit.Tests/Reactive/Copilot/ObservableCopilotClientTests.cs b/Octokit.Tests/Reactive/Copilot/ObservableCopilotClientTests.cs new file mode 100644 index 0000000000..078633bb50 --- /dev/null +++ b/Octokit.Tests/Reactive/Copilot/ObservableCopilotClientTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using NSubstitute; +using Octokit.Reactive; +using Xunit; + +namespace Octokit.Tests.Reactive +{ + public class ObservableCopilotClientTests + { + private const string orgName = "test"; + + public class TheGetCopilotBillingSettingsMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableCopilotClient(githubClient); + + client.GetSummaryForOrganization("test"); + + githubClient.Copilot.Received(1).GetSummaryForOrganization(orgName); + } + } + + public class TheGetAllCopilotLicensesMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var endpoint = new Uri($"orgs/test/copilot/billing/seats", UriKind.Relative); + var connection = Substitute.For(); + var gitHubClient = Substitute.For(); + gitHubClient.Connection.Returns(connection); + var client = new ObservableCopilotClient(gitHubClient); + + var apiOptions = new ApiOptions() { PageSize = 50, PageCount = 10 }; + client.Licensing.GetAll("test", apiOptions); + + connection.Received().Get>(endpoint, + Arg.Is>(d => d.Count > 0)); + } + } + + public class TheAssignCopilotLicenseMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableCopilotClient(githubClient); + const string expectedUser = "copilot-user"; + + client.Licensing.Assign(orgName, expectedUser); + + githubClient.Copilot.Licensing.Received().Assign(orgName, expectedUser); + } + } + + public class TheAssignCopilotLicensesMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableCopilotClient(githubClient); + + var payloadData = new UserSeatAllocation() { SelectedUsernames = new[] { "copilot-user" } }; + client.Licensing.Assign(orgName, payloadData); + + githubClient.Copilot.Licensing.Received().Assign(orgName, payloadData); + } + } + + public class TheRemoveCopilotLicenseMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableCopilotClient(githubClient); + const string expectedUser = "copilot-user"; + + client.Licensing.Remove(orgName, expectedUser); + + githubClient.Copilot.Licensing.Received().Remove(orgName, expectedUser); + } + } + + public class TheRemoveCopilotLicensesMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var githubClient = Substitute.For(); + var client = new ObservableCopilotClient(githubClient); + + var payloadData = new UserSeatAllocation() { SelectedUsernames = new[] { "copilot-user" } }; + client.Licensing.Remove(orgName, payloadData); + + githubClient.Copilot.Licensing.Received().Remove(orgName, payloadData); + } + } + + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws( + () => new ObservableCopilotClient(null)); + } + } + } +} \ No newline at end of file diff --git a/Octokit/Clients/Copilot/CopilotClient.cs b/Octokit/Clients/Copilot/CopilotClient.cs new file mode 100644 index 0000000000..30c167ac8a --- /dev/null +++ b/Octokit/Clients/Copilot/CopilotClient.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's Copilot for Business API. + /// Allows listing, creating, and deleting Copilot licenses. + /// + /// + /// See the Copilot for Business API documentation for more information. + /// + public class CopilotClient : ApiClient, ICopilotClient + { + /// + /// Instantiates a new GitHub Copilot API client. + /// + /// + public CopilotClient(IApiConnection apiConnection) : base(apiConnection) + { + Licensing = new CopilotLicenseClient(apiConnection); + } + + /// + /// Returns a summary of the Copilot for Business configuration for an organization. Includes a seat + /// details summary of the current billing cycle, and the mode of seat management. + /// + /// the organization name to retrieve billing settings for + /// A instance + [ManualRoute("GET", "/orgs/{org}/copilot/billing")] + public async Task GetSummaryForOrganization(string organization) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + + return await ApiConnection.Get(ApiUrls.CopilotBillingSettings(organization)); + } + + /// + /// Client for maintaining Copilot licenses for users in an organization. + /// + public ICopilotLicenseClient Licensing { get; private set; } + } +} \ No newline at end of file diff --git a/Octokit/Clients/Copilot/CopilotLicenseClient.cs b/Octokit/Clients/Copilot/CopilotLicenseClient.cs new file mode 100644 index 0000000000..aaf61977aa --- /dev/null +++ b/Octokit/Clients/Copilot/CopilotLicenseClient.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Octokit; +using Octokit.Models.Request.Enterprise; + +/// +/// A client for managing licenses for GitHub Copilot for Business +/// +public class CopilotLicenseClient : ApiClient, ICopilotLicenseClient +{ + /// + /// Initializes a new GitHub Copilot for Business License API client. + /// + /// An API connection + public CopilotLicenseClient(IApiConnection apiConnection) : base(apiConnection) + { + } + + /// + /// Removes a license for a user + /// + /// The organization name + /// The github users profile name to remove a license from + /// A instance with results + [ManualRoute("DELETE", "/orgs/{org}/copilot/billing/selected_users")] + public async Task Remove(string organization, string userName) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(userName, nameof(userName)); + + var allocation = new UserSeatAllocation + { + SelectedUsernames = new[] { userName } + }; + + return await Remove(organization, allocation); + } + + /// + /// Removes a license for one or many users + /// + /// The organization name + /// A instance, containing the names of the user(s) to remove licenses for + /// A instance with results + [ManualRoute("DELETE", "/orgs/{org}/copilot/billing/selected_users")] + public async Task Remove(string organization, UserSeatAllocation userSeatAllocation) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(userSeatAllocation, nameof(userSeatAllocation)); + + return await ApiConnection.Delete(ApiUrls.CopilotBillingLicense(organization), userSeatAllocation); + } + + /// + /// Assigns a license to a user + /// + /// The organization name + /// The github users profile name to add a license to + /// A instance with results + [ManualRoute("POST", "/orgs/{org}/copilot/billing/selected_users")] + public async Task Assign(string organization, string userName) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(userName, nameof(userName)); + + var allocation = new UserSeatAllocation + { + SelectedUsernames = new[] { userName } + }; + + return await Assign(organization, allocation); + } + + /// + /// Assigns a license for one or many users + /// + /// The organization name + /// A instance, containing the names of the user(s) to add licenses to + /// A instance with results + [ManualRoute("POST", "/orgs/{org}/copilot/billing/selected_users")] + public async Task Assign(string organization, UserSeatAllocation userSeatAllocation) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + Ensure.ArgumentNotNull(userSeatAllocation, nameof(userSeatAllocation)); + + return await ApiConnection.Post(ApiUrls.CopilotBillingLicense(organization), userSeatAllocation); + } + + /// + /// Gets all of the currently allocated licenses for an organization + /// + /// The organization + /// Options to control page size when making API requests + /// A list of instance containing the currently allocated user licenses. + [ManualRoute("GET", "/orgs/{org}/copilot/billing/seats")] + public async Task> GetAll(string organization, ApiOptions options) + { + Ensure.ArgumentNotNull(organization, nameof(organization)); + + var extendedOptions = new ApiOptionsExtended() + { + PageSize = options.PageSize + }; + + return await ApiConnection.GetAll(ApiUrls.CopilotAllocatedLicenses(organization), extendedOptions); + } +} diff --git a/Octokit/Clients/Copilot/ICopilotClient.cs b/Octokit/Clients/Copilot/ICopilotClient.cs new file mode 100644 index 0000000000..dd72ba7704 --- /dev/null +++ b/Octokit/Clients/Copilot/ICopilotClient.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// Access GitHub's Copilot for Business API. + /// + public interface ICopilotClient + { + /// + /// Returns a summary of the Copilot for Business configuration for an organization. Includes a seat + /// details summary of the current billing cycle, and the mode of seat management. + /// + /// the organization name to retrieve billing settings for + /// A instance + Task GetSummaryForOrganization(string organization); + + /// + /// For checking and managing licenses for GitHub Copilot for Business + /// + ICopilotLicenseClient Licensing { get; } + } +} diff --git a/Octokit/Clients/Copilot/ICopilotLicenseClient.cs b/Octokit/Clients/Copilot/ICopilotLicenseClient.cs new file mode 100644 index 0000000000..8c6ed2aebe --- /dev/null +++ b/Octokit/Clients/Copilot/ICopilotLicenseClient.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for managing licenses for GitHub Copilot for Business + /// + public interface ICopilotLicenseClient + { + /// + /// Removes a license for a user + /// + /// The organization name + /// The github users profile name to remove a license from + /// A instance with results + Task Remove(string organization, string userName); + + /// + /// Removes a license for one or many users + /// + /// The organization name + /// A instance, containing the names of the user(s) to remove licenses for + /// A instance with results + Task Remove(string organization, UserSeatAllocation userSeatAllocation); + + /// + /// Assigns a license to a user + /// + /// The organization name + /// The github users profile name to add a license to + /// A instance with results + Task Assign(string organization, string userName); + + /// + /// Assigns a license for one or many users + /// + /// The organization name + /// A instance, containing the names of the user(s) to add licenses to + /// A instance with results + Task Assign(string organization, UserSeatAllocation userSeatAllocation); + + /// + /// Gets all of the currently allocated licenses for an organization + /// + /// The organization + /// The api options to use when making the API call, such as paging + /// A instance containing the currently allocated user licenses + Task> GetAll(string organization, ApiOptions options); + } +} diff --git a/Octokit/GitHubClient.cs b/Octokit/GitHubClient.cs index 2a839b3b1f..0839d4c400 100644 --- a/Octokit/GitHubClient.cs +++ b/Octokit/GitHubClient.cs @@ -121,6 +121,7 @@ public GitHubClient(IConnection connection) Meta = new MetaClient(apiConnection); Actions = new ActionsClient(apiConnection); Codespaces = new CodespacesClient(apiConnection); + Copilot = new CopilotClient(apiConnection); } /// @@ -395,6 +396,11 @@ public Uri BaseAddress public IActionsClient Actions { get; private set; } public ICodespacesClient Codespaces { get; private set; } + + /// + /// Access GitHub's Copilot for Business API + /// + public ICopilotClient Copilot { get; private set; } static Uri FixUpBaseUri(Uri uri) { diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 41cb5ca958..98722edd20 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -5481,7 +5481,37 @@ public static Uri ActionsListOrganizationRunnerGroupRepositories(string org, lon { return "orgs/{0}/actions/runner-groups/{1}/repositories".FormatUri(org, runnerGroupId); } - + + /// + /// Returns the that handles adding or removing of copilot licenses for an organisation + /// + /// The name of the organization + /// A Uri Instance + public static Uri CopilotBillingLicense(string org) + { + return $"orgs/{org}/copilot/billing/selected_users".FormatUri(org); + } + + /// + /// Returns the that handles reading copilot billing settings for an organization + /// + /// The name of the organization + /// A Uri Instance + public static Uri CopilotBillingSettings(string org) + { + return $"orgs/{org}/copilot/billing".FormatUri(org); + } + + /// + /// Returns the that allows for searching across all licenses for an organisation + /// + /// + /// + public static Uri CopilotAllocatedLicenses(string org) + { + return $"orgs/{org}/copilot/billing/seats".FormatUri(org); + } + public static Uri Codespaces() { return _currentUserAllCodespaces; diff --git a/Octokit/IGitHubClient.cs b/Octokit/IGitHubClient.cs index 0fe8d3d3af..91ef11b772 100644 --- a/Octokit/IGitHubClient.cs +++ b/Octokit/IGitHubClient.cs @@ -217,5 +217,10 @@ public interface IGitHubClient : IApiInfoProvider IEmojisClient Emojis { get; } ICodespacesClient Codespaces { get; } + + /// + /// Access to the GitHub Copilot for Business API + /// + ICopilotClient Copilot { get; } } } diff --git a/Octokit/Models/Response/Copilot/BillingSettings.cs b/Octokit/Models/Response/Copilot/BillingSettings.cs new file mode 100644 index 0000000000..458e20ce40 --- /dev/null +++ b/Octokit/Models/Response/Copilot/BillingSettings.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + /// + /// The billing settings for a Copilot-enabled organization. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public partial class BillingSettings + { + public BillingSettings() + { + } + + public BillingSettings(SeatBreakdown seatBreakdown, string seatManagementSetting, string publicCodeSuggestions) + { + SeatBreakdown = seatBreakdown; + SeatManagementSetting = seatManagementSetting; + PublicCodeSuggestions = publicCodeSuggestions; + } + + /// + /// A summary of the current billing settings for the organization. + /// + public SeatBreakdown SeatBreakdown { get; private set; } + + /// + /// A string that indicates how seats are billed for the organization. + /// + public string SeatManagementSetting { get; private set; } + + /// + /// A string that indicates if public code suggestions are enabled or blocked for the organization. + /// + public string PublicCodeSuggestions { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "SeatManagementSetting: {0}, PublicCodeSuggestions: {1}", SeatManagementSetting, PublicCodeSuggestions); + } + } + } +} + diff --git a/Octokit/Models/Response/Copilot/CopilotSeat.cs b/Octokit/Models/Response/Copilot/CopilotSeat.cs new file mode 100644 index 0000000000..3be9cb673b --- /dev/null +++ b/Octokit/Models/Response/Copilot/CopilotSeat.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + /// + /// Details about a Copilot seat allocated to an organization member. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class CopilotSeat + { + public CopilotSeat() + { + } + + public CopilotSeat(DateTimeOffset? createdAt, DateTimeOffset? updatedAt, string pendingCancellationDate, DateTimeOffset? lastActivityAt, string lastActivityEditor, User assignee, Team assigningTeam) + { + CreatedAt = createdAt; + UpdatedAt = updatedAt; + PendingCancellationDate = pendingCancellationDate; + LastActivityAt = lastActivityAt; + LastActivityEditor = lastActivityEditor; + Assignee = assignee; + AssigningTeam = assigningTeam; + } + + /// + /// Timestamp of when the assignee was last granted access to GitHub Copilot, in ISO 8601 format + /// + public DateTimeOffset? CreatedAt { get; private set; } + + /// + /// Timestamp of when the assignee's GitHub Copilot access was last updated, in ISO 8601 format. + /// + public DateTimeOffset? UpdatedAt { get; private set; } + + /// + /// The pending cancellation date for the seat, in `YYYY-MM-DD` format. This will be null unless + /// the assignee's Copilot access has been canceled during the current billing cycle. + /// If the seat has been cancelled, this corresponds to the start of the organization's next billing cycle. + /// + public string PendingCancellationDate { get; private set; } + + /// + /// Timestamp of user's last GitHub Copilot activity, in ISO 8601 format. + /// + public DateTimeOffset? LastActivityAt { get; private set; } + + /// + /// Last editor that was used by the user for a GitHub Copilot completion. + /// + public string LastActivityEditor { get; private set; } + + /// + /// The assignee that has been granted access to GitHub Copilot + /// + public User Assignee { get; private set; } + + /// + /// The team that granted access to GitHub Copilot to the assignee. This will be null if the + /// user was assigned a seat individually. + /// + public Team AssigningTeam { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "User: {0}, CreatedAt: {1}", Assignee.Name, CreatedAt); + } + } + } +} \ No newline at end of file diff --git a/Octokit/Models/Response/Copilot/CopilotSeatAllocation.cs b/Octokit/Models/Response/Copilot/CopilotSeatAllocation.cs new file mode 100644 index 0000000000..726f9898f1 --- /dev/null +++ b/Octokit/Models/Response/Copilot/CopilotSeatAllocation.cs @@ -0,0 +1,41 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + + /// + /// Holds information about an API response after adding or removing seats for a Copilot-enabled organization. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class CopilotSeatAllocation + { + public CopilotSeatAllocation() + { + } + + public CopilotSeatAllocation(long seatsCancelled, long seatsCreated) + { + SeatsCancelled = seatsCancelled; + SeatsCreated = seatsCreated; + } + + /// + /// The total number of seat assignments removed. + /// + public long SeatsCancelled { get; private set; } + + /// + /// The total number of seat assignments created. + /// + public long SeatsCreated { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "SeatsCancelled: {0}, SeatsCreated: {1}", SeatsCancelled, SeatsCreated); + } + } + } +} \ No newline at end of file diff --git a/Octokit/Models/Response/Copilot/CopilotSeats.cs b/Octokit/Models/Response/Copilot/CopilotSeats.cs new file mode 100644 index 0000000000..97a9ab3043 --- /dev/null +++ b/Octokit/Models/Response/Copilot/CopilotSeats.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class CopilotSeats + { + public CopilotSeats() + { + } + + public CopilotSeats(int totalSeats, IReadOnlyList seats) + { + TotalSeats = totalSeats; + Seats = seats; + } + + /// + /// Total number of Copilot For Business seats for the organization currently being billed + /// + public long TotalSeats { get; private set; } + + /// + /// Information about a Copilot Business seat assignment for a user, team, or organization. + /// + + public IReadOnlyList Seats { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "TotalSeats: {0}", TotalSeats); + } + } + } +} \ No newline at end of file diff --git a/Octokit/Models/Response/Copilot/SeatBreakdown.cs b/Octokit/Models/Response/Copilot/SeatBreakdown.cs new file mode 100644 index 0000000000..a8f40732f0 --- /dev/null +++ b/Octokit/Models/Response/Copilot/SeatBreakdown.cs @@ -0,0 +1,64 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + /// + /// The breakdown of Copilot Business seats for the organization. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class SeatBreakdown + { + public SeatBreakdown() + { + } + + public SeatBreakdown(long total, long addedThisCycle, long pendingInvitation, long pendingCancellation, long activeThisCycle, long inactiveThisCycle) + { + Total = total; + AddedThisCycle = addedThisCycle; + PendingInvitation = pendingInvitation; + PendingCancellation = pendingCancellation; + ActiveThisCycle = activeThisCycle; + InactiveThisCycle = inactiveThisCycle; + } + + /// + /// The total number of seats being billed for the organization as of the current billing cycle. + /// + public long Total { get; private set; } + + /// + /// Seats added during the current billing cycle + /// + public long AddedThisCycle { get; private set; } + + /// + /// The number of seats that have been assigned to users that have not yet accepted an invitation to this organization. + /// + public long PendingInvitation { get; private set; } + + /// + /// The number of seats that are pending cancellation at the end of the current billing cycle. + /// + public long PendingCancellation { get; private set; } + + /// + /// The number of seats that have used Copilot during the current billing cycle. + /// + public long ActiveThisCycle { get; private set; } + + /// + /// The number of seats that have not used Copilot during the current billing cycle + /// + public long InactiveThisCycle { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "Total: {0}", Total); + } + } + } +} \ No newline at end of file diff --git a/Octokit/Models/Response/Copilot/UserSeatAllocation.cs b/Octokit/Models/Response/Copilot/UserSeatAllocation.cs new file mode 100644 index 0000000000..07805b4c11 --- /dev/null +++ b/Octokit/Models/Response/Copilot/UserSeatAllocation.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + /// + /// Holds information about user names to be added or removed from a Copilot-enabled organization. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class UserSeatAllocation + { + /// + /// One or more usernames to be added or removed from a Copilot-enabled organization. + /// + public string[] SelectedUsernames { get; set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "SelectedUsernames: {0}", string.Join(",", SelectedUsernames)); + } + } + } +} \ No newline at end of file